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 review someone else’s code and find LiveData in Repository, I usually point it out as a problem, but sometimes the official recommendation is used to counter me:

For example, the above code is from the official documentation, and LiveData is supported by first-party components like Room. Perhaps it is this series of official endorsement, intentional or not, that makes many people happy to use LiveData in the relevant code of the data layer

2. What is the official attitude?

In the past, The use of LiveData has been fairly casual, but in the latest official document, the scope of LiveData use has been clearly limited, which specifically states that it should not be used in Repo:

LiveDatais not designed to handle asynchronous streams of data layer. Even though you can use LiveData doubling 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.

Room’s support for LiveData is also currently considered a bug

3. 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:

  • Thread switching is not supported
  • Heavily dependent Lifecycle

3.1 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.

“Digress: 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(): LiveData<List<User>> {\ ... \ }\ \ fun getNewPremiumUsers(): 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.

3.2 Rely heavily on 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. Lifecycle should not be compromised. In MVVM both the ViewModel and the Model 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. This code is more generic and platform independent.

4. 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

  • Single request
  • Streaming request

4.1 Sending single Requests

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

class UserViewModel(private val userRepo: UserRepository): ViewModel() {\ ... \ val user = liveData { //CoroutineScope\ emit(userRepo.getUser(10))\ }\ ... The \}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)\ ... The \}Copy the code

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

4.2 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

al 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.

5. To summarize

Due to the simplicity of LiveData, many people will use LiveData in Domain or even Data layer and other non-UI scenarios. Such usage 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.