Skip to content

Instantly share code, notes, and snippets.

@Struka9

Struka9/blog1.md Secret

Created March 10, 2019 02:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Struka9/b64225c3a3d8a4072cf24fb1febae0cb to your computer and use it in GitHub Desktop.
Save Struka9/b64225c3a3d8a4072cf24fb1febae0cb to your computer and use it in GitHub Desktop.

The Gentle Guide to LiveData

What is LiveData?

LiveData is an observable class that holds data, it’s part of the Android Architecture Components and unlike other observables it has the advantage that it is lifecycle-aware, this means that it respects the lifecycle of other components (like activities, fragments and services) and will dispatch updates to it’s observers only when their components are on an active state.

An Observer is considered active if it’s on the ::STARTED:: or ::RESUMED:: state, any observer that is inactive will not be notified about the updates. You register the observer along with the lifecycle owner which is simply an object that implements the LifecycleOwner interface, the relationship between lifecycle owner and observers allows LiveData to safely remove observers whose lifecycle has reached the ::DESTROYED:: state. This behavior is specially useful for activities and fragments where observers can be safely removed without worrying about leaks.

Properly used, LiveData provides a solution to common problems on Android Development such as:

  • Memory Leaks
  • Cumbersome code to keep UI in sync with data
  • Crashes due to stopped activities
  • Configuration changes issues (who hasn’t locked their activities to not deal with this?)

Observing your LiveData

Usually you access your LiveData instances through a ViewModel which has been designed “to store and manage UI-related data in a lifecycle conscious way”, doing this will allow your LiveData instances to survive configurations changes such as screen rotations.

You want to start observing your LiveData as soon as possible, usually an app component’s onCreate() is the right place to do this, the observer will receive the update as soon as the component moves to it’s ::STARTED:: state.

Updating LiveData

LiveData has no publicly available methods to update the stored data. The MutableLiveData subclass exposes the setValue(T) and postValue(T) methods publicly and you must use these if you need to edit the value stored in a LiveData object. Usually MutableLiveData is used in the ViewModel and then the ViewModel only exposes immutable LiveData objects to the observers.

How do you use LiveData?

Before anything else you will need to include Google’s repository to your project, open the project level gradle file and include the repo as shown below:

allprojects {
		repositories {
			google()
			jcenter()
		}
}

Next, you will need to include the actual dependencies to the module level gradle file as shown below:

dependencies {
  def lifecycle_version = "2.0.0"

  // ViewModel and LiveData
  implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
  // alternatively - just LiveData
  implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"

::Notice that even when in most cases you will be using LiveData along with ViewModel is still possible to use it alone.::

That’s it, you are ready to use LiveData in your project, as said before you can use LiveData to maintain in sync your UI with the underlaying data without worrying about memory leaks or configuration changes:

class MainActivity : AppCompatActivity() {

    private lateinit var etName: EditText
    private lateinit var btGreet: Button
    private lateinit var tvGreetings: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...activity initialization 

        val viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
        val liveData = viewModel.greetingLiveData
        liveData.observe(this /*The owner of the lifecycle to which the observer is bound*/,
            Observer {
                tvGreetings.text = it
            })
        btGreet.setOnClickListener {
            val name = if (etName.text.isNullOrEmpty()) "World" else etName.text.toString()
            viewModel.setUserNameLongRunning(name)
        }
    }
}

Here we have a really simple example, as mentioned before in most cases you will want to have your LiveData instances provided by a ViewModel class, so the instances are decoupled from the actual lifecycle of the component observing them, once you got the instance you want to observe, you need to call the observe() method on it passing the lifecycle owner and the Observer that will be notified on data change. You really want to do this as early in the component’s lifecycle as you can (here we do it in the onCreate() method of the activity) because once a component bound to an observer passes from inactive to active state the first time it will get the up-to-date data. It’s worth to mention that if the component goes from active to inactive state and to active again it will not get an update unless the data actually changes.

In the example above we mimic a long running operation with the ::MainActivityViewModel::’s setUserNameLongRunning() method that will modify the actual value of the LiveData instance and once it changes it will be assigned to the ::TextView::’s text property. As simple as this looks, it’s a really powerful setup because it ensures all your UI shows the most recent data and protects you from crashes due to configuration changes right out of the box! YAY!

Transformations

You may want to make some changes to the value stored in a LiveData object before sending it to it’s observers or you may need to return a different LiveData based on the value of another one. In these cases Transformations class is useful, it contains map() and switchMap() helper methods that will support these cases.

Transformations.map() will apply a function to the value of a LiveData object and propagates the result downstream:

val personLiveData: LiveData<Person> = viewModel.personLiveData
val userName: LiveData<String> = Transformations.map(personLiveData) {
  person -> "${person.firstName} ${person.lastName}"
}

The Transformations.map() function will return a LiveData composed of the firstNameand lastName of the Person object stored as the value of the original LiveData, anytime the underlaying Person value changes, the username LiveData will also be updated, nice!

In addition to Transformations.map() we got Transformations.switchMap() which is very similar with the difference that the function passed to it must return a LiveData object.

Suppose you have a function that returns a ::User:: based on the id passed to it, you can easily implement this using Transformations.switchMap() as follows:

private fun getUser(id: String): LiveData<User> {
	// ...
}
val userId: LiveData<String> = ...
val user = Transformations.switchMap(userId) { id -> getUser(id) }

For everything else there's MediatorLiveData

The Transformations helper functions should satisfy your needs in a wide range of use cases, but in the case it doesn’t, the MediatorLiveData class can help you. This is a special type of MutableLiveData, it can listen to changes of other LiveData objects added as a source through the addSource() function. Under the hood the above helper functions use MutableLiveData in their implementation.

This opens a whole new array of possibilities, suppose you have two LiveData objects but you would like to wait until a value for both of them has been set, with MutableLiveData this is easily achieved as follows:

fun <A, B> waitForBoth(liveDataA: LiveData<A>, liveDataB: LiveData<B>): LiveData<Pair<A, B>> {
	return MediatorLiveData<Pair<A, B>>().apply {
        var lastA: A? = null
        var lastB: B? = null

        addSource(liveDataA) {
            if (it == null && value != null) value = null
            lastA = it
            if (lastA != null && lastB != null) value = lastA!! to lastB!!
        }

        addSource(liveDataB) {
            if (it == null && value != null) value = null
            lastB = it
            if (lastA != null && lastB != null) value = lastA!! to lastB!!
        }
    }
}

The addSource() function takes a LiveData and a Observer object, in this way it can react to the underlying updates and successfully report the results downstream.

Conclusion

LiveData is a useful tool that you should keep in your Android arsenal, it solves a lot of problems when designing the architecture of your apps with the added advantage that it will work in synergy with the other Android Architecture Components.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment