当前位置: 首页 > article >正文

【Android项目学习】2.抖音二级评论

项目链接资料

文章目录

  • 一. 项目结构
  • 二. Kotlin语法及Android技术栈学习
    • 1. Sealed Interface
    • 2. 协程 suspendCoroutine
    • 3. ListAdapter常用方法
    • 4. invoke
    • 5. Reducer
    • 6. 扩展函数语法
      • 解析 `val reduce: suspend List<CommentItem>.() -> List<CommentItem>`
      • 示例
      • 小结
    • 7. typealias别名
  • 三. 小功能实现
    • 1. 实现热评功能 (扩展函数 + buildSpannedString)
    • 2. 实现回复框 (Diaglog + suspendCoroutine)
  • 四. 项目解析
    • 1. 数据类的设计
    • 2. 首页:Recyleview
    • 3. Adapter: ListAdapter
    • 4. Reducer
    • 5. FakeApi

一. 项目结构

单 RecyclerView+多 ItemType+ListAdapter 框架、数据源转换、异步处理 + Reducer
在这里插入图片描述

二. Kotlin语法及Android技术栈学习

总结在该文章

1. Sealed Interface

2. 协程 suspendCoroutine

3. ListAdapter常用方法

4. invoke

5. Reducer

6. 扩展函数语法

是的,val reduce: suspend List<CommentItem>.() -> List<CommentItem> 这个声明确实是一个扩展函数类型的定义。让我们来详细分析一下这个声明的含义和用法。

解析 val reduce: suspend List<CommentItem>.() -> List<CommentItem>

  • 扩展接收者: List<CommentItem>.() 表示这是一个针对 List<CommentItem> 的扩展函数。即这个函数可以在一个 List<CommentItem> 的实例上被调用。

  • 挂起函数: suspend 关键字表示这个函数是一个挂起函数(suspend function),意味着它可以在协程中被调用,并能够在执行过程中暂停和恢复。挂起函数通常用于处理异步操作,例如网络请求或数据库操作。

  • 返回类型: -> List<CommentItem> 表示这个函数的返回值是一个 List<CommentItem>

示例

为了更好地理解这个扩展函数类型,我们可以看一个示例,这个示例展示了如何定义和使用这个类型的扩展函数:

data class CommentItem(val id: Int, val content: String)

// 定义一个挂起扩展函数
val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {
    // 这里可以处理列表中的 CommentItem
    // 例如,我们只保留内容长度大于3的评论
    this.filter { it.content.length > 3 }
}

// 使用协程来调用这个扩展函数
import kotlinx.coroutines.*

fun main() = runBlocking {
    val comments = listOf(
        CommentItem(1, "Hi"),
        CommentItem(2, "Hello"),
        CommentItem(3, "Kotlin"),
        CommentItem(4, "World")
    )

    // 调用扩展函数
    val filteredComments = comments.reduce() // 调用 reduce 扩展函数
    println(filteredComments) // 输出: [CommentItem(id=2, content=Hello), CommentItem(id=3, content=Kotlin), CommentItem(id=4, content=World)]
}

小结

在这个示例中,reduce 是一个对 List<CommentItem> 的扩展函数类型,它可以在协程中被调用。通过使用 this 关键字,我们可以访问扩展函数接收者(List<CommentItem>)的实例,并对其进行操作。

这种结构非常有用,尤其是在 Kotlin 的协程环境中,它允许我们编写简洁且可读性高的代码来处理集合或其他类型的数据。

7. typealias别名

三. 小功能实现

1. 实现热评功能 (扩展函数 + buildSpannedString)

content = if (entity.hot) entity.content.makeHot() else entity.content,

fun CharSequence.makeHot(): CharSequence {
    return buildSpannedString {
        color(Color.RED) {
            append("热评  ")
        }
        append(this@makeHot)
    }
}

2. 实现回复框 (Diaglog + suspendCoroutine)

见本文4.4 Reducer

四. 项目解析

1. 数据类的设计

  • ICommentEntity (接口 + data class)
interface ICommentEntity {
    val id: Int
    val content: CharSequence
    val userId: Int
    val userName: CharSequence
}

