Categories
Programming

Kotlin: Using MediatorLiveData to Use Data from Two Tables at Once

With my current app, the main data is Subjects, where each Subject have multiple Reminders tied to it. For this purpose, I have a couple of tables in the database, which in its simplified form can be seen as below:

Subject table with:

  1. Subject ID
  2. Subject title

Reminder table with:

  1. Reminder ID
  2. Reminder timestamp
  3. Subject ID

I’m using Room for database and LiveData to pull data from the tables. The data then gets supplied to a RecyclerView, where each RecyclerView item will display a Subject with its Reminders.

RecyclerView where each row is a Subject and its four Reminder dates.

The tricky part is that, since there are two tables, I have two separate methods returning LiveData object from each table. Most tutorials out there assume the use of only one LiveData to be observed by the RecyclerView, so it can get confusing when there’s two LiveData at once.

I found a smart solution on Stack Overflow that makes use of MediatorLiveData and Kotlin’s Pair to make the whole thing work. This article is an attempt to expand on that solution by writing down my understanding of it.

This article assumes familiarity with Android concepts for RecyclerView, Room, ViewModel, and LiveData.



Create a “combination” MediatorLiveData class

The first thing to do is to create a class that extends MediatorLiveData. The interesting part is that we need to use Pair<List<Subject>, List<Reminder>> as MediatorLiveData’s type.

After that, we simply construct the class by sending it LiveData<List<Subject>> and LiveData<List<Reminder>> as constructor parameters, then put those two inside a Pair, and finally set the Pair as the MediatorLiveData object’s value.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
// This class ia a MediatorLiveData created since I have two different sources of data:
// – subjects table
// – reminders table
// Since they then become two separate LiveData, and I need both to fill in the data
// on the main RecyclerView, then we're using a MediatorLiveData to grab both LiveData
// and add them as sources, so that we can notify observers when any of the data gets
// updated.
class CombinedSubjectReminders(
ldSubject: LiveData<List<Subject>>,
ldReminder: LiveData<List<Reminder>>
) : MediatorLiveData<Pair<List<Subject>, List<Reminder>>>() {
private var listSubject: List<Subject> = emptyList()
private var listReminder: List<Reminder> = emptyList()
init {
value = Pair(listSubject, listReminder)
addSource(ldSubject) {
if( it != null ) listSubject = it
value = Pair(listSubject, listReminder)
}
addSource(ldReminder) {
if( it != null ) listReminder = it
value = Pair(listSubject, listReminder)
}
}
}
Example of a MediatorLiveData class to handle two LiveDatas at once.

That is pretty much where the magic lies. The potentially tricky part is the addSource method and the value variable, both of which are built-in features of the MediatorLiveData.

Inside ViewModel

Inside the ViewModel, we call the repository’s methods for pulling LiveData from each table, then we return a “combination” object as we describe in the class above, and put in the LiveData as its parameters.

fun getSubjects(): LiveData<List<Subject>>? {
return sroRepository.getSubjects()
}
fun getReminders(): LiveData<List<Reminder>>? {
return sroRepository.getReminders()
}
fun getAllData(): CombinedSubjectReminders? {
var ldSubjects = getSubjects()
var ldReminders = getReminders()
if (ldSubjects != null && ldReminders != null) {
return CombinedSubjectReminders(ldSubjects, ldReminders)
}
return null
}
view raw MainViewModel.kt hosted with ❤ by GitHub
Methods inside ViewModel to grab two LiveData from repository and put them into one MediatorLiveData

Inside RecyclerViewAdapter

Inside the adapter, we modify its constructor to accept two Lists as parameters, the List<Subject> and the List<Reminder>.

class SubjectRecyclerViewAdapter(
val subjects: List<Subject>,
val reminders: List<Reminder>,
val listener: OnListFragmentInteractionListener?
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
Put in the two List<> as constructor parameters in RecyclerViewAdapter

Observe the MediatorLiveData

Finally, inside the fragment or activity that has the RecyclerView, we observe the returned MediatorLiveData from the ViewModel. When there’s any change, the observer gives a Pair containing a List<Subject> and a List<Reminder>. We can then put those two as the parameter of the adapter.

mainViewModel = ViewModelProvider(activity!!).get(MainViewModel::class.java)
// New observer
mainViewModel.getAllData()?.observe(viewLifecycleOwner,
Observer { subjectsAndRemindersPair ->
view.sro_subject_list.adapter = SubjectRecyclerViewAdapter(
subjectsAndRemindersPair.first,
subjectsAndRemindersPair.second,
listener)
})
view raw SubjectFragment.kt hosted with ❤ by GitHub
Observe the MediatorLiveData given by ViewModel, and update RecyclerViewAdapter accordingly.

Note how the Observer pattern uses a lambda with subjectsAndRemindersPair as the parameter. That is essentially the type of the MediatorLiveData in question, and by default Kotlin call that parameter it. I renamed it to subjectsAndRemindersPair to make it easier to understand.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s