Обновить объект внутри вложенного 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 шт):

Автор решения: Egor

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.

→ Ссылка
Автор решения: VSV

Нашёл в чем было дело. У меня при каждом 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
    )
}
→ Ссылка