data class CommentLevel1(
    override val id: Int,
    override val content: CharSequence,
    override val userId: Int,
    override val userName: CharSequence,
    val level2Count: Int,
) : ICommentEntity

data class CommentLevel2(
    override val id: Int,
    override val content: CharSequence,
    override val userId: Int,
    override val userName: CharSequence,
    val parentId: Int,
    val hot: Boolean = false,
) : ICommentEntity

功能: ICommentEntity 是一个接口,主要用于定义评论实体的基本属性。这些属性包括 id、content、userId 和 userName,这些都是评论的基本信息。
用途: 这个接口的主要目的是让不同类型的评论实体(如 CommentLevel1 和 CommentLevel2)能够实现相同的属性结构,从而提供一致的接口,便于统一处理和管理。

  • CommentItem (密封接口 + data class)
/**
 * 密封接口类
 */
sealed interface CommentItem {
    val id: Int
    val content: CharSequence
    val userId: Int
    val userName: CharSequence

    /**
     * 数据加载
     */
    data class Loading(
        val page: Int = 0,
        val state: State = State.LOADING
    ) : CommentItem {
        override val id: Int=0
        override val content: CharSequence
            get() = when(state) {
                State.LOADED_ALL -> "全部加载"
                else -> "加载中..."
            }
        override val userId: Int=0
        override val userName: CharSequence=""

        enum class State {
            IDLE, LOADING, LOADED_ALL
        }
    }

    /**
     * 评论层级1
     */
    data class Level1(
        override val id: Int,
        override val content: CharSequence,
        override val userId: Int,
        override val userName: CharSequence,
        val level2Count: Int,
    ) : CommentItem

    /**
     * 评论层级2
     */
    data class Level2{
    }: CommentItem

    /**
     * 折叠数据展示
     */
    data class Folding(
        val parentId: Int,
        // 页数,从第一页开始排序
        val page: Int = 1,
        val pageSize: Int = 3,
        val state: State = State.IDLE
    ) : CommentItem {
        override val id: Int
            get() = parentId * 1000 + page
        override val content: CharSequence
            get() = if (state == State.LOADING) {
                "加载中..."
            } else {
                when {
                    page <= 1 -> "展开20条回复"
                    else -> "展开更多"
                }
            }
        override val userId: Int = 0
        override val userName: CharSequence = ""

        enum class State {
            IDLE, LOADING, LOADED_ALL
        }
    }
}

功能: CommentItem 是一个密封接口,表示不同类型的评论项,包括不同层级的评论(如 Level1 和 Level2)以及其他状态(如 Loading 和 Folding)。它不仅包含评论的基本信息,还可以包含其他与评论视图相关的状态信息。
用途: CommentItem 的设计目的是为了在 UI 层管理不同类型的评论和其状态,允许系统根据不同的情况(如正在加载、已加载、折叠等)来渲染不同的 UI 组件。

  • Entity2ItemMapper
    将不同类型的评论实体(CommentLevel1、CommentLevel2)转换为适合在 UI 中显示的评论项(CommentItem)

2. 首页:Recyleview

class CommentMainActivity: AppCompatActivity(){
    private lateinit var commentAdapter: CommentAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_comment_main)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        commentAdapter = CommentAdapter {
            lifecycleScope.launchWhenResumed {
                val newList = withContext(Dispatchers.IO) {
                    reduce.invoke(commentAdapter.currentList)  // 在XXReducer中进行了实现(具体的就是VH中的参数)
                }
                val firstSubmit = commentAdapter.itemCount == 1
                commentAdapter.submitList(newList) {
                    if (firstSubmit) {
                        recyclerView.scrollToPosition(0)
                    } else if (this@CommentAdapter is FoldReducer) {
                        val index = commentAdapter.currentList.indexOf(this@CommentAdapter.folding)
                        recyclerView.scrollToPosition(index)
                    }
                }
            }
        }
        recyclerView.adapter = commentAdapter
    }

}

3. Adapter: ListAdapter

核心思想:将页面分成四种情况:一级评论,二级评论,折叠区域,加载中;
在这里插入图片描述
根据itemView的Type的不同,绑定不同的viewHolder

