Original address: medium.com/androiddeve…

Author: medium.com/josealcerr…

Published: May 17, 2021-9 minutes to read

LiveData is what we need in 2017. Observer mode made our lives easier, but at the time options like RxJava were too complex for beginners. The Architecture Components team created LiveData: a very opinionated observable data holder class designed specifically for Android. This class is kept simple to get started, and it is recommended to use RxJava for more complex reactive flow cases to take advantage of the integration between the two.

Death data?

LiveData continues to be our solution for Java developers, beginners, and simple situations. For others, a good option is to move to Kotlin Flows. Flows still have a steep learning curve, but they are part of the Kotlin language and are supported by Jetbrains; And Compose is coming, which fits nicely with the reactive model.

We’ve been talking for some time about using processes to connect different parts of your application, except the view and ViewModel. Now that we have a more secure way to collect Android UIs traffic, we can create a complete migration guide.

In this article, you’ll learn how to expose streams to views, how to collect them, and how to fine-tune them to suit specific needs.

Process: Simple things are harder, complex things are easier

LiveData does one thing, and it does it well: it exposes the data, caches the latest values and understands the Android life cycle. As we later learned, it can also start looping programs and create complex transformations, but this is a more complex problem.

Let’s take a look at some LiveData schemas and their Flow counterparts.

#1: Expose the result of an operation with a mutable data holder

This is a classic pattern where you mutate a state retainer with the result of a Coroutine.

Expose the results of an operation with a LiveData holder

<! -- Copyright2020 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

class MyViewModel {
    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
    val myUiState: LiveData<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}
Copy the code

To do the same for processes, we use Mutable StateFlow.

Use a variable data holder (StateFlow) to display the results of an operation.

class MyViewModel {
    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}
Copy the code

StateFlow is a special type of SharedFlow (which is a special type of Flow), closest to LiveData.

  • It always has a value.
  • It has only one value.
  • It supports multiple observers (so streams are shared).
  • It always copies the most recent value at subscription time, regardless of the number of active observers.

Use StateFlow when exposing UI state to views. It is a safe, efficient observer, designed to keep the UI state.

#2: Expose the result of an operation

This is equivalent to the previous section, which exposes the result of a coroutine call with no mutable support attributes. In LiveData, we use the LiveData Coroutine Builder to do this.

Expose the result of an operation (LiveData)

class MyViewModel(...). : ViewModel() {val result: LiveData<Result<UiState>> = liveData {
        emit(Result.Loading)
        emit(repository.fetchItem())
    }
}
Copy the code

Since the state holder always has a value, it’s a good idea to wrap our UI state with some kind of result class that supports states such as load, success, and error.

The Flow equivalent is a little bit more, because you have to do some configuration.

Expose the result of an operation (StateFlow)

class MyViewModel(...). : ViewModel() {val result: StateFlow<Result<UiState>> = flow {
        emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
        initialValue = Result.Loading
    )
}
Copy the code

StateIn is a Flow operator that converts a Flow to StateFlow. Let’s trust these parameters for a moment, because we need more complexity to interpret it properly.

#3: One-time data loading with parameters

Suppose you want to load some data depending on the user ID, and you get this information from an AuthManager that exposes the Flow.

One-off data loading with parameters (LiveData)

With LiveData, you do something similar.

class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: LiveData<String? > = authManager.observeUser().map { user -> user.id }.asLiveData()val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
        liveData { emit(repository.fetchItem(newUserId)) }
    }
}
Copy the code

SwitchMap is a transformation where the principal is executed and the result is subscribed when the userId changes.

If there is no reason for the userId to be LiveData, a better alternative is to combine a Flow with a Flow and eventually convert the exposed results to LiveData.

class MyViewModel(authManager... , repository...) : ViewModel() {private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
       repository.fetchItem(newUserId)
    }.asLiveData()
}
Copy the code

Doing this with Flow looks very similar.

One-time data load with > parameters (StateFlow)

Note that if you need more flexibility, you can also use transformLatest and launch projects explicitly.

    valresult = userId.transformLatest { newUserId -> emit(Result.LoadingData) emit(repository.fetchItem(newUserId)) }.stateIn(  scope = viewModelScope, started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser // Note the different Loading states
    )
