Ad

How To Refresh The UI Of A Fragment Using LiveData After Firestore Data Loads

I'm making an Android app using Kotlin with Firebase products. I have successful connections with Firestore and can successfully retrieve the data I want, but I am having difficulty displaying it within a RecyclerView.

When the application loads, and after a user has logged in, my Firestore queries use the UID of the user to get a list of their assignments. Using logs I can see that this occurs without issue as the home screen loads. Within the home screen fragment I have data binding for the RecyclerView and setup my ViewModel to have the fragment observe the returned Firestore data.

I believe it is a misunderstanding on my part on exactly how LiveData works because if I tap the bottom nav icon for the home screen to trigger a refresh of the UI then the list populates and I can use the app as desired. Therefore my observer/LiveData must not be setup properly as it is not automatically refreshing once the data has changed (null list to not null list).

As I'm new to programming I'm sure I've fallen into a number of pitfalls and done a few things incorrectly, but I've been searching through StackOverflow and YouTube for help on this issue for months now. Unfortunately I don't have all of the links saved to every video and every post.

I've tried tweaking the ViewModel and the Repository/Database class (singleton) to different effects and currently I'm at my best version with only a single tap required to refresh the UI. Previously it took multiple taps.

from the Database class

private val assignments = MutableLiveData<List<AssignmentModel>>()

private fun getUserAssignments(c: ClassModel) {
        val assignmentQuery = assignmentRef.whereEqualTo("Class_ID", c.Class_ID)

        assignmentQuery.addSnapshotListener { documents, _ ->
            documents?.forEach { document ->
                val a = document.toObject(AssignmentModel::class.java)
                a.Assignment_ID = document.id
                a.Class_Title = c.Title

                a.Formatted_Date_Due = formatAssignmentDueDate(a)

                assignmentMap[a.Assignment_ID] = a
            }
        }
    }

    fun getAssignments() : LiveData<List<AssignmentModel>> {
        assignments.value = assignmentMap.values.toList().filter {
            if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
            .sortedBy { it.Date_Due }
        return assignments
    }

from the ViewModel

class AssignmentListViewModel internal constructor(private val myDatabase: Database) : ViewModel() {

    private var _assignments: LiveData<List<AssignmentModel>>? = null

    fun getAssignments() : LiveData<List<AssignmentModel>> {
        var liveData = _assignments
        if (liveData == null) {
            liveData = myDatabase.getAssignments()
            _assignments = liveData
        }
        return liveData
    }
}

from the Fragment

class AssignmentList : Fragment() {
    private lateinit var model: AssignmentListViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = AssignmentListBinding.inflate(inflater, container, false)

        val factory = InjectorUtils.provideAssignmentListViewModelFactory()
        model = ViewModelProvider(this, factory).get(AssignmentListViewModel::class.java)

        val assignmentAdapter = AssignmentAdapter()
        binding.assignmentRecycler.adapter = assignmentAdapter
        updateUI(assignmentAdapter)
        return binding.root
    }

    private fun updateUI(adapter: AssignmentAdapter) {
        model.getAssignments().observe(this, Observer { assignments ->
            if (assignments.isNotEmpty()) adapter.submitList(assignments)
        })
    }
}

Again, I expect the RecyclerView to populate automatically once the data from Firestore appears, but it doesn't. The screen remains empty until I tap the home screen button.

These snippets show the most recent changes I've made. Originally I had the Firestore query function returning the LiveData directly. I also had a much simpler ViewModel of something like fun getAssignments() = myDatabase.getAssignments().

Thanks for any and all help and advice.

Ad

Answer

When troubleshooting this issue, I'd recommend starting by looking at two things.

Take a look at where/when you're updating your LiveData

The goal is whenever the data in Firebase updates, your assignments LiveData updates your UI. Something like:

  1. Firestore updates
  2. Firestore triggers SnapshotListener
  3. SnapshotListener updates LiveData
  4. LiveData observer updates UI

So in your snapshot listener, you should be updating your LiveData, which is what I think you're missing. So it would be something like:

// Where you define your SnapshotListner
assignmentQuery.addSnapshotListener { documents, _ ->
    // Process the data
    documents?.forEach { document ->
        val a = document.toObject(AssignmentModel::class.java)
        a.Assignment_ID = document.id
        a.Class_Title = c.Title

        a.Formatted_Date_Due = formatAssignmentDueDate(a)

        assignmentMap[a.Assignment_ID] = a
    }

    // Update your LiveData
    assignments.value = assignmentMap.values.toList().filter {
    if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
    .sortedBy { it.Date_Due }

}

Now every time your Firestore updates, your LiveData will update and your UI should update.

Given the code change, getAssignments() can just return assignments. You can do this using a Kotlin backing property, covered here:

private val _assignments = MutableLiveData<List<AssignmentModel>>()
val assignments: LiveData<List<AssignmentModel>>
    get() = _assignments

As for why it's not working at the moment, right now you call getAssignments() once on start up. This will filter an empty assignmentMap.values (I believe - might be worth checking), because when it's called, Firebase hasn't finished getting you any data. And when Firebase does get it's new data, it triggers the listener, but you don't update the LiveData.

Mind where you're setting up your listeners/observers

A tricky thing with LiveData observers and Firebase listeners is to make sure you only set them up once.

For your Firebase listener, you should be setting up the listener when you initialize your database and not every single time you call getUserAssignments. Then you wouldn't need all the null checking in the ViewModel, which essentially ensures that at least the ViewModel won't call getUserAssignments twice....but if you have other classes interacting with your database, they might call getUserAssignments multiple times and then you have tons of extra listeners.

Also, make sure you detach your listener.

One way to handle this is described in Doug Stevenson's talk Firebase and Android Jetpack: Fit Like a Glove - the talk includes a demo code here. The part that's related to this is how he handles LiveData -- notice how the class includes adding and removing the listener. The TL;DR is that he's using LiveData's lifecycle awareness to automatically do Firebase listener setup and cleanup. How that's done is a bit complicated, so I'd suggest watching the talk from here.

For your LiveData, setup/tear down looks correct since it's getting setup in onCreateView (and torn down automatically via the fact it's lifecycle aware). I might rename updateUI to something like setupUIObservation, since updateUI sounds like something you call multiple times. As with the Firebase listeners, you want to make sure you're not setting up the same LiveData observer more than once.

Hope that helps!

Ad
source: stackoverflow.com
Ad