0

I have the following fuction -

    private fun fetchGroupData(callback: (groupModelList: List<GroupModel>) -> Unit) {
        val groupModelList = mutableListOf<GroupModel>()
        groupViewmodel.getAllGroupEntities().observeOnce(requireActivity(), Observer { groupEntityList ->
            groupEntityList.forEach { groupEntity ->
                /*
                We iterate though all of the available groups,
                for each group we get all of it's groupMembers models
                */
                val groupName = groupEntity.groupName
                val groupId = groupEntity.id
                taskViewmodel.getGroupTaskCounter(groupId).observeOnce(requireActivity(), Observer { groupTaskCount ->
                    /*
                    For each group we observe it's task counter
                     */
                    groupViewmodel.getGroupMembersForGroupId(groupId).observeOnce(requireActivity(), Observer { groupMembers ->
                        /*
                        For each group, we iterate through all of the groupMembers and for each of them we use it's userId
                         to fetch the user model, getting it's full name and adding it to a list of group users full name.
                         */
                        val groupUsersFullNames = mutableListOf<String>()
                        groupMembers.forEach { groupMember ->
                            val memberId = groupMember.userId
                            groupViewmodel.getGroupParticipantForUserId(memberId).observeOnce(requireActivity(), Observer { groupUser ->
                                groupUsersFullNames.add(groupUser.fullName)
                                /*
                                When the groupUsersFullNames size matches the groupMembers size, we can add a model to our list.
                                 */
                                if (groupUsersFullNames.size == groupMembers.size)
                                    groupModelList.add(GroupModel(groupId, groupName, groupTaskCount, groupUsersFullNames))
                                /*
                                When the new list matches the size of the group list in the DB we call the callback.
                                 */
                                if (groupModelList.size == groupEntityList.size)
                                    callback(groupModelList)
                            })
                        }
                    })
                })
            }
        })
    }

That is being used by the following function -

 private fun initAdapter() {
        fetchGroupData { groupModelList ->
            if (groupModelList.isEmpty()) {
                binding.groupsListNoGroupsMessageTitle.setAsVisible()
                binding.groupsListNoGroupsMessageDescription.setAsVisible()
                return@fetchGroupData
            }
            binding.groupsListNoGroupsMessageTitle.setAsGone()
            binding.groupsListNoGroupsMessageDescription.setAsGone()
            val newList = mutableListOf<GroupModel>()
            newList.addAll(groupModelList)
            adapter.submitList(groupModelList)
            Log.d("submitList", "submitList")
            binding.groupsListRecyclerview.setAdapterWithItemDecoration(requireContext(), adapter)
        }
    }

These 2 functions represent group list fetch from my local DB into a RecyclerView.

In order to be notified when a new group has been created, I am holding a shared ViewModel object with a boolean indicating if a new group has been created.

In the same Fragment that these 2 functions ^ are written, I am observing this Boolean, and if the value is true I trigger a re-fetch for the entire list -

private fun observeSharedInformation() {
        sharedInformationViewModel.value.groupCreatedFlag.observe(requireActivity(), Observer { hasGroupBeenCreated ->
            if (!hasGroupBeenCreated) return@Observer
            sharedInformationViewModel.value.groupCreatedFlag.value = false
            Log.d("submitList", "groupCreatedFlag")
            initAdapter()

        })
    }

At some point in my code in a different Fragment that also has an instance of my shared ViewModel, I trigger a value change for my Boolean LiveData -

sharedInformationViewModel.value.groupCreatedFlag.value = true

Which in turn triggers the observer, and does a re-fetch for my group list.

The issue I am facing is that when re-fetching for a new list (because a new group has been added) I do get the current information and everything should work 100% fine, but the new data - the newly created group - does not appear.

The newly added data appears in the list under 2 circumstances -

  1. I restart the app
  2. The function is triggered again - what happens now is that I see the list with the previous newly added group, but the newest group to be added does not appear.

There is one exception to this issue - if the group list is empty, the first group to be added does indeed appear when I submit the list with one group.

What is it that I am missing?

Edit -

Here is my adapter.

I am using a custom call called DefaultAdapterDiffUtilCallback, which expects a model that implements an interface that defines the unique ID for each model so I can compare new and old models.

