MENU

【译】在 Android RecyclerView 中使用密封类

November 11, 2020 • 开发

在多 View Type 的 RecyclerView 中使用密封类

在 Android 中展示海量列表数据的最好方式是使用 RecyclerView。作为开发者,想必各位都使用过它。它有着很多进阶功能如 ViewHolder、丰富的 Animation、以及提升性能表现的 Diff-Utils 等等。诸如 WhatsApp 、Gmail 一类的应用都在使用 RecyclerView 来展示近乎无穷的会话列表。

我所使用到的 RecyclerView 最重要的一个进阶功能就是 View Type。这样我们就可以在同一个 RecyclerView 内展示多种样式的视图。早前,开发者通过维护一个在列表 Model 内 View Type 的标记并在 RecyclerView AdaptergetViewType 方法中返回它来实现这个功能。

为什么选择 Kotlin 的密封类?

在为 Android 开发引入 Kotlin 之后,我们处理关键实现的方式发生了巨大变化。我的意思是,扩展函数的功能几乎取代了维护 Android 组件 Base 类的需求。 Kotlin 的 Delegate 改变了我们使用 settergetter 的方式。

现在,是时候改变我们使用 RecyclerView Adapter 的方式了。Kotlin 密封类在对状态管理方面展现出显著作用。如果您想了解它们,建议你阅读这篇文章

受到这篇文章的启发,我想通过密封类来作为 RecyclerView 的 View Type。我将尝试不使用 Int 或 Layout Res 作为 View Type 的值,而是使用密封类来替代它们。如果你是一个热爱使用 Kotlin 的开发者,我确信你会喜欢上这种实现方式。

创建 Kotlin 密封类

我们需要做的第一件事情就是创建所有将在 Adapter 中使用到的数据类,并将它们链接在一个密封类内:

data class FeedItem(val title: String,
                    val desp : String,
                    val businessName : String,
                    ...)

data class PromotionItem(val title: String,
                         val desp : String,
                         val image : String)

data class RatingCardItem(val title: String,
                         val desp : String,
                         val link : String,
                         val button_tittle : String)

data class LoadingStateItem(val isLoading: Boolean,
                          val isRetry : Boolean,
                          val error_message : String)

这是一些我希望通过服务端获取后展示在列表内的数据类。你可以根据自己的需要创建任意多个数据类。

这样的最大好处就是我们可以在此维护加载状态、Header、Footer 等等而不需要创建额外的类。你很快就会知道该怎么做。下一步创建密封类并持用所有可能用到的数据类:

sealed class UIModel{

    class FeedyModel(val feedItem: FeedItem) : UIModel()

    class PromotionModel(val promotionItem: PromotionItem) : UIModel()

    class RatingCardModel(val ratingCardItem : RatingCardItem) : UIModel()

    class LoadingModel(val loadingStateItem : LoadingStateItem) : UIModel()
    
}

就像我刚刚说过的那样,不需要额外的工作,仅仅使用 Kotlin 中的 Object 就可以在 RecyclerView 中轻松的添加 Header 和 Footer:

sealed class UIModel{

    object Header : UIModel()

    object Footer : UIModel()

    class FeedyModel(val feedItem: FeedItem) : UIModel()

    class PromotionModel(val promotionItem: PromotionItem) : UIModel()

    class RatingCardModel(val ratingCardItem : RatingCardItem) : UIModel()

    class LoadingModel(val loadingStateItem : LoadingStateItem) : UIModel()

}

到这里,我们已经完成了密封类的实现。

创建 RecyclerView Adapter

既然已经完成了密封类的工作,是时候使用 UIModel 列表来创建 RecyclerView Adapter 了。这是一个简单的 RecyclerView,但它使用的是一个密封类的 ArrayList:

class FeedAdapter(context: Context) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
    private var arrayList : ArrayList<UIModel> = ArrayList()
    
    fun submitData(list : ArrayList<UIModel>){
        arrayList.clear()
        arrayList.addAll(list)
    }

    override fun getItemCount(): Int = arrayList.size

    override fun getItemViewType(position: Int): Int {
        return super.getItemViewType(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        TODO("not implemented") 
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        TODO("not implemented") 
    }

}

上面的代码展示了不使用密封类的逻辑时 RecyclerView Adapter 的基本用法。你可以看到,我们声明了一个密封类的 ArrayList(UIModel)。下一步是基于 postion 返回适当的 View Type:

override fun getItemViewType(position: Int) = when (arrayList[position]) {
    is UIModel.FeedyModel -> R.layout.adapter_feed
    is UIModel.PromotionModel -> R.layout.adapter_promotion
    is UIModel.RatingCardModel -> R.layout.adapter_rating
    is UIModel.LoadingModel -> R.layout.adapter_loading
    is UIModel.Header -> R.layout.adapter_header
    is UIModel.Footer -> R.layout.adapter_footor
    null -> throw IllegalStateException("Unknown view")
}

现在我们成功通过密封类返回了对应的布局文件,接下来要根据 viewTypeonCreateViewHolder 方法中创建各自的 ViewHolder

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val v = layoutInflater.inflate(viewType, parent, false)
    return when (viewType) {
        R.layout.adapter_feed -> FeedViewHolder(v)
        R.layout.adapter_promotion -> PromotionalCardViweHolder(v)
        R.layout.adapter_rating -> RatingCardViweHolder(v)
        R.layout.adapter_header -> HeaderViweHolder(v)
        R.layout.adapter_footor -> FootorViweHolder(v)
        else -> LoadingViewholder(v)
    }
}

最后一步是使用当前项目数据更新 ViewHolder,以便 Adapter 可以在 UI 中展示数据。由于 Adapter 具有多个视图,因此我们必须对其进行分类,然后调用各自的ViewHolder

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val item = arrayList[position]
    when (holder) {
        is FeedViewHolder -> holder.onBindView(item as UIModel.FeedyModel)
        is PromotionalCardViweHolder -> holder.onBindView(item as UIModel.PromotionModel
        is RatingCardViweHolder -> holder.onBindView(item as UIModel.RatingCardModel)
        is HeaderViweHolder -> holder.onBindView(item as UIModel.Header)
        is FootorViweHolder -> holder.onBindView(item as UIModel.Footer)
        is LoadingViewholder -> holder.onBindView(item as UIModel.LoadingModel)
    }
    
}

当我们把这些整合到一起,最终的代码看起来将是这样的:

class FeedAdapter(context: Context) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var arrayList : ArrayList<UIModel> = ArrayList()

    fun submitData(list : ArrayList<UIModel>){
        arrayList.clear()
        arrayList.addAll(list)
    }

    override fun getItemCount(): Int = arrayList.size

    override fun getItemViewType(position: Int) = when (arrayList[position]) {
        is UIModel.FeedyModel -> R.layout.adapter_feed
        is UIModel.PromotionModel -> R.layout.adapter_promotion
        is UIModel.RatingCardModel -> R.layout.adapter_rating
        is UIModel.LoadingModel -> R.layout.adapter_loading
        is UIModel.Header -> R.layout.adapter_header
        is UIModel.Footer -> R.layout.adapter_footor
        null -> throw IllegalStateException("Unknown view")
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val v = layoutInflater.inflate(viewType, parent, false)
        return when (viewType) {
            R.layout.adapter_feed -> FeedViewHolder(v)
            R.layout.adapter_promotion -> PromotionalCardViweHolder(v)
            R.layout.adapter_rating -> RatingCardViweHolder(v)
            R.layout.adapter_header -> HeaderViweHolder(v)
            R.layout.adapter_footor -> FootorViweHolder(v)
            else -> LoadingViewholder(v)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = arrayList[position]
        when (holder) {
            is FeedViewHolder -> holder.onBindView(item as UIModel.FeedyModel)
            is PromotionalCardViweHolder -> holder.onBindView(item as UIModel.PromotionModel)
            is RatingCardViweHolder -> holder.onBindView(item as UIModel.RatingCardModel)
            is HeaderViweHolder -> holder.onBindView(item as UIModel.Header)
            is FootorViweHolder -> holder.onBindView(item as UIModel.Footer)
            is LoadingViewholder -> holder.onBindView(item as UIModel.LoadingModel)
        }

    }

}

至此,我们进展十分顺利。我们已经完成了所有的必须实现的方法。你可以在你的 Activity/Fragment 中创建一个 Adapter 实例并设置给 RecyclerView。当你获取到展示数据后,通过调用 submitData 方法,给它传入一个 ArrayList<UIModel>

lateinit var adapter: FeedAdapter

fun assignAdapter(){
  
  adapter = FeedAdapter(this)
  categories_recyclerView?.adapter = adapter
  feedViewModel.scope.launch {
      feedViewModel.getFeed().collectLatest {
          adapter.submitData(it)
      }
  }
  
}

DiffCallback

“DiffUtil 是一个工具类,用来计算两个列表的差异并输出更新操作列表,从而把旧的数据集过渡到新的数据集。” — Android Developer

实现 diffcallback 方法并不是强制性的,但是在数据集很大时可以有效提升处理性能。因此在我们的 Adapter 中实现 diffCallback,我们需要对模型进行区分并比较必要的变量值:

companion object {
    object diffCallback : DiffUtil.ItemCallback<UIModel>() {
      
        override fun areItemsTheSame(oldItem: UIModel, newItem: UIModel): Boolean {
          
            val isSameRepoItem = oldItem is UIModel.FeedyModel
                    && newItem is UIModel.FeedyModel
                    && oldItem.feedItem.businessName == newItem.feedItem.businessName
          
            val isSameSeparatorItem = oldItem is UIModel.PromotionModel
                    && newItem is UIModel.PromotionModel
                    && oldItem.promotionItem.title == newItem.promotionItem.title
          
            return isSameRepoItem || isSameSeparatorItem
        }
      
        override fun areContentsTheSame(oldItem: UIModel, newItem: UIModel) = oldItem == newItem
      
    }
}

这和一个常规的 diffcallback 实现很像。但是我们需要区分类型。当创建完以后,在构造函数中将其链接到 Adapter

到此为止。希望你学到了一些有用的东西。谢谢阅读。

原文链接

Last Modified: November 23, 2020