This is the second day of my participation in the August More text Challenge. For details, see:August is more challenging

First of all, to admit that this series is a bit of a headline bash, Jetpack’s MVVM itself is not to blame, but some misuse by the developers. This series will share some common AAC misuses to help you build healthier application architectures

Flow vs LiveData

With StateFlow/ SharedFlow, the official recommendation to replace LiveData with Flow in MVVM began. (See article: Migrating from LiveData to Kotlin Data Streams)

Flow is based on coroutine implementation and has rich operators, through which you can switch threads and process streaming data, which is more powerful than LiveData. The only drawback is that it can’t sense the life cycle the way LiveData does.

The aware lifecycle brings at least two benefits to LiveData:

  1. Avoid leaks: When lifecycleOwner enters DESTROYED, the Observer is automatically deleted
  2. Resource savings: Start accepting data when lifecycleOwner is STARTED, avoiding invalid calculations when the UI is in the background.

Flow also needs to do the above two things to truly replace LiveData.

lifecycleScope

Lifecycle – runtime – KTX library provides lifecycleOwner. LifecycleScope extensions, can at the end of the current Activity or fragments to destroy this coroutines, prevent leakage.

Flow also runs in coroutines, and the lifecycleScope can help Flow solve memory leaks:

lifecycleScope.launch {
    viewMode.stateFlow.collect { 
       updateUI(it)
    }
}
Copy the code

Although the memory leak has been fixed, lifecyclescope. launch starts the coroutine immediately and then runs until the coroutine is destroyed, unlike LiveData, which only executes when the UI is in the foreground, which is a waste of resources.

Thus lifecycle Run-time KTX provides LaunchWhenStarted and LaunchWhenResumed (hereinafter collectively referred to as LaunchWhenX)

The advantages and disadvantages of launchWhenX

LaunchWhenX waits until lifecycleOwner enters the X state and suspends the coroutine when it leaves the X state. The combination of lifecycleScope + launchWhenX finally gives Flow the same lifecycle awareness as LiveData:

  1. Avoid leaks: When lifecycleOwner enters DESTROYED, lifecycleScope terminates the coroutine
  2. Resources saved: launchWhenX resumes execution when lifecycleOwner goes STARTED/RESUMED, otherwise hangs.

But for launchWhenX, when lifecycleOwner leaves the X state, the coroutine just hangs the coroutine, not destroys it. If you subscribe to Flow with that coroutine, it means that the Flow collection is suspended, but the upstream processing continues. The problem of waste of resources has not been thoroughly addressed.

Waste of resources

Give an example of the waste of resources to further understand

fun FusedLocationProviderClient.locationFlow(a) = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?).{ result ? :return
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    // Keep getting the latest geographical location
    requestLocationUpdates(
        createLocationRequest(), callback, Looper.getMainLooper())

}
Copy the code

As mentioned above, you use callbackFlow to encapsulate a service that gets location in GoogleMap. RequestLocationUpdates gets the latest location in real time and returns it via Flow

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        // When STATED, collect starts to receive data
        // When STOPED is entered, collect is suspended
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // Update the UI}}}}Copy the code

When LocationActivity into STOPED lifecycleScope. LaunchWhenStarted hangs, stop accepting Flow data, and subsequent stop updating the UI. However, requestLocationUpdates in callbackFlow are still ongoing, resulting in a waste of resources.

Therefore, subscribing to Flow even in launchWhenX is not enough to completely avoid the waste of resources

Solution: repeatOnLifecycle

Lifecycle – runtime – KTX since 2.4.0 – alpha01, provides a new coroutines constructor lifecyle. RepeatOnLifecycle, it destroyed coroutines when leaving state of X, then the state of X when restarting coroutines. This is also intuitive from the name, which repeatedly starts new coroutines around the comings and goes of a lifecycle.

Using repeatOnLifecycle can compensate for the above disadvantages of launchWhenX only suspending coroutines without destroying them. Therefore, the correct way to subscribe to Flow is as follows (for example in a Fragment) :

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

Data collection starts when the Fragment is in the STARTED state, continues when the Fragment is in the STOPPED state, and finally ends when the Fragment enters the STOPPED state.

Note that repeatOnLifecycle itself is a suspended function. Once called, there will be no subsequent code that will be DESTROYED unless lifecycle enters the DESTROYED window.

Cold flow or heat flow

By the way, the previous map SDK example is a cold flow example. Is repeatOnLifecycle necessary for hot flow (StateFlow/SharedFlow)? I think the heat flow usage scenario would be less like the previous example, but in the StateFlow/SharedFlow implementation, each FlowCollector would need to be allocated some resources, and it would be beneficial if the FlowCollector could be destroyed. Meanwhile, in order to maintain the unity of writing method, it is suggested to use repeatOnLifecycle regardless of cold flow and heat flow

Finally: Flow. FlowWithLifecycle

When we have only one Flow to collect, we can simplify the code by using flowWithLifecycle as a Flow operator

lifecycleScope.launch {
     viewMode.stateFlow
          .flowWithLifecycle(this, Lifecycle.State.STARTED) .collect { ... }}Copy the code

Of course, the essence of this is the encapsulation of repeatOnLifecycle:

public fun <T> Flow<T>.flowWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowWithLifecycle.collect {
            send(it)
        }
    }
    close()
}
Copy the code


series

One of Jetpack MVVM’s seven deadly SINS: Using fragments as LifecycleOwner