preface

The use of LiveData is almost inevitable in Android projects today. In the MVP era, we needed to define various IXXXView interfaces to communicate with Presenter. Nowadays, there are very few such interfaces. People are used to designing the communication between the presentation and logical layer in a responsive way. It is not appropriate to use it in Domain or even Data layers, but many people do.

1. Why do people use LiveData in Repository?

When I found out in my colleague’s code that LiveData should not be used in Repository, he would justifiably respond with official documentation. This is probably why many people like to use it this way, as it used to be the recommended practice in official documentation:



Developer.android.com/jetpack/gui…



Github.com/android/arc…

This is all code that has been shown in the official website documentation and Sample, and even Jetpack Room has support for LiveData, generating an API for the LiveData interface for daOs. As you can see, it is because of the official recommendation that such usage has become popular.

Attitudes today

That’s no longer the recommendation, and the latest documentation clearly limits the use of LiveData, with a particular emphasis on avoiding it in Repo

LiveDatais not designed to handle asynchronous streams of data layer. Even though you can use LiveData transformations and MediatorLiveData to achieve this, this approach has drawbacks: the capability to combine streams of data is very limited and all LiveData objects are observed on the main thread. Developer.android.com/topic/libra…

Even using LiveData in Room is considered a mistake



Github.com/cashapp/sql…

2. Disadvantages of using LiveData in Repo

Google once hoped to achieve responsive communication between VM and M in MVVM based on LiveData

However, LiveData is designed to only serve the communication scene between View and ViewModel. Due to its focused responsibilities, it has limited capability and is not suitable for non-UI scenarios, which is mainly reflected in two aspects:

  1. Thread switching is not supported
  2. Heavily dependent Lifecycle

Thread switching is not supported

Although LiveData is a subscriptable object, it does not have a thread-switch operator like RxJava or Coroutine Flow. Check the source code of LiveData and you can see that Observe can only be called from the main thread. When we subscribe to the Repo’s LiveData in the ViewModel, the data can only be received and processed in the UI thread. But the ViewModel is more responsible for logic processing, should not occupy the main thread precious resources, if the VM logic once there is a time-consuming operation will cause the UI lag.

As an aside: Time-consuming processing in A VM is inherently unreasonable, the responsibilities of a VM in a standard MVVM should be as simple as possible, and more business logic should be done in the Model or Domain layer. The Model layer is more than a simple API definition

In some business logic, we may use Transformations#map and Transformations#swichMap to transform LiveData, which are also performed on the main thread by default

class UserRepository {

    // DON'T DO THIS! LiveData objects should not live in the repository.
    fun getUsers(a): LiveData<List<User>> {
        ...
    }

    fun getNewPremiumUsers(a): LiveData<List<User>> {
        return TransformationsLiveData.map(getUsers()) { users ->
            // This is an expensive call being made on the main thread and may
            // cause noticeable jank in the UI!
            users
                .filter { user ->
                  user.isPremium
                }
          .filter { user ->
              val lastSyncedTime = dao.getLastSyncedTime()
              user.timeCreated > lastSyncedTime
                }
    }
}

Copy the code

As above, map {} is executed on the main thread, and ANR can occur when there is an IO operation like getLastSyncedTime inside

Although LiveData can provide the ability of asynchronous postValue, many complex business scenarios often require multi-segment processing of data flow. In order to achieve the so-called high performance programming, it is required that each segment of the process can be assigned a separate thread, such as RxJava observeOn and Flow flowOn capabilities, which LiveData does not have.

Heavily dependent Lifecycle

LiveData relies on Lifecycle, which is an Android UI property, Use in non-ui scenarios that will either require a custom Lifecycle (for example someone will customize the so called LifecycleAwareViewModel) or use LiveData#observerForever (which creates a risk of leaks), Jose Alcerreca also worked on ViewModels and LiveData: Patterns + AntiPatterns recommends using Transformations#switchMap to circumvention the lack of Lifecycle.

In my opinion these are not good approaches, Lifecycle should not be compromised and both viewModels and Models in MVVM should focus on platform-independent business logic.

A good ViewModel or Repository should be a pure Java or Kotlin class that does not rely on the various Andorid libraries including Lifecycle and should not hold the Context so that the code will be generic and platform independent.

3. Provide a responsive interface to the Repo

Since LiveData is not available, how do you provide a responsive API to the Repo? Once upon a time, RxJava was the most popular, and popular tripartite libraries like Retrofit have friendly support for RxJava, but now in the Kotlin era, I recommend coroutines.

There are two common types of data requests in a Repo

  1. Single request
  2. Streaming request

Single request

For example, there is a one-to-one correspondence between request and Response in common HTTP requests. At this point you can define the API using the suspend function, such as converting it to LiveData using the LiveData Builder

The LiveData Builder needs to introduce lifecyce-LiveData-ktx

implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.4.0." "
Copy the code

LiveData Builder provides a CoroutineScope that calls suspended functions while defining LiveData

class UserViewModel(private val userRepo: UserRepository): ViewModel() {
    ...
    val user = liveData { //CoroutineScope
        emit(userRepo.getUser(10))}... }Copy the code

When the LiveData Observer enters the active state for the first time, the coroutine is started. When no active Observer exists, the coroutine is automatically cancelled to avoid leakage. The LiveData Builder can also specify the timeoutInMs parameter to extend the lifetime of the coroutine

An Observer that is inactive for a short period of time due to the Activity’s withdrawal to the background will not be cancelled until the timeoutInMs has expired. This ensures that background tasks continue to be executed while avoiding resource waste.

In Migrating from LiveData to Kotlin’s Flow, Jose Alcerreca also recommends replacing ViewModel LiveData with StateFlow:

class UserViewModel(private val userRepo: UserRepository): ViewModel() {
    ...
    val user = flow { //CoroutineScope
        emit(userRepo.getUser(10))
    }.stateIn(viewModelScope)
    ...
}
Copy the code

Build a Flow using the Flow Builder and convert it to StateFlow using the stateIn operator.

Streaming request

When streaming requests are common for observing a mutable data source, such as listening for changes to a database, you can use Flow to define a responsive API

In ViewModel, we can convert the Flow in the Repo to a Livedata via Flow#asLiveData of lifecyce-LiveData-ktx

val user = userRepo
        .getUserLikes()
        .onStart { 
            // Emit first value
        }
        .asLiveData()
Copy the code

If the ViewModel does not use LiveData, use stateIn to convert to StateFlow as with single send requests.

conclusion

Due to the simple and easy use of LiveData and the early recommendation of the official website, many people will use LiveData in Domain or even Data layer and other non-UI scenarios, which is not reasonable and is no longer officially recommended. The correct approach is to define the Repo API as much as possible using suspended functions or flows, then reasonably call them in the ViewModel and convert them to LiveData or StateFlow for UI layer subscription.

More articles in series

  • One of the Seven deadly SINS of Jetpack MVVM: Taking Fragment as LifecycleOwner
  • Jetpack MVVM’s second of the seven deadly SINS: starting coroutines in launchWhenX
  • Jetpack MVVM seven deadly SINS: Loading data in onViewCreated
  • Jetpack MVVM seven deadly SINS: Sending Events using LiveData/StateFlow