Обновить объект внутри вложенного RecyclerView с использованием AdapterDelegates

Рейтинг: 1Ответов: 2Опубликовано: 12.04.2023

Всем привет Пытаюсь реализовать систему лайков для вложенных списков. Использую 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

Дальше вместо HomeScreen будет Grid

Поправил DiffUtil у Grid и Item

class GridDiffUtil : DiffUtil.ItemCallback<Grid>() {

    override fun areItemsTheSame(oldItem: Grid, newItem: Grid): Boolean =
        oldItem.titleId == newItem.titleId

    override fun areContentsTheSame(oldItem: Grid, newItem: Grid): Boolean =
        (oldItem.titleId == newItem.titleId)
                && (oldItem.list == newItem.list)
                && (oldItem.orientation == newItem.orientation)
                && (oldItem.spanCount == newItem.spanCount)

    override fun getChangePayload(oldItem: Grid, newItem: Grid): Any? {
        return if (oldItem.list.map { it.isFavorite } != newItem.list.map { it.isFavorite }) {
            Bundle().apply {
                putString("key", "isFavoriteInList")
            }
        } else super.getChangePayload(oldItem, newItem)
    }
}
class ItemDiffUtil : DiffUtil.ItemCallback<Item>() {
    override fun areItemsTheSame(oldItem: Item, newItem: Item) =
        oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: Item, newItem: Item) =
        (oldItem.id == newItem.id) && (oldItem.isFavorite == newItem.isFavorite)

    override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
        return if (oldItem.isFavorite != newItem.isFavorite) {
            Bundle().apply {
                putString("key", "isFavorite")
            }
        } else super.getChangePayload(oldItem, newItem)
    }
}

Вынес создание адаптера и привязку его к ресайклеру из bind

fun horizontalGridDelegate(
    onItemClick: (ClickableView, Item) -> Unit,
    onContainerAllButtonClick: (ClickableView) -> Unit) =
    adapterDelegateViewBinding<HorizontalGrid, Grid, ItemContainerViewHolderBinding>({ inflater, root ->
        ItemContainerViewHolderBinding.inflate(inflater, root, false)
    }) {
        val itemAdapter = ItemAdapter{ clickableView, item ->
            clickableView.listPosition = bindingAdapterPosition
            onItemClick(clickableView, item)
        }
        binding.recyclerView.adapter = itemAdapter
        binding.btnAll.setOnClickListener {
            ClickableView.ALL_BUTTON.container = DOG_CONTAINER
            onContainerAllButtonClick(ClickableView.ALL_BUTTON)
        }
        bind {
                binding.bind(item, itemAdapter)
        }
    }

fun ItemContainerViewHolderBinding.bind(
    item: Grid,
    adapter: ItemAdapter
) {
    adapter.items = item.list
    itemTitle.text = itemTitle.context.getString(item.titleId)
    recyclerView.layoutManager = GridLayoutManager(
        recyclerView.context, item.spanCount, item.orientation, false
    )
}

Теперь отрисовывается только кнопка лайка, но горизонтальный ресайклер всё равно прокручивается в начало

Ответы

▲ 0Принят

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

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.