Обновить объект внутри вложенного RecyclerView с использованием AdapterDelegates
Всем привет Пытаюсь реализовать систему лайков для вложенных списков. Использую AdapterDelegates, MVVM, StateFlow. Экран выглядит примерно так:
Данные приходят с апи, во вьюМодели закидываются в StateFlow:
class HomeViewModel @Inject constructor(private val repository: Repository,) : ViewModel() {
private val _data = MutableStateFlow<List<HomeScreen>>(emptyList())
val data = _data.asStateFlow()
init {createHomeScreen()}
private fun createHomeScreen() {
viewModelScope.launch(Dispatchers.IO) {
_data.value = repository.getItems()
}
}
...
}
Далее во фрагменте всё это дело закидывается в адаптер:
class HomeFragment : Fragment {
private val adapter by lazy {HomeAdapter { clickableView, item ->onItemClick(clickableView, item)}}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data.collect{ data ->
adapter.items = data
}
}
private fun onItemClick(clickableView: ClickableView, item: Item) {
viewModel.onItemClick(clickableView, item)
}
}
Внешний адаптер:
class HomeAdapter(onItemClick: (ClickableView, Item) -> Unit) :
AsyncListDifferDelegationAdapter<HomeScreen>(HomeScreenDiffUtil()) {
init {
delegatesManager
.addDelegate(horizontalGridDelegate(onItemClick))
}
}
И его делегат:
fun horizontalGridDelegate(onItemClick: (ClickableView, Item) -> Unit) =
adapterDelegateViewBinding<HorizontalGrid, HomeScreen, ItemContainerViewHolderBinding>({ inflater, root ->
ItemContainerViewHolderBinding.inflate(inflater, root, false)
}) {
bind {
binding.bind(item){clickableView, item ->
clickableView.listPosition = bindingAdapterPosition
onItemClick(clickableView, item) }
}
}
fun ItemContainerViewHolderBinding.bind(
item: HomeScreen,
onItemClick: (ClickableView, Item) -> Unit
) {
val dogAdapter = OneListItemAdapter(onItemClick)
recyclerView.adapter = dogAdapter
dogAdapter.items = item.list
...
)
}
Внутренний адаптер и его делегат:
class OneListItemAdapter(onItemClick: (ClickableView, Item) -> Unit) :
AsyncListDifferDelegationAdapter<Item>(ItemDiffUtil()) {
init {
delegatesManager
.addDelegate(dogsDelegate(onItemClick))
}
}
fun dogsDelegate(onItemClick: (ClickableView, Item) -> Unit) =
adapterDelegateViewBinding<Dog, Item, DogViewHolderBinding>({ inflater, root ->
DogViewHolderBinding.inflate(inflater, root, false)
}) {
binding.btnFavorite.setOnClickListener {
/**клик, который нам нужен*/
ClickableView.FAVORITE.itemPosition = bindingAdapterPosition
onItemClick(ClickableView.FAVORITE, item)
}
bind {
binding.btnFavorite.isSelected = item.isFavorite
}
}
Суть проблемы:
Нельзя просто поставить срабатывание селектора при клике, так как запрос на сервер может не пройти. Поэтому нужно как-то грамотно это заэмитить во флоу. Пока что попытался сделать вот так:
... ViewModel() {
private fun addToFavorites(item: Dog, itemPosition: Int, listPosition: Int) {
viewModelScope.launch(Dispatchers.IO) {
val newData = _data.value.toMutableList()
val newList = newData[listPosition].list.toMutableList() as MutableList<Dog>
newList[itemPosition] =
newList[itemPosition].copy(isFavorite = !newList[itemPosition].isFavorite)
newData[listPosition] = (newData[listPosition] as HorizontalGrid).copy(list = newList)
_data.value = newData
}
}
}
но при такой реализации у меня обновляется еще и весь внутренний ресайклер. Возможно мне помогут Payloads от DiffUtil. Но мне не хватает знаний, в том числе по котлину, чтобы их внедрить. Или может есть какой-то другой способ? Уже кучу информации по делегатам прошерстил, но нигде не наткнулся на перерисовку объектов. Понимаю, что в данном случае можно обойтись вообще без делегатов, но стоит цель использовать именно эту библиотеку. Заранее спасибо за помощь
Upd
class HomeScreenDiffUtil : DiffUtil.ItemCallback<HomeScreen>() {
override fun areItemsTheSame(oldItem: HomeScreen, newItem: HomeScreen): Boolean =
oldItem == newItem
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: HomeScreen, newItem: HomeScreen): Boolean =
oldItem.titleId == newItem.titleId
}
Вынес создание адаптера из bind. Не помогло
fun horizontalGridDelegate(
onItemClick: (ClickableView, Item) -> Unit) =
adapterDelegateViewBinding<HorizontalGrid, HomeScreen, ItemContainerViewHolderBinding>({ inflater, root ->
ItemContainerViewHolderBinding.inflate(inflater, root, false)
}) {
val itemAdapter = OneListItemAdapter{clickableView, item ->
clickableView.listPosition = bindingAdapterPosition
onItemClick(clickableView, item) }
bind {
binding.bind(item, itemAdapter)
}
}
fun ItemContainerViewHolderBinding.bind(
item: HomeScreen,
adapter: OneListItemAdapter
) {
recyclerView.adapter = adapter
adapter.items = item.list
itemTitle.text = itemTitle.context.getString(item.titleId)
recyclerView.layoutManager = GridLayoutManager(
recyclerView.context, item.spanCount, item.orientation, false
)
}
Ответы (2 шт):
1 мысль
Следовало бы начать с того, что нужно научить внешний адаптер различать элементы. Судя по коду, это описано в HomeScreenDiffUtil, но код реализации не приведён.
Скажу, что можно было бы заменить вызов HomeScreenDiffUtil() дефолтным:
AdapterUtil.diffUtilItemCallbackEquals(HomeScreen::id) или вроде того (id - параметр который отвечает за то, что этот элемент один и тот же).
По сути, если сам контент отдельно взятого HomeScreen не изменился - он не должен был перерисовываться (AdapterUtil.diffUtilItemCallbackEquals так работает). Если в элементе у вас много разной информации, которая не относится к UI слою - ваш элемент снова будет перерисовываться (что для вас нежелательно но логично исходя из того, что какой-то параметр изменился - адаптер понимает что его нужно перерисовать).
2 мысль
Также обратил внимание на то как реализован horizontalGridDelegate. Вижу ошибку: зачем-то адаптер создаётся в методе bind. Но так делать нельзя, потому что это ведёт к пересозданию адаптера при каждом вызове bind. А сам метод bind вызывается, как вы можете знать, в тот момент когда элемент не был виден и становится видим на экране, каждый раз. Логичнее вынести создание адаптера из метода bind. В bind же устанавливать значение items этого адаптера. Это будет верно и логично. Подчеркну, я говорю именно про метод bind, а не про созданное вами расширение bind.
P.S.
Если будет чуть больше информации и описание реакции на предложенные варианты решения - можно будет дополнить ответ. Но я думаю что проблема именно в bind.
Нашёл в чем было дело. У меня при каждом bind переопределялся layoutManager. Поставил ему зависимость от payloads и всё заработало
bind {
binding.bind(item, ItemAdapter, it)
}
fun ItemContainerViewHolderBinding.bind(
item: Grid,
adapter: ItemAdapter,
payloads: List<Any>
) {
adapter.items = item.list
itemTitle.text = itemTitle.context.getString(item.titleId)
if(payloads.isEmpty())
recyclerView.layoutManager = GridLayoutManager(
recyclerView.context, item.spanCount, item.orientation, false
)
}
