Android Jetpack analysis – LiveData

LiveData is an observable data storage class. Unlike regular observable classes, LiveData is lifecycle aware, meaning that it follows the lifecycle of other application components (such as activities, fragments, or services). This awareness ensures that LiveData only updates app component observers that are in an active lifecycle state.

LiveData considers an observer (represented by the Observer class) to be active if its life cycle is in the STARTED or RESUMED state. LiveData will only notify active observers of the message. Inactive observers registered to observe LiveData objects will not be notified of changes.

You can register observers paired with objects that implement the LifecycleOwner interface. With this relationship, secondary observers can be removed when the corresponding Lifecycle object’s state changes to DESTROYED. This is especially useful for activities and fragments. Because they can safely observe LiveData objects without worrying about leakage (when the activity and fragment life cycles are destroyed, the system will unsubscribe them immediately).

1. Advantages of using LiveData

Using LiveData has the following advantages:

Ensure the interface matches the data state
LiveData follows the observer pattern. When the underlying data changes, LiveData notifies the Observer object. You can integrate code to update the interface in the Observer object. This way you don’t have to update the UI every time your app data changes, as the observer will do it for you.

No memory leak

Observers are bound to Lifecycle objects and clean themselves up after their associated lifecycle is destroyed.

No crash caused by Activity stopping
If the observer’s life cycle is in an inactive state (such as returning to an activity on the stack), it will not receive any LiveData events.

No need to manually handle lifecycle
The interface component only observes relevant data and does not stop or resume observation. LiveData will automatically manage all these operations because it is aware of relevant lifecycle state changes when observing.

Data is always up to date
If the lifecycle becomes inactive, it will receive the latest data when it becomes active again. For example, an Activity that was once in the background will receive the latest data immediately after returning to the foreground.

Appropriate configuration changes
If an activity or fragment is recreated due to a configuration change (such as device rotation), it immediately receives the latest available data.

Shared resources
You can use the singleton pattern to extend LiveData objects to encapsulate system services so that they can be shared among applications. The LiveData object is linked to the system service once, and then any observations that require the corresponding resource only need to observe the LiveData object.

2. Use LiveData object

Follow these steps to use the LiveData object:

Create an instance of LiveData to store some type of data. This is usually done in the ViewModel class.

Create an Observer object that defines the onChanged() method, which controls what happens when the data stored in the LiveData object changes. Typically, you create Observer objects in an interface controller (such as an activity or fragment).

Attach an Observer object to a LiveData object using the observer method. The observer() method takes the LifecycleOwner object. This subscribes the Observer object to the LiveData object so that it is notified about changes. Typically, you attach Observer objects in interface controllers such as activities or fragments.

Note: You can use the observeForever(Onserver) method to register an observer without an associated LifecycleOwner object. In this case, the observer is considered to be always active, so it will always receive notifications about modifications. You can remove these observers by calling the removeObserver(Observer) method.

When you update the value stored in the LiveData object, it triggers all registered observers (only the attached LifecycleOwner is active).
LiveData allows interface controllers to observe subscription updates. When the data stored by the LiveData object changes, the interface automatically updates in response.

2.1. Create LiveData object

LiveData is a wrapper container that can be used for any data, including objects that implement Collections, such as List. LiveData objects are typically stored in ViewModel objects and accessed through getter methods, as shown in the following example:

class NameViewModel: ViewModel() {
    // Create a LiveData with a String
    val currentName: MutableLiveData<String> by lazy {
        MutableLiveData<String>()
    }
    
    // Rest of the ViewModel...
}

Initially, the data in the LiveData object is not set.
markdown copy code Note: Please ensure that the LiveData object used to update the interface is stored in the ViewModel object instead of storing it in the activity or fragment for the following reasons:

  1. Avoid activities and fragments that are too large. Now, these interface controllers are responsible for displaying data, but not for storing data state.
  2. Decouple a LiveData instance from a specific Activity or Fragment instance and enable the LiveData object to survive configuration changes.

2.2. Observe LiveData objects

In most cases, the application component’s onCreate() method is the right place to start observing LiveData objects for the following reasons:

  • Ensure that the system does not make redundant calls from the Activity or Fragment’s onResume() method.
  • Ensure that the activity or fragment has data that can be displayed immediately after it becomes active. Once the application component is in the STARTED state, it receives the latest value from the LiveData object it is observing. Only the LiveData object that is set to be observed accepts the latest value. This only happens if the LiveData object to be observed is set.

Typically, LiveData only sends updates when the data changes, and only to active observers. An exception to this behavior is that observers are updated when they change from inactive to active. Additionally, if the observer changes from inactive to active for a second time, it will only be updated if the value has changed since it last became active.

