Android’s usual layered architecture

Loading UI data in Android is not easy, and developers often have to deal with boundary cases. Such as various life cycles and Activity destruction and rebuilding due to “configuration changes.”

There are many scenarios for “configuration changes” : screen rotation, switching to multiple window modes, resizing Windows, switching between light and dark modes, changing the default language, changing the font size, and so on

So the common approach is to use a layered architecture. This allows developers to write UI-independent code without having to worry too much about life cycles, configuration changes, and so on. For example, we can add a Domain Layer to the Presentation Layer to hold the business logic, and use the Data Layer to shield the Data source from the upper Layer (the Data may come from a remote service, or it may be a local database).

The presentation layer can be divided into components with different responsibilities:

  • View: Handles life cycle callbacks, user events, and page jumps
  • Presenter or ViewModel: Provides data to a View without knowing the View’s life cycle, which is usually longer than the View’s

Presenters and viewModels have different mechanisms for providing data to a View. Simply put:

  • A Presenter provides data to the View by holding a reference to the View and calling the action View directly
  • The ViewModel provides data to the View by exposing observable data to the observer

The officially available observable data component is LiveData. With the official release of Kotlin 1.4.0, developers have new options: StateFlow and SharedFlow.

Recently, there are voices circulating on the Internet that LiveData is abandoned and should be replaced by Flow.

Is LiveData really that bad? Is Flow really right for you?

Don’t say what anyone says, just get close to the truth. We will discuss these two components today.

ViewModel + LiveData

In order to efficiently load UI data and achieve the best user experience, the following goals should be achieved:

  • Goal 1: Loaded data does not need to be loaded again in the configuration Change scenario
  • Goal 2: Avoid being inactive (noSTARTEDRESUMED) load data and refresh the UI
  • Goal 3: Work without interruption when configuration changes

Google officially released the Architecture Component Library in 2017: Using ViewModel + LiveData to help developers achieve this goal.

The ViewModel has a much longer life cycle than an Activity/Fragment and is not affected by “configuration changes” that cause activities/fragments to rebuild. It just meets goal 1 and goal 3.

LiveData is lifecycle aware. New values are assigned to an observer only when the lifecycle is STARTED or RESUMED, and the observer automatically unregisters, avoiding memory leaks. LiveData is useful for goals 1 and 2: It caches the latest value of the data it holds and automatically dispatches that value to the new observer.

The characteristics of LiveData

Since there are voices saying “LiveData is being deprecated”, let’s take a look at LiveData. Talk about what it does and doesn’t do, and what to be careful about when using it.

LiveData is part of the Android Jetpack Lifecycle component. It is part of the official library and can be used by Kotlin/Java.

In a word, LiveData is life-cycle aware, observable, and data holder.

Its power and role is simple: update the UI.

It has a number of features that can be considered advantages:

  • The observer callback always occurs on the main thread
  • Only a single and up-to-date data is held
  • Automatic unsubscribe
  • Provides read write and read only version narrowing permissions
  • Cooperate withDataBindingImplementing “bidirectional binding”

The observer callback always occurs on the main thread

This makes sense; LiveData is used to update the UI, so the onChanged() method of the Observer is called back on the main thread.

The principle behind LiveData is very simple, setValue() happens on the main thread (non-main thread calls throw exceptions, postValue() switches to the main thread calls setValue())). The onChanged() method then iterates over all observers.

Only a single and up-to-date data is held

As a data holder, LiveData only holds a single, up-to-date data.

Single and up to date means that LiveData holds data one ata time, and new data overwrites one.

This design is easy to understand, the data determines the PRESENTATION of the UI, the UI must be drawn using the latest data, “outdated data” should be ignored.

With Lifecycle, an observer will only receive the latest data that LiveData holds if it is active. Drawing the UI in an inactive state makes no sense and is a waste of resources.

Automatic unsubscribe

This is an important aspect of LiveData’s aware life cycle, and automatic unsubscription means that developers don’t have to manually write unsubscribed template code, reducing the possibility of memory leaks.

The principle behind them is to remove the observer while the lifecycle is in a DESTROYED state.

Available in readable and writable and readable only versions

Click to see the code
public abstract class LiveData<T> {
  @MainThread
  protected void setValue(T value) {
    // ...
  }
  
  protected void postValue(T value) {
  	// ...  
  }
  
  @Nullable
  public T getValue(a) {
    // ...}}public class MutableLiveData<T> extends LiveData<T> {
  @Override
	public void postValue(T value) {
    super.postValue(value);
  }
  @Override
  public void setValue(T value) {
    super.setValue(value); }}Copy the code

The setValue() and postValue() of the abstract LiveData classes are protected, while the implementation MutableLiveData classes are public.

LiveData provides two classes, MutableLiveData, which is readable and writable, and Imdata, which is readable only. Through the refinement of permissions, users can get what they need to avoid data anomalies caused by excessive permissions.

Click to see the code
class SharedViewModel : ViewModel() {
  private val _user : MutableLiveData<User> = MutableLiveData()
  