class GroupsListAdapter(
    private val context: Context,
    private val onClick: (model : GroupModel) -> Unit
) : ListAdapter<GroupModel, GroupsListViewHolder>(DefaultAdapterDiffUtilCallback<GroupModel>()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupsListViewHolder {
        val binding = GroupsListViewHolderBinding.inflate(LayoutInflater.from(context), parent, false)
        return GroupsListViewHolder(binding)
    }

    override fun onBindViewHolder(holder: GroupsListViewHolder, position: Int) {
        holder.bind(getItem(position), onClick)
    }

    override fun submitList(list: List<GroupModel>?) {
        super.submitList(list?.let { ArrayList(it) })
    }
}


/**
 * Default DiffUtil callback for lists adapters.
 * The adapter utilizes the fact that all models in the app implement the "ModelWithId" interfaces, so
 * it uses it in order to compare the unique ID of each model for `areItemsTheSame` function.
 * As for areContentsTheSame we utilize the fact that Kotlin Data Class implements for us the equals between
 * all fields, so use the equals() method to compare one object to another.
 */
class DefaultAdapterDiffUtilCallback<T : ModelWithId> : DiffUtil.ItemCallback<T>() {
    override fun areItemsTheSame(oldItem: T, newItem: T) =
        oldItem.fetchId() == newItem.fetchId()

    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: T, newItem: T) =
        oldItem == newItem
}

/**
 * An interface to determine for each model in the app what is the unique ID for it.
 * This is used for comparing the unique ID for each model for abstracting the DiffUtil Callback
 * and creating a default general one rather than a new class for each new adapter.
 */
interface ModelWithId {
        fun fetchId(): String
}


data class GroupModel(val id: String, val groupName: String, var tasksCounter: Int, val usersFullNames: List<String>) : ModelWithId {

    override fun fetchId(): String = id
}


edit 2.0 -

my observeOnce() extension -

fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
    observe(lifecycleOwner, object : Observer<T> {
        override fun onChanged(t: T?) {
            observer.onChanged(t)
            removeObserver(this)
        }
    })
}
Alon Shlider
  • 1,187
  • 1
  • 16
  • 46

2 Answers2

1

Are you using the "new" ListAdapter?

import androidx.recyclerview.widget.ListAdapter

In this case, I can think of an answer to your problem. But since I do not know more about your exact implementation it is based on my assumptions and you have to verify if it applies or not.

For this ListAdapter you have to implement areItemsTheSame and areContentsTheSame methods. I've once had a similar problem. I was submitting the list but it just didn't update the list in the view.

I could resolve this issue by checking carefully how I was comparing if the contents are the same or not.

For your comparison function, consider the following:

    override fun areContentsTheSame(oldItem: GroupModel, newItem: GroupModel): Boolean {
        // assuming GroupModel is a class
        // this comparison is most likely not getting the result you want
        val groupModelsAreMatching = oldItem == newItem // don't do this
        
        // for data classes it usually gives the expected result
        val exampleDataClassesMatch = oldItem.dataClass == newItem.dataClass
        // But: the properties that need to be compared need to be declared in the primary constructor
        // and not in the function body

        // compare all relevant custom properties
        val groupIdMatches = oldItem.groupId == newItem.groupId
        val groupNameMatches = oldItem.groupName == newItem.groupName
        val groupTaskCountMatches = oldItem.groupTaskCount == newItem.groupTaskCount
        val groupUsersFullNamesMatches = oldItem.groupUsersFullNames == newItem.groupUsersFullNames

        return groupIdMatches && groupNameMatches && groupTaskCountMatches && groupUsersFullNamesMatches
}

And of course you need to make sure that areItemsTheSame. Here you only need to compare the groupIds.

Did you do it like this already?

