Android: MVVM - RecyclerView dynamic data load

Viewed 1119

My case is as following: Supposing that I have an app where I show a list of users, and their profile pics (similar to whatsapp). First I load the list of users, and observer the LiveData of users. The problem is, that the url of their profile pic, doesn't come with the getUsersList API. Instead, I have to do another network call on fly (while rendering the list), in order to retrieve the profile pic for a given User, let's say getUserPicByUserId.

Using, MVVM design pattern, I did this implementation:

  1. In Fragment class I load the list of Users from getUsersList API. For each user item, profilePicUrl is Null.

  2. In Adapter/ViewHolder class I check if profilePicUrl is null. If so, using a listener(MyAdapterListener), I do a call to getUserPic API for the given User.

  3. When the response of getUserPic API is ready, I update the user item in ViewHolder (see the lambda function onUrlLoaded: (url: String) -> Unit) and load the image.

Problems: This approach is causing that all items get the same image, because the observer of userProfilePicLiveData is always listening for each user item. And every item will load the last retrieven url.

Adding:

viewModel.userProfilePicLiveData.removeObservers(lifecycleOwner) after callback onUrlLoaded(profilePicUrl) in Fragment will not work either.

I spent some time on this problem, but cannot find a solution.

What would be the appropriate approach for this scenario? How to perform a network call when rendering each RecyclerView item, and send the result back to the Adapter to update the view?

Here there is the simplified code of what I have done so far:

Model User

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

profilePicUrl doesn't come with getUsersList API, so by default is Null.

Model UserProfilePic:

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

Implementation example of ViewModel class:

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
  }

}

Adapter class:

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 
        }
      }
    }
}

Fragment class:

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 Answers

Your ViewModel should update the list stored in usersListLiveData every time loadProfilePicByUserId(userId: String) pulls a new profile image from the API. Your Fragment should observe usersListLiveData and notify the adapter when the data changes (using adapter.notifyDataSetChanged() or an equivalent).

One caveat is that User is a complex object, and usersListLiveData is a list of these complex objects. You can't get away with just changing the one property profilePicUrl in the relevant list item. You need to call something like usersListLiveData.value = newUserListor else the observer of the MutableLiveData won't fire. Observers won't fire when you just change a single property of a complex object. You need to call setValue() or postValue().

Related