RecyclerView
— это один из самых лучших инструментов для отображения больших списков на Android. Как разработчики, вы, скорее всего понимаете о чем я говорю. У нас есть много дополнительных фич, таких как шаблоны вью холдеров, сложная анимация, Diff-Utils колбек для повышения производительности и т. д. Такие приложения, как WhatsApp и Gmail, используют RecyclerView
для отображения бесконечного количества сообщений.
Одна из важнейших фич RecyclerView
, которые я использую, — это типы представлений (view types). В RecyclerView
мы можем отобразить несколько типов представлений. Раньше разработчики делали это с помощью флага типа представления в модели списка, который возвращали в функции getViewType
адаптера RecyclerView
.
Почему sealed классы Kotlin?
После появления Kotlin для разработки приложений под Android наши подходы к реализации кода кардинально изменились. То есть такие фичи, как расширения, почти заменили потребность в поддержании базовых классов для компонентов Android. Делегаты Kotlin внесли изменения в нашу работу с сеттерами и геттерами.
Теперь пришло время обновлений в работе адаптера RecyclerView
. Sealed классы из Kotlin оказывают значительное влияние на управление состояниями. Подробнее прочитать об этом вы можете в этой статье.
Вдохновившись этой статьей, я хочу показать вам реализацию типов представлений в RecyclerView
с использованием sealed классов. Мы постараемся развить сравнение случайных чисел или лейаутов до типов классов. Если вы фанат Kotlin, я уверен, что вам понравится эта реализация.
Создание sealed классов в Kotlin
Первое, что нам нужно сделать при этом подходе — это создать все классы данных, которые мы намерены использовать в адаптере, а затем необходимо связать их в sealed
классе. Давайте создадим группу классов данных:
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)
Это несколько классов данных, которые я хотел отобразить в списке, основываясь на данных с серверов. Вы можете создать столько классов данных, сколько захотите. Этот подход хорошо масштабируется.
То, что мы можем работать с состояниями загрузки, хедерами, футерами и многим другое без написания дополнительных классов — это одно из крутых преимуществ данного метода. Вы скоро узнаете, как это сделать. Следующим шагом является создание sealed
классов, содержащих все необходимые классы данных:
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()
}
Sealed
класс с пользовательскими моделями
Как я упоминал ранее, мы можем без дополнительных сложностей добавлять хидеры и футеры из RecyclerView
, используя объект Kotlin:
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()
}
Sealed
класс с хидером и футером
На этом этапе заканчивается реализация нашего sealed
класса.
Создание адаптера RecyclerView
После того, как мы разобрались с sealed
классом, пришло время создать адаптер RecyclerView
с UIModel списком. Это простой RecyclerView
, но с sealed
классом 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") //To change body of created functions use File | Settings | File Templates.
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
В приведенном выше коде показана базовая реализация адаптера RecyclerView
без какой-либо логики sealed
классов. Как можно заметить, мы объявили sealed
классы arraylist
(UIModel). Следующим шагом является возврат соответствующего типа представления на основе позиции:
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")
}
Сравнение модели sealed
класса для получения типа представления
Теперь, когда мы успешно вернули правильный лейаут на основе модели sealed
класса, нам нужно создать соответствующий ViewHolder
в функции onCreateViewHolder
на основе viewtype
:
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)
}
}
Создание вью холдера с учетом типа представления из sealed
классов
Последний шаг — обновить вью холдер на основе текущих данных элемента, чтобы адаптер мог отображать данные в пользовательском интерфейсе. Поскольку у адаптера есть несколько представлений, мы должны классифицировать тип, а затем вызвать соответствующий 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
и присвоить его 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)
}
}
}
Публикация данных в адаптер RecyclerView
DiffCallback
«DiffUtil — это вспомогательный класс, который может вычислять разницу между двумя списками и выводить список операций обновления, который преобразует первый список во второй». — Android Developer
Реализация diffcallback
не является обязательной, но она повысит производительность, если вы работаете с большими наборами данных. Итак, чтобы реализовать difCallback
в нашем адаптере, нам нужно различать модели и сравнивать нужные переменные:
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
Она похожа на стандартную реализацию diffCallback
, но нам необходимо разделять типы. Создав ее, свяжите ее с адаптером в конструкторе.
Это все. Надеюсь, эта статья была для вас полезной. Спасибо за внимание!
Материал подготовлен в рамках специализации «Android Developer».
Всех желающих приглашаем на двухдневный онлайн-интенсив «Делаем мобильную мини-игру за 2 дня». За 2 дня вы сделаете мобильную версию PopIt на языке Kotlin. В приложении будет простая анимация, звук хлопка, вибрация, таймер как соревновательный элемент. Интенсив подойдет для тех, кто хочет попробовать себя в роли Android-разработчика.
>> РЕГИСТРАЦИЯ
KursikS
Можно layout id положить прямо в дата класс и избавиться от одного when