  val user : LiveData<User> = _user
  
  fun setUser(user: User) {
    _user.posetValue(user)
  }
}
Copy the code

Work with DataBinding for “bidirectional binding”

LiveData, in conjunction with DataBinding, can automatically drive UI changes with updated data, and if “bidirectional binding” is used, UI changes can affect data changes.


The following are also LiveData features, but I wouldn’t classify them as “design flaws” or “LiveData weaknesses.” As a developer, you should be aware of these features and handle them properly during use.

  • The value is nullable
  • You need to pass in the correct fragment subscriptionlifecycleOwner
  • whenLiveDataWhen holding data that is an “event”, you may encounter”Viscous event
  • LiveDataIt’s not shaky
  • LiveDatatransformationWork on the main thread

The value is nullable

Click to see the code
@Nullable
public T getValue(a) {
    Object data = mData;
    if(data ! = NOT_SET) {return (T) data;
    }
    return null;
}
Copy the code

LiveData#getValue() is nullable and should be used with care.

Use the correct lifecycleOwner

Fragment calling LiveData# Observe () passing this is different from viewLifecycleOwner.

The reasons have been written about before and will not be repeated here. Interested partners can take a look.

AS prevents developers from making such mistakes when lint checks.

Viscous event

Using LiveData in SnackBar, Navigation and other events describes a scenario where “data is consumed only once”. Such as displaying Snackbar, page jump event or pop-up Dialog.

Since LiveData notifies the observer of the latest data when the observer is active, a “sticky event” occurs.

For example, clicking a button will pop up a Snackbar, the lifecycleOwner will be rebuilt while the screen rotates, and the new observer will call Livedata# Observe () again, so the Snackbar will pop up again.

The solution is to make the event part of the state and not notify the observer after the event has been consumed. Two solutions are recommended:

  • KunMinX/UnPeek-LiveData

  • Using kotlin extension functions and TypeAlias neatly encapsulates LiveData that resolves “sticky” events

Default does not shake

SetValue ()/postValue() is called multiple times with the same value, and the observer’s onChanged() is called multiple times.

Strictly speaking, this is not a problem. Depending on the specific business scenario, it is easy to handle, just check whether the vlaue is the same as before before calling setValue()/postValue().

Click to see the code
class MainViewModel {
  private val _username = MutableLiveData<String>()
  val username: LiveData<String> = _username

  fun setUsername(username: String) {
    if(_username.value ! = username) _headerText.postValue(username) } }Copy the code

Transformation works on the main thread

Sometimes the data we get from the Repository layer needs to be processed, for example to get a User List from the database, and we want to get a User based on its ID.

In this case, we can use MediatorLiveData and Transformatoins to implement:

  • Transformations.map)
  • Transformations.switchMap

Click to see the code
class MainViewModel {
  val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
     convertDataToMainUIModel(data)
  }
}
Copy the code

Both map and switchMap are implemented internally using the MediatorLiveData#addSource() method, which is called on the main thread and can cause performance problems if used improperly.

Click to see the code
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
    Source<S> e = newSource<>(source, onChanged); Source<? > existing = mSources.putIfAbsent(source, e);if(existing ! =null&& existing.mObserver ! = onChanged) {throw new IllegalArgumentException(
                "This source was already added with the different observer");
    }
    if(existing ! =null) {
        return;
    }
    if(hasActiveObservers()) { e.plug(); }}Copy the code

We can use Kotlin coroutines and RxJava to implement asynchronous tasks and finally return LiveData on the main thread. Such as androidx. Lifecycle: lifecycle – livedata – KTX provides written as

Click to see the code
val result: LiveData<Result> = liveData {
    val data = someSuspendingFunction() // coroutine
    emit(data)}Copy the code

LiveData summary

  • LiveData is designed to update the UI as a life-cycle aware, observable, data holder

  • LiveData is light and very restrained, so restrained that it needs to be used with the ViewModel to show its value

  • Due to its focus on a single function, LiveData is limited in some of its methods, which are designed to force developers to code in the correct way (e.g. observers only call back in the main thread, preventing developers from updating the UI in child threads).

  • Since LiveData focuses on a single function, MediatorLiveData’s ability to manipulate data is limited if you want to use it outside of the presentation layer, and only map and switchMap occur on the main thread. You can use coroutines or RxJava in switchMap to process asynchronous tasks and finally return LiveData in the main thread. If RxJava AutoDispose is used in your project, you can even dispense with LiveData, which will be described later on Kotlin coroutine Flow.

  • I don’t like to convert LiveData to bus and let components do their job (this is my opinion)