The following example code illustrates how to start observing a LiveData object:

class NameActivity: AppCompatActivity() {
    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artiface
    private val model: NameViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstance)
        
        // Other code to setup the activity...
        
        // Create the observer which updates the UI.
        val nameObserver = Observer<String> { newName ->
            // Update the UI, int this case, a TextView
            nameTextView.text = newName
        }
        
        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        model.currentName.observe(this, nameObserver)
    }
}

Immediately after calling observer() passing the nameObserver parameter, onChanged() is called, providing the latest value stored in mCurrentName. If the LiveData object has not yet set a value in mCurrentName, the system does not call onChanged().

2.3. Update LiveData object

LiveData has no publicly available methods to update stored data. The MutableLiveData class will expose setValue(T) and postValue(T) methods, which you must use if you need to modify the value stored in the LiveData object. Normally you would only use MutableLiveData in a ViewModel, and then the ViewModel would only expose the immutable LiveData object to observers.

After setting up the observer relationship, you can update the value of the LiveData object (as shown in the following example) so that all observers are fired when the user taps a button:

button.setOnClickListener {
    val anotherName = "John Doe"
    model.currentName.setValue(anotherName)
}

Calling setValue(T) in this example causes the observer to call its onChanged() method with the value John Doe. This example demonstrates the button press method, but you can also call setValue() or postValue() to update mName for a variety of reasons, including the corresponding network request or the completion of the database load. In all cases, calling setValue() or postValue() will trigger the observer and update the interface.

Note: You must call setValue(T) method one to update the LiveData object from the main thread. If executing in a worker thread, you can use the postValue(T) method instead to update the LiveData object.

2.4. Using LiveData with Room

The Room persistence library supports observable queries that return LiveData objects. Observable queries are part of the Database Access Object (DAO).

When the database is updated, Room generates all the code needed to update the LiveData object. The generated code runs the query asynchronously on a background thread when needed. This mode helps keep the data displayed in the interface in sync with the data stored in the database.

3. LiveData in application architecture

LiveData has life cycle awareness and follows the life cycle of entities such as activities and fragments. You can use LiveData to pass data between these lifecycle owners and other objects with different lifecycles (such as ViewModel objects). ViewModel’s main responsibility is to load and manage interface-related data, so it is very suitable as an alternative to persisting LiveData objects. You can create LiveData objects in the ViewModel and then use these objects to expose state to the interface layer.

Activities and fragments should not retain LiveData instances because their purpose is to display data, not to maintain state. In addition, if activities and fragments do not need to retain data, it can also simplify the writing of unit tests.

You may be tempted to use LiveData objects in your data layer classes, but LiveData is not suitable for handling asynchronous data streams. While you can use a LiveData transformation and MediatorLiveData to achieve this, the disadvantage of this approach is that the functionality for combining data streams is very limited, and all LiveData objects (including those created through transformations) are observed in the main thread. Below is a sample code that shows how retaining LiveData in the Respository blocks the main thread:

class UserRepository {
    // DON'T DO THIS! LiveData objects should not live in the repository
    fun getUsers(): LiveData<List<User>> {
        ...
    }
    
    fun getNewPremiumUsers(): LiveData<List<User>> {
        return getUsers().map { user ->
            // This is an expensive call being made on the main thread and may
            // cause noticeable jank in the UI!
            users
                .filter { user ->
                    user.isPermium
                }
            .filter { user ->
                val lastSyncedTime = dao.getLastSyncedTime()
                user.timeCreated > lastSyncedTime
            }
        }
    }
}

If you need to use data flows in other layers of your app, consider using Kotlin Flow and then convert the Kotlin Flow to LiveData in the ViewModel using asLiveData().

4. Extend LiveData

If the observer’s lifetime is in the STARTED or RESUMED state, LiveData will consider the observer to be active. The following example code illustrates how to extend the LiveData class:

class StockLiveData(symbol: String): LiveData<BigDecimal>() {
    private val stockManager = StockManager(symbol)
    
    private val listener = { price: BitDecimal ->
        value = price
    }
    
    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }
    
    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }
}

The price listener implementation in this example includes the following important methods:

  • When a LiveData object has active observers, the onActive() method is called. This means that you need to start watching stock price updates from this method.
  • When the LiveData object does not have any active observers, the onInactive() method is called. Since there are no observers listening, there is no reason to stay connected to the StockManager service.
  • The setValue(T) method will update the value of the LiveData instance and notify active observers of the change.

You can use StockLiveData class as follows:

