Android: динамическая загрузка данных MVVM — RecyclerView

#android #mvvm #android-recyclerview #android-adapter #android-livedata

#Android #mvvm #android-recyclerview #android-адаптер #android-livedata

Вопрос:

Мой случай следующий: предположим, что у меня есть приложение, в котором я показываю список пользователей и фотографии их профилей (аналогично WhatsApp). Сначала я загружаю список пользователей и наблюдаю за LiveData пользователями. Проблема в том, что URL-адрес их изображения профиля не поставляется с getUsersList API. Вместо этого мне нужно выполнить еще один сетевой вызов на лету (при рендеринге списка), чтобы, скажем, получить изображение профиля для данного пользователя getUserPicByUserId .

Используя шаблон проектирования MVVM, я выполнил эту реализацию:

  1. В классе Fragment я загружаю список пользователей из getUsersList API. Для каждого пользовательского элемента profilePicUrl равно нулю.

  2. В классе Adapter / ViewHolder я проверяю, является ли profilePicUrl значение null. Если это так, используя прослушиватель ( MyAdapterListener ), я выполняю вызов getUserPic API для данного пользователя.

  3. Когда ответ getUserPic API готов, я обновляю пользовательский элемент в ViewHolder (см. лямбда-функцию onUrlLoaded: (url: String) -> Unit ) и загружаю изображение.

Проблемы: Этот подход приводит к тому, что все элементы получают одинаковое изображение, потому что наблюдатель userProfilePicLiveData всегда прослушивает каждый пользовательский элемент. И каждый элемент будет загружать последний повторный URL.

Добавление:

viewModel.userProfilePicLiveData.removeObservers(lifecycleOwner) после обратного вызова onUrlLoaded(profilePicUrl) во фрагменте также не будет работать.

Я потратил некоторое время на эту проблему, но не могу найти решение.

Какой подход был бы подходящим для этого сценария? Как выполнить сетевой вызов при рендеринге каждого RecyclerView элемента и отправить результат обратно на адаптер для обновления представления?

Здесь есть упрощенный код того, что я сделал до сих пор:

Модель пользователя

 data class User(
  val id: String,
  val name: String,
  val profilePicUrl: String? = null, // By default is null
  ...
)
  

profilePicUrl не поставляется с getUsersList API, поэтому по умолчанию является Null .

Модель UserProfilePic:

 data class UserProfilePic(
  val url: String
  ...
)
  

Пример реализации класса ViewModel:

 class MyViewModel: ViewModel() {
  val usersListLiveData = LiveData<List<User>>
  val userProfilePicLiveData = LiveData<UserProfilePic>


  fun loadUsers() {
    // Network call...
    usersListLiveData.value = usersList
  }

  fun loadProfilePicByUserId(userId: String) {
    // Network call...
    userProfilePicLiveData.value = userProfilePic
  }

}
  

Класс адаптера:

 class RecyclerViewAdapter(val usersList: List<User>): RecyclerView.Adapter<MyViewHolder>() {

interface MyAdapterListener {
        fun onLoadProfilePicUrl(
            user: User,
            onUrlLoaded: (url: String) -> Unit
        )
    }

class HomeVenueViewHolder(
 val listener: MyAdapterListener
) : RecyclerView.ViewHolder() {
      fun bind(user: User) {
        // Fill view list item ...
        
        if (user.profilePicUrl is Null ) {
          listener.onLoadProfilePicUrl(user) { url ->
            user.profilePicUrl = url

            // Load Image From the Url
          }
        } else {
          // Valid url: Load image 
        }
      }
    }
}
  

Класс фрагмента:

 class MyFragment: Fragment(), MyAdapterListener {

  val viewModel: MyViewModel


  viewModel.usersListLiveData.observe(viewLifecycleOwner, { usersList ->
    // Setup adapter and show list
  })

  fun loadUsers() {
    viewModel.loadUsers()
  }

  override fun onLoadProfilePicUrl(
      user: User,
      onUrlLoaded: (url: String) -> Unit
  ) {
    viewModel.loadProfilePicByUserId(user.id)

    viewModel.userProfilePicLiveData.observe(viewLifecycleOwner, { profilePicUrl ->
      // This is a callback to Adapter
       onUrlLoaded(profilePicUrl)
    })
  }

}
  

Комментарии:

1. почему ваш API возвращает пользователя без его изображения? это не похоже на полноценный пользовательский объект, вероятно, не имеет отношения к вашему вопросу

2. @a_local_nobody Для этого ничего не нужно делать. Услуга предлагается третьей стороной, и я должен найти решение с учетом этих ограничений.

3. Почему бы просто не подождать, пока вы не выполните оба вызова API, прежде чем отправлять данные на адаптер?

4. @GavinWright Каким образом? Я пытался сделать это на Interceptor уровне. Повторение для каждого пользователя и выполнение getProfilePic для каждого пользователя для завершения User объекта. Но это привело к значительной задержке… Есть ли лучшее решение для этого?

Ответ №1:

Ваша ViewModel должна обновлять список, хранящийся в, usersListLiveData каждый раз loadProfilePicByUserId(userId: String) , когда извлекает новое изображение профиля из API. Ваш фрагмент должен отслеживать usersListLiveData и уведомлять адаптер при изменении данных (используя adapter.notifyDataSetChanged() или эквивалент).

Одно предостережение заключается в том, что User это сложный объект, и usersListLiveData это список этих сложных объектов. Вам не обойтись простым изменением одного свойства profilePicUrl в соответствующем элементе списка. Вам нужно вызвать что-то вроде usersListLiveData.value = newUserList , иначе наблюдатель MutableLiveData не сработает. Наблюдатели не будут срабатывать, когда вы просто измените одно свойство сложного объекта. Вам нужно вызвать setValue() или postValue() .