class CommentAdapter(private val reduceBlock: Reducer.() -> Unit) :
    ListAdapter<CommentItem, VH>(object : DiffUtil.ItemCallback<CommentItem>() {
        override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
            if (oldItem::class.java != newItem::class.java) return false
            return (oldItem as? CommentItem.Level1) == (newItem as? CommentItem.Level1)
                    || (oldItem as? CommentItem.Level2) == (newItem as? CommentItem.Level2)
                    || (oldItem as? CommentItem.Folding) == (newItem as? CommentItem.Folding)
                    || (oldItem as? CommentItem.Loading) == (newItem as? CommentItem.Loading)
        }
    }) {

    init {
        submitList(listOf(CommentItem.Loading(page = 0, CommentItem.Loading.State.IDLE)))
    }

    // 根据不同条件,创建ViewHolder
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            TYPE_LEVEL1 -> Level1VH(
                inflater.inflate(R.layout.item_comment_level_1, parent, false),
                reduceBlock
            )

            TYPE_LEVEL2 -> Level2VH(
                inflater.inflate(R.layout.item_comment_level_2, parent, false),
                reduceBlock
            )

            TYPE_LOADING -> LoadingVH(
                inflater.inflate(
                    R.layout.item_comment_loading,
                    parent,
                    false
                ), reduceBlock
            )

            else -> FoldingVH(
                inflater.inflate(R.layout.item_comment_folding, parent, false),
                reduceBlock
            )
        }
    }

    // 绑定VH
    override fun onBindViewHolder(holder: VH, position: Int) {
        holder.onBind(getItem(position))
    }

    // 获取Item的类型
    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is CommentItem.Level1 -> TYPE_LEVEL1
            is CommentItem.Level2 -> TYPE_LEVEL2
            is CommentItem.Loading -> TYPE_LOADING
            else -> TYPE_FOLDING
        }
    }

    companion object {
        private const val TYPE_LEVEL1 = 0
        private const val TYPE_LEVEL2 = 1
        private const val TYPE_FOLDING = 2
        private const val TYPE_LOADING = 3
    }
}

/**
 * ViewHolder 给每个列表项的视图绑定数据
 */
abstract class VH(itemView: View, protected val reduceBlock: Reducer.() -> Unit) :
    RecyclerView.ViewHolder(itemView) {
    abstract fun onBind(item: CommentItem)
}

class Level1VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) 
class Level2VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) 
class FoldingVH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) 
class LoadingVH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) 

4. Reducer

实现一个接口Reducer,后面根据加载、折叠、展开、回复实现不同的Reducer

/**
 * Reducer 接口定义了一个可用于处理 CommentItem 列表的挂起函数属性 reduce,该函数作为扩展函数
 */
interface Reducer {
    // 表示一个扩展函数类型的变量 reduce,该变量的类型是一个挂起函数(suspend function),
    // 它的接收者是一个 List<CommentItem>,返回值是 List<CommentItem>
    val reduce: suspend List<CommentItem>.() -> List<CommentItem>
}

实现回复功能的弹窗

/**
 * 回复评论的Reducer:需要传入一个context用于创建对话框
 */
class ReplyReducer(private val commentItem: CommentItem, private val context: Context) : Reducer {
    private val mapper: Entity2ItemMapper by lazy { Entity2ItemMapper() }
    override val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {
        // 切到主线程
        val content = withContext(Dispatchers.Main) {
            // suspendCoroutine挂起协程,直到在block中使用continuation.resume方法;
            // 一旦协程恢复,就返回it
            suspendCoroutine { continuation ->
                ReplyDialog(context) {
                    continuation.resume(it) //suspendCoroutine语法:这里返回的即是it
                }.show()
            }
        }
        val parentId = (commentItem as? CommentItem.Level1)?.id
            ?: (commentItem as? CommentItem.Level2)?.parentId ?: 0
        val replyItem = mapper.invoke(FakeApi.addComment(parentId, content))
        val insertIndex = indexOf(commentItem) + 1
        toMutableList().apply {
            add(insertIndex, replyItem)
        }
    }
}