Flow

Flow is a function provided by Kotlin language. It is part of Kotlin coroutine and used only by Kotlin.

Kotlin coroutines are used to handle asynchronous tasks, whereas flows handle asynchronous data flows.

So what’s the difference between the suspend method and Flow? What are the respective usage scenarios?

One-shot Call and Data Stream

If the following elements are displayed on a screen of our app, the part in the red box is not real-time, so it is not necessary to refresh frequently. Forwarding and liking are data with high real-time performance, which need to be refreshed regularly.

For data with low real-time performance, we can use Kotlin coroutine processing (where the data request is an asynchronous task) :

suspend fun loadData(a): Data

uiScope.launch {
  val data = loadData()
  updateUI(data)}Copy the code

However, for the real-time data, the suspension function is powerless. Some people might say, “Just return a List.” No matter what type is returned, this operation is a one-shot Call, a one-time request that ends with the result.

The example of liking and forwarding requires a structure where the data is evaluated asynchronously and can provide multiple values in sequence. In the Kotlin coroutine we have Flow.

fun dataStream(a): Flow<Data>

uiScope.launch {
  dataStream().collect { data ->
     updateUI(data)}}Copy the code

When the number of likes or retweets changes, updateUI() is executed and the UI is updated based on the latest data

The troika of Flow

There are three important concepts in FLow:

  • They are all producers.
  • Consumer
  • The major categories of Intermediaries (wholesalers and retailers)

Producers provide data in the data Flow, and thanks to the Kotlin coroutine, flows can produce data asynchronously.

The consumer consumes data within the data stream, and in the example above, the updateUI() method is the consumer.

Mediations can make changes to the data in the data stream, or even to the data stream itself, as we can understand with the help of animations in the official video:

In Android, the DataSource/Repository of the data layer is the producer of UI data; The View /ViewModel is the consumer; To put it another way, in the presentation layer, the View is the producer of user input events (such as button clicks), and the other layers are consumers.

“Cold Flow” and “Hot Flow”

You may have seen the description: “The flow is cold.”

Simply put, the cold stream data stream only produces data when there are consumers to consume it.

val dataFlow = flow {
    // The code block will only be called if there is a consumer collect
    val data = dataSource.fetchData()
    emit(data)}... dataFlow.collect { ... }Copy the code

There are special flows, such as StateFlow/SharedFlow, that are heat flows. These flows can survive without active consumers; in other words, data is generated outside the flow and then passed to the flow.

BroadcastChannel will be deprecated in Kotlin 1.6.0 and removed in Kotlin 1.7.0. Its alternatives are StateFlow and SharedFlow

StateFlow

StateFlow is also available in readable and writable and readable only versions.

SateFlow implements SharedFlow and MutableStateFlow implements MutableSharedFlow

StateFlow is very similar to LiveData, or they are positioned similarly.

StateFlow has some similarities with LiveData:

  • Available in readable and writable and readable only versions (StateFlow, MutableStateFlow)

  • Its value is unique

  • It is allowed to be shared by multiple observers (and therefore a shared data stream)

  • It will always just reproduce the latest value to the subscriber, regardless of the number of active observers

  • Support DataBinding

There are some differences:

  • Initial values must be configured
  • The value of empty safe
  • Image stabilization

The MutableStateFlow constructor forces the assignment of a non-empty value, and the value is also non-empty. This means StateFlow always has a value

The emit() and tryEmit() methods of StateFlow are internally implemented the same by calling setValue()

StateFlow is shock-proof by default. When updating data, it determines whether the current value is the same as the new value. If so, it does not update data.

SharedFlow

Like SateFlow, SharedFlow is available in two versions: SharedFlow and MutableSharedFlow.

So how are they different?

  • MutableSharedFlowNo starting value
  • SharedFlowHistorical data can be retained
  • MutableSharedFlowEmission values need to be calledemit()/tryEmit()Method,There is no setValue()methods

Unlike MutableSharedFlow, default values cannot be passed in the constructor, which means that MutableSharedFlow has no default values.

val mySharedFlow = MutableSharedFlow<Int> ()val myStateFlow = MutableStateFlow<Int> (0)... mySharedFlow.emit(1)
myStateFlow.emit(1)
Copy the code

Another difference between SateFlow and SharedFlow is that SateFlow only keeps the latest values, that is, new subscribers only get the latest and subsequent data.

SharedFlow, on the other hand, can retain historical data depending on the configuration, and new subscribers can access a series of previously transmitted data.

The principle behind this is described below

They are used for different scenarios: whether UI data is a state or an event.

States and Events

State can be the visibility of UI components, and it always has a value (show/hide)

Events fire only when one or more prerequisites are met, and do not need or should have default values

