【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
)
}
}