Copy the code

#4: Observe the data stream with parameters

Now let’s make this example more reactive. Data is not captured, but observed, so we automatically propagate changes to the data source to the user interface.

Continuing our example: Instead of calling fetchItem on the data source, we use an imaginary observeItem operator that returns a Flow.

With LiveData, you can convert streams to LiveData and send out all updates.

Observing a LiveData with parameters

class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: LiveData<String? > = authManager.observeUser().map { user -> user.id }.asLiveData()val result = userId.switchMap { newUserId ->
        repository.observeItem(newUserId).asLiveData()
    }
}
Copy the code

Alternatively, it is better to combine the two streams using flatMapLatest, converting only the output to LiveData.

class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: Flow<String? > = authManager.observeUser().map { user -> user? .id }val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.asLiveData()
}
Copy the code

Flow is implemented similarly, but without the LiveData conversion.

Observe a flow with parameters (StateFlow)

class MyViewModel(authManager... , repository...) : ViewModel() {private valuserId: Flow<String? > = authManager.observeUser().map { user -> user? .id }val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser
    )
}
Copy the code

The exposed StateFlow receives updates every time a user changes or the user’s data in the repository changes.

#5 Combine multiple sources. MediatorLiveData -> Flow.combined

MediatorLiveData lets you observe one or more update sources (LiveData observers) and do something when they get new data. Normally, you update the value of MediatorLiveData.

val liveData1: LiveData<Int> =...val liveData2: LiveData<Int> =...val result = MediatorLiveData<Int>() result.addSource(liveData1) { value -> result.setValue(liveData1.value ? :0+ (liveData2.value ? :0)) } result.addSource(liveData2) { value -> result.setValue(liveData1.value ? :0+ (liveData2.value ? :0))}Copy the code

The Flow equivalent is more straightforward.

val flow1: Flow<Int> =...val flow2: Flow<Int> =...val result = combine(flow1, flow2) { a, b -> a + b }
Copy the code

You can also use the combinedTransform function, or zip.