/**
 * 实现一个回复对话框
 */
class ReplyDialog(context: Context, private val callback: (String) -> Unit) : Dialog(context) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.dialog_reply)
        val editText = findViewById<EditText>(R.id.content)
        findViewById<Button>(R.id.submit).setOnClickListener {
            if (editText.text.toString().isBlank()) {
                Toast.makeText(context, "评论不能为空", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            callback.invoke(editText.text.toString()) //调用回调函数,等价于callback(editText.text.toString())
            dismiss()
        }
    }
}

5. FakeApi

处理假数据

package person.tools.treasurebox.comment.data

import kotlinx.coroutines.delay

object FakeApi {
    private var id = 0

    /**
     * 每页返回pageSize个一级评论,每个一级评论返回最多两个热门二级评论
     */
    suspend fun getComments(page: Int, pageSize: Int = 5): Result<List<ICommentEntity>> {
        delay(2000)
        val list = (0 until pageSize).map {
            val id = id++
            CommentLevel1(
                id = id,
                content = "我是一级评论${id}",
                userId = 1,
                userName = "一级评论员",
                level2Count = 20
            )
        }.map {
            listOf(
                it,
                CommentLevel2(
                    id = id++,
                    content = "我是二级评论$id",
                    userId = 2,
                    userName = "二级评论员",
                    parentId = it.id,
                    hot = true,
                ), CommentLevel2(
                    id = id++,
                    content = "我是二级评论$id",
                    userId = 2,
                    userName = "二级评论员",
                    parentId = it.id,
                    hot = true,
                )
            )
        }.flatten()
        return Result.success(list)
    }

    suspend fun getLevel2Comments(
        parentId: Int,
        page: Int,
        pageSize: Int = 3
    ): Result<List<ICommentEntity>> {
        delay(500)
        // 这里检查请求的页码是否大于5。
        // 如果是,则返回一个成功的 Result,但包含一个空列表。这可以用于限制最多只返回5页评论的逻辑。
        if (page > 5) return Result.success(emptyList())
        val list = (0 until pageSize).map {
            CommentLevel2(
                id = id++,
                content = "我是二级评论$id",
                userId = 2,
                userName = "二级评论员",
                parentId = parentId,
            )
        }
        return Result.success(list)
    }

    /**
     * 添加数据
     */
    suspend fun addComment(
        id: Int,
        content: String,
    ): CommentLevel2 {
        delay(400)
        FakeApi.id++
        return CommentLevel2(
            id = FakeApi.id++,
            content,
            userId = 3,
            userName = "哈哈哈哈",
            parentId = id
        )
    }
}

http://www.kler.cn/a/468781.html

相关文章:

  • Android 性能优化:内存优化(实践篇)
  • git理解记录
  • 125个Docker的常用命令
  • 数据插入操作的深度分析:INSERT 语句使用及实践
  • 一文讲明白朴素贝叶斯算法及其计算公式(入门普及)
  • 基于单片机的肺功能MVV简单测算
  • 代码随想录算法训练营第二十四天-回溯算法-78. 子集
  • yolov5核查数据标注漏报和误报
  • Python常用算法
  • WPS计算机二级•数据查找分析
  • jupyter展示图片做法以及为什么会无法展示图片
  • 编辑音频的基本属性
  • 基于Python的考研学习系统
  • 亚马逊云科技 re:Invent 2024 Amazon Bedrock 推出新功能,加速AI落地
  • 渗透测试实战-DC-1
  • 力扣23.合并K个升序链表
  • MySQL 主从同步模式选择指南
  • 大模型与EDA工具
  • Pytorch库结构是什么样的
  • C语言冒泡排序教程简介
  • Viggle AI:支持小孩或者卡通人物吗? [Viggle AI实战教程] – 第2篇
  • Go语言的 的垃圾回收(Garbage Collection)基础知识
  • 统计学就业方向(ai)
  • 基于51单片机智能温控风扇设计—数码管显示
  • >>>、/deep/、::v-deep、::v-deep()和:deep()的区别与用法
  • 【cursor破解】【cursor白嫖】