To better understand the usage scenarios of SateFlow and SharedFlow, let’s look at the following example:

  1. The user clicks the login button
  2. Invoke the server to validate the login
  3. After successful login, go to the home page

Let’s first treat Step 3 as a state:

Using state management has the same “sticky event” problem as LiveData, if in ViewNavigationState our action is to pop up the snackbar, and it has already popped up once. After you rotate the screen, the Snackbar pops up again.

If we treat Step 3 as an event:

There is no “sticky event” problem with SharedFlow. The MutableSharedFlow constructor has a replay parameter, which means that multiple previously issued values can be re-sent to new subscribers. The default value is 0.

SharedFlow keeps a specific number of recent values in its replayCache. Each new subscriber is first evaluated from replayCache and then gets the newly emitted value. The maximum size of replayCache is specified by the replay parameter when SharedFlow is created. Reset replayCache can use MutableSharedFlow. ResetReplayCache method.

When replay is 0, replayCache size is 0 and new subscribers can’t get the previous data, so there is no “sticky event” problem.

StateFlow’s replayCache always has the most current data:

At this point, the usage scenarios for StateFlow and SharedFlow are clear:

Use StateFlow; Events use SharedFlow

Comparison of use of StateFlow, SharedFlow and LiveData

The figure above shows the use of LiveData, StateFlow, and SharedFlow in ViewModel respectively.

Where LiveEventLiveData is used in LiveDataViewModel to handle “sticky events”

Use SharedFlow to handle “sticky events” in FlowViewModel

The emit() method is to suspend the function, or you can use tryEmit()

Note: The collect method of Flow cannot be written in the same lifecycleScope

FlowWithLifecycle is an extension provided after Lifecycle – Runtime – KTX :2.4.0-alpha01

Flow is much more complicated to use in fragments than LiveData. We can encapsulate an extension method to simplify:

For repeatOnLifecycle design issues, you can step through the story behind the repeatOnLifecycle API.

There is one thing to note when using the collect method.

This is wrong!

ViewModel. The headerText. Collect before coroutines cancelled will always hangs, behind this code won’t execute.

The Flow and RxJava

Flow and RxJava are very close to each other. Due to space reasons, we will not expand on them here. In this section, we will just list their corresponding relationships:

  • Flow = (cold) Flowable / Observable / Single

  • Channel = Subjects

  • StateFlow = BehaviorSubjects (Always have values)

  • SharedFlow = PublishSubjects (no initial value)

  • suspend function = Single / Maybe / Completable

Reference documents and recommended resources

  • LiveData: Let me die before it’s popular? I’ll go to your Kotlin coroutine
  • Coroutines Flow best practice | application based on the Android developer summit
  • Collect Android UI data streams in a more secure way
  • Migrate from LiveData to Kotlin data streams
  • The story behind designing the repeatOnLifecycle API
  • LiveData with Coroutines and Flow — Part I: Reactive UIs
  • LiveData with Coroutines and Flow — Part II: Launching Coroutines with Architecture Components
  • LiveData beyond the ViewModel — Reactive patterns using doubling and MediatorLiveData
  • The Benefits of StateFlow and SharedFlow over LiveData – Andrey Liashuk
  • Going with the flow – Kotlin Vocabulary
  • LiveData with Coroutines and Flow (Android Dev Summit ’19)
  • DevFest South Africa – Migrating from LiveData to Coroutines and Flow – Jossi Wolf

conclusion

  • LiveData’s main responsibility is to update the UI, to fully understand its features, rational use

  • Flow can be divided into three roles: producer, consumer and intermediary

  • The biggest difference between cold flow and hot flow is that the former depends on the existence of consumer Collect, while the hot flow always exists until it is cancelled

  • StateFlow is similar to LiveData positioning in that it must be configured with initial values, value null security, and default anti-shake

  • StateFlow and SharedFlow are used in different scenarios, the former for “states” and the latter for “events”

Back to the topic at the beginning of the article, LiveData is not that bad. Because of its single function and simple function, simplicity means it is not easy to make mistakes. So it’s a good choice to expose LiveData to the view from the ViewModel in the presentation layer. In Repository or DataSource, we can use LiveData + coroutine to handle data conversion. Of course, we can also use a more powerful Flow.

LiveData, StateFLow, and SharedFlow all have their own usage scenarios. And if used improperly, will more or less encounter some so-called “pit”. Therefore, when using a component, it is important to fully understand its design reasons and related features, otherwise you will fall into the trap of receiving behavior that does not meet your expectations.

About me

People love to do things that give them positive feedback. If this article is helpful to you, please click 👍. This is very important to me

I am Flywith24. Only through discussion with others can we know whether our own experience is real or not. By communicating with others on wechat, we can make progress together.

  • The Denver nuggets
  • Small column
  • Github
  • WeChat: Flywith24