Configure exposed StateFlow (the stateIn operator

We used stateIn earlier to convert a normal flow to StateFlow, but it requires some configuration. If you don’t want to go into that right now, just copy and paste, this is the combination I recommend.

val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
Copy the code

However, if you’re not sure about that seemingly random 5-second start parameter, read on. StateIn takes three parameters (from the documentation).

@param Scope starts the shared Coroutine scope. @Param Started Controls the policy for when to start and stop sharing. @param initialValue the initialValue of the state flow. When using a ` replayExpirationMillis' parameters [SharingStarted. WhileSubscribed] strategy resetting the state of flow, also can use this value.Copy the code

Started can take three values.

  • Lazily: start when the first subscriber appears and stop when the scope is cancelled.
  • Eagerly: Start immediately and stop when the range is cancelled.
  • WhileSubscribed. It’s complicated.

For one-time operations, you can use Lazily or gbit/s. However, if you are looking at other processes you should use WhileSubscribed to do a number of small but important optimizations, as described below.

WhileSubscribed strategy

When no collector is available, WhileSubscribed disconnects upstream streams. The StateFlow created using stateIn exposes data to the View, but it is also observing flows from other layers or applications (upstream). Keeping these flows active can result in a waste of resources, for example, if they continue to read data from other sources, such as database connections, hardware sensors, and so on. When your application goes to the background, you should be a good citizen and stop these polling programs.

WhileSubscribed requires two parameters.

public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
)
Copy the code

Stop the overtime

According to its documentation.

StopTimeoutMillis configures the delay (in milliseconds) between the disappearance of the last user and the cessation of the upstream stream. Its default value is zero (stop immediately).

This is useful because you don’t want to cancel upstream traffic when the view stops listening for a fraction of a second. This happens all the time — for example, when the user rotates the device, the view is quickly destroyed and recreated.

The solution in the liveData Coroutine builder is to add a 5 second delay, after which the Coroutine will be stopped if there are no subscribers. WhileSubscribed(5000) does exactly that.

class MyViewModel(...). : ViewModel() {val result = userId.mapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}
Copy the code

This method checks all boxes.

  • When a user sends your application to the background, updates from other layers stop after 5 seconds, saving batteries.
  • The latest values will still be cached, so when the user comes back, the view will immediately have some data.
  • Subscriptions are restarted and new values come in, refreshing the screen as they become available.

Replay expired

If you don’t want the user to see stale data after they’ve been away for too long and you prefer to display a load screen, look at the replayExpirationMillis parameter in WhileSubscribed. It’s handy in this case, and it saves some memory because the cached values revert to the initial values defined in stateIn. Going back to the app won’t be as fast, but you won’t show old data.

ReplayExpirationMillis – Configures the delay (in milliseconds) between a shared program stopping and reslowing memory reset (this makes the shareIn operator’s cache empty and resets the cached value to the original initial value of the stateIn operator). Its default value is long.max_value (always hold the redo buffer, never reset the buffer). Use a zero value to expire the cache immediately.

View StateFlow from the view

As we’ve seen so far, it is important for views to let StateFlows in the ViewModel know that they are no longer listening. However, as with all things life cycle, it’s not that simple.

To collect a process, you need a Coroutine. Activities and fragments provide a bunch of Coroutine builders.

  • Activity. LifecycleScope. Launch: start the coroutine, immediately and cancel it when Activity is destroyed.
  • Fragments. LifecycleScope. Launch: immediately start the circulation process, and the Fragment is destroyed when cancel it.
  • Fragments. ViewLifecycleOwner. LifecycleScope. Launch: immediately launched the coroutine and view the Fragment lifecycle is destroyed to cancel it. If you are modifying the user interface, you should use the view life cycle.

LaunchWhenStarted, launchWhenResumed…

A specialized version of launch, called launchWhenX, will wait until the lifecycleOwner is in the X state and suspend the Coroutine when the lifecycleOwner drops to the X state. It is worth noting that they do not cancel loops until their lifecycle owners are destroyed.

It is not safe to collect streams with launch/launchWhenX

Receiving updates while the application is in the background can cause a crash, which can be resolved by pausing the collection in the view. However, when the application is in the background, upstream flows remain active and can waste resources.

This means that everything we’ve done so far to configure StateFlow will be pretty useless; However, there is a new API in town.

Lifecycle. RepeatOnLifecycle rescue

This new Coroutine Builder (available from Lifecycle – Runtime-kTX 2.4.0-alpha01) is exactly what we need: it starts Coroutines in a specific state and stops them when the lifecycle owner is below that state.

Different traffic collection methods

For example, in a Fragment.

onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... }}}}Copy the code

This will start collecting when the Fragment’s view is STARTED, continue until the RESUMED, and stop when it returns to STOPPED. Read all about it in safer Ways to Collect Traffic from Android UIs.

Mixing the repeatOnLifecycle API with the StateFlow guidance above will give you the best performance while making good use of your device’s resources.

StateFlow exposed by WhileSubscribed(5000) and StateFlow collected by repeatOnLifecycle(STARTED)

Warning. StateFlow support, recently added to the Data Binding, uses launchWhenCreated to collect updates, and when it reaches stability, it will start using repeatOnLifecycle instead.

For data binding, you should use streams everywhere and simply add asLiveData() to expose them to the view. Data binding will be updated when Lifecycle – Run-time – KTX 2.4.0 becomes stable.

conclusion

The best way to expose data from the ViewModel and collect data from the view is.

  • ✔️ exposes a StateFlow, using the WhileSubscribed strategy, which has a timeout. [example]
  • ✔️ collect with repeatOnLifecycle. [for example]

Any other combination keeps upstream streams active and wastes resources.

  • ❌ in lifecycleScope. Launch/launchWhenX use WhileSubscribed and collecting to express.
  • ❌ exposed using Lazily/ gbit/s and collected by repeatOnLifecycle.

Of course, if you don’t need the full functionality of Flow…… Just use LiveData. 🙂

Thanks to Manuel, Wojtek, Yigit, Alex Cook, Florina and Chris!


www.deepl.com translation