cewaphi
  • 410
  • 2
  • 7
  • Thank you for your answer. Please check my post, I have edited it and put my adapter implementation - as far as I can see, I did everything correctly. If you see any mistakes please let me know. – Alon Shlider Aug 13 '20 at 19:58
  • 1
    @AlonShlider how exactly is your observeOnce method working, especially in this nested use case? Is the implementation somewhat similar to this single event observer example with live data? https://stackoverflow.com/a/56063739/13138026 – cewaphi Aug 13 '20 at 20:23
  • added my `observeOnce()` extension to the main post. – Alon Shlider Aug 13 '20 at 21:50
  • 1
    @AlonShlider Please correct me if I am wrong. So you have a dedicated function called `fetchGroupData`. Then inside this function you have nested calls of LiveData Observers to finally get your data together (asynchronously) And after that you remove the observers again so that it does not get updated again without you triggering your `fetch` function again. Is that what your are trying? – cewaphi Aug 14 '20 at 18:35
  • 1
    @AlonShlider Then I would like to know: what is the purpose of using LiveData here when all you do is actually calling a function for getting the most recent data and do not on the other hand want to use the feature of live data to "observe" changes (such as changes to your groupModel's data)? – cewaphi Aug 14 '20 at 18:53
  • I guess you are asking this question because of the `observeOnce` extension -the reason is that I do need to observe changes, but only once. For the reason of the time, I want to manually control the observing by telling it specifically to re-fetch for 1 time only again. – Alon Shlider Aug 15 '20 at 15:41
  • 1
    Then I understand that observation of the data is a clear requirement for future development. For which properties do you really need to observe the changes? I still don't see the use case or necessity for having 3 layers of nesting the observers. Above you have mentioned that "adding a new group" is the change you want to observe. – cewaphi Aug 15 '20 at 18:32
  • 1
    Also you mentioned that in a different fragment you have a dummy livedata boolean to trigger a re-fetch of the model. What is the purpose of this variable when are also observing real changes in the database? It seems to me this boolean live data serves the purpose of triggering a one time event as well. You could use a single live data event for that (so no need to change the value to false afterwards...) – cewaphi Aug 15 '20 at 18:34
  • 1
    When triggering your boolean to "fetch" the data, how do you make sure that all data related to the group is written to the data base before submitting your boolean for fetching the data? If this is an issue it could at least explain why you never get the newest one (except it is the only/first one) – cewaphi Aug 15 '20 at 18:58
  • There is no place where I perform a check that the data has been written successfully to the DB, though when debugging the function the new list that I receive is indeed correct with the new group but it does not update it visually. More I can say, that I have a different function that fetches the number of groups which does indeed update the correct data value when a new group has been added. – Alon Shlider Aug 16 '20 at 06:29
  • @AlonShlider Could you for testing purposes see if the error still occurs when you either perform all your fetches in a suspending coroutine on a background thread or, if it is faster to implement for you, on the main thread? So that your fetching is definetely called after all data was obtained from the database. Shouldn't take long. just copy your fetch function and instead of getting live data (and observing) you just directly request the data from your DB – cewaphi Aug 16 '20 at 10:35
  • I could do that but I doubt it is necessary as I am seeing, again and again, correct information when debugging the `initAdapter()` method - it does get the newly updated information and the failure is related to it not displaying the information and not the information not fetching correctly. – Alon Shlider Aug 16 '20 at 10:43
  • For the changes you are observing, do you really need this nested observation? If you want to fetch according to changes of multiple `LiveData` resources, could `MediatorLiveData` be an option? Similar to here https://stackoverflow.com/questions/56518217/nested-observers-with-livedata-observing-an-observer – cewaphi Aug 16 '20 at 10:44
  • You mentioned your data is being displayed correctly (except the newest one) when you fetch again. What happens when you do not use `observeOnce` but just `observe` instead? Is then also the most recent one displayed? – cewaphi Aug 16 '20 at 10:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/219907/discussion-between-alon-shlider-and-cewaphi). – Alon Shlider Aug 16 '20 at 14:06
0

I figured out the problem.

And it has nothing to do with my fetch logics.

The issue is the following -

When creating a group, I am adding a new Fragment to the backstack and popping it off when completed.

When deleting a group, I am navigating forward to the main Fragment of mine while using popUpTo and popUpToInclusive - that works fine.

I needed to use the navigation rather than popping backwards the stack in order to see the new list.

This took me 3 days of work to figure out. jeez

Alon Shlider
  • 1,187
  • 1
  • 16
  • 46