public class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val myPriceListener: LiveData<BigDecimal> = ...
        myPriceListener.observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
            //Update the UI
        })
    }
}

The observe() method passes the LifecycleOwner associated with the Fragment view as the first parameter. Doing so means that this observer is bound to the Lifecycle object associated with the owner, which means:

  • If the Lifecycle object is not active, the observer will not be called even if the value changes.
  • After the Lifecycle object is destroyed, the observer will be automatically removed.

The fact that LiveData objects are lifecycle aware means that you can share these objects between multiple activities, fragments, and services. To keep the example simple, you can implement the LiveData class as a singleton as follows:

class StockLiveData(symbol: String): LiveData<BitDecimal>() {
    private val stockManager: StockManager = StockManager(symbol)
    
    private val listener = { private: BigDecimal ->
        value = price
    }
    
    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }
    
    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }
    
    companion object {
        private lateinit var sInstance: StockLiveData
        
        @MainThread
        fun get(symbol: String): StockLiveData {
            sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
            return sInstance
        }
    }
}

And you can use it in Fragment like this:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        StockLiveData.get(symbol).observe(viewLifecycleOwner, Observer<BigDecimal> { price: BigDecimal? ->
        //Update the UI
    }
}

Multiple fragments and activities can observe MyPriceListener instances. LiveData will connect to one or more system services only if the service is visible and active.

5. Convert LiveData

You may want to make changes to the values stored in a LiveData object before dispatching it to an observer, or you may need to return a different LiveData instance based on the value of another instance. The Lifecycle package provides the Transformations class, which includes helper methods to handle these situations.

Transformations.map()
:Apply a function to the value stored in the LiveData object and pass the result downstream.

val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = userLiveData.map {
    user -> "${user.name} ${user.lastName}"
}

Transformations.switchMap()
: Similar to map(), applies a function to the value stored in the LiveData object and unmarshals and dispatches the result downstream. The function passed to switchMap() must return a LiveData object, as shown in the following example:

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

You can use conversion methods to transfer information during the observer’s lifetime. Transformations are not evaluated unless an observer is observing the returned LiveData object. Because conversions are calculated lazily, lifecycle-related behavior is passed on implicitly without the need for additional explicit calls or dependencies.

If you think you need a Lifecycle object in the ViewModel object, then converting it may be a better solution. For example, suppose you have an interface component that accepts an address and returns the zip code for that address. You can implement a simple ViewModel for this component, as shown in the following sample code:

class MyViewModel(private val repository: PostalCodeRepository): ViewModel() {
    private fun getPostalCode(address: String): LiveData<String> {
        // DON'T DO THIS
        return repository.getPostCode(addredd)
    }
}

The interface component then needs to unregister the previous LiveData object and register a new instance each time getPostalCode() is called. In addition, if the interface component is recreated, it will make another call to the repository.getPostCode() method instead of using the result of the previous call.

You can also implement a postal code lookup as a transformation of an address input, as shown in the following example:

class MyViewModel(private val repository: PostalCodeRepository): ViewModel() {
    private val addressInput = MutableLiveData<String>()
    val postalCode: LiveData<String> = addressInput.switchMap{ address -> repository.getPostCode(address) }
    
    private fun setInput(address: String) {
        addressInput.value = address
    }
}

In this case, the postalCode field is defined as the conversion of addressInput. As long as your app has an active observer associated with the postalCode field, the field’s value will be recalculated and retrieved every time addressInput changes.

This mechanism allows lower-level applications to create LiveData objects that are computed on demand in a lazy manner. The ViewModel object can easily obtain a reference to the LiveData object and then define transformation rules based on it.

5.1. Create a new transformation

There are a dozen different specific transformations that may be useful in your app, but they are not provided by default. To implement your own transformations, you can use the MediatorLiveData class, which can listen to other LiveData objects and handle the events they emit. MediatorLiveData correctly propagates its state to the source LiveData object.

6. Merge multiple LiveData sources

MediatorLiveData is a subclass of LiveData that allows you to merge multiple LiveData sources. Observers for MediatorLiveData objects are fired whenever any of the original LiveData source objects changes.
For example, if your interface has a LiveData object that can be updated from a local database or the network, you can add the following source to the MediatorLiveData object:

LiveData object associated with data stored in the database.

LiveData object associated with data accessed from the network.
Your activity simply observes the MediatorLiveData object to receive updates from both sources.

Android learning notes

Android performance optimization: https://qr18.cn/FVlo89
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android audio and video: https://qr18.cn/Ei3VPD
Jetpack family bucket chapter (including Compose): https://qr18.cn/A0gajp
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap