preface

In our Android App, Kotlin Flows are typically used to collect the kind of data that the UI layer needs to present. But when you collect data, you have to make sure that it doesn’t do a lot of extra work, waste resources, or leak memory because the view layer falls back into the background or is destroyed.

Because both Kotlin Flows and RxJava can have these problems, official LiveData is a good choice. LiveData, however, lacks the composability of FLOWS and RxJava, as well as much support for handy chained operators.

So this article shows you how to use Kotlin Flows to collect data with LiveData features. Also supports various combination and transformation capabilities.

Below will introduce how to use the Lifecycle. RepeatOnLifecycle and Flow flowWithLifecycle API to avoid the waste of resources. And explain why it’s a good way to collect data at the UI level

Waste of resources

In normal coding, it is recommended to provide the Flow Api from the lower level of the architecture to the upper level. But make sure you collect/subscribe to them correctly.

A cold flow buffered by channel or using operators such as buffer, conflate, flowOn, or shareIn. When using some existing apis (such as: CoroutineScope. Launch, Flow. LaunchIn, or LifecycleCoroutineScope. LaunchWhenX) to collect is unsafe. Unless you manually cancel the Job, when the Activity enters the background. These apis keep the underlying data producers of the flow active in the background and waste resources

Note: A cold flow is a flow that executes the producer’s block of code to generate data when a new subscriber is collecting/subscribing.

An official example: use callbackFlow to continuously send location updates

/ / this class extends to FusedLocationProviderClient a locationFlow () method, the return value is a Flow
/ / implementation callbackFlow
fun FusedLocationProviderClient.locationFlow(a) = callbackFlow<Location> {
    // Callback to update location information
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?).{ result ? :return // Return if null
            // Try to send data
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    // Perform location updates
    requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
        .addOnFailureListener { e ->
            close(e) // in case of exception, close the Flow
        }
    // This is a suspend function, and the code in the block is executed when the flow is closed
    awaitClose {
      	// Remove the listener
        removeLocationUpdates(callback)
    }
}
Copy the code

Collecting/subscribing to the Flow from the UI layer and launching it using one of the apis mentioned above will result in location updates even if the View is not displayed on screen or in the background.

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

        // This is triggered when the state is at least STARTED
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // New location! Update the map}}}}Copy the code

To solve this problem, you may need to manually cancel when your View is in the background. Avoid wasting resources by sending location information all the time. For example, you might want to do the following:

class LocationActivity : AppCompatActivity() {
    // Coroutine listening for Locations
    private var locationUpdatesJob: Job? = null
    override fun onStart(a) {
        super.onStart()
        locationUpdatesJob = lifecycleScope.launch {
            locationProvider.locationFlow().collect {
                // New location! Update the map}}}override fun onStop(a) {
        // Stop collecting when the View goes to the backgroundlocationUpdatesJob? .cancel()super.onStop()
    }
}
Copy the code

This is a good way to handle it. But this code is too template and unfriendly. For us developers, we refuse to write template code. There’s another big benefit to not writing boilerplate code. If you write less code, you’ll make fewer errors!

Lifecycle.repeatOnLifecycle

Now we need to solve these problems. Two points need to be met:

  1. Simple enough to use
  2. The Api is friendly and easy to understand and remember
  3. The most important thing is safety! It should also support flows for any scenario, regardless of the implementation details of the Flow.

In the lifecycle – runtime – KTX repository support lifecycle. RepeatOnLifecycle let’s take a look at the following code:

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

        // Create a coroutine
        lifecycleScope.launch {
            // The block passed to repeatOnLifecycle is executed when the lifecycle
            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
            // It automatically restarts the block when the lifecycle is STARTED again.
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Safely collect from locationFlow when the lifecycle is STARTED
                // and stops collection when the lifecycle is STOPPED
                locationProvider.locationFlow().collect {
                    // New location! Update the map
                }
            }
        }
    }
}
Copy the code

RepeatOnLifecycle is a suspend method of the suspend tag, which has a Lifecycle.State parameter

When Lifecycle reaches the state specified, it will automatically create and start a new coroutine to execute the specified block. And when the state is reversed below the given state, it automatically removes the coroutine.

This is a good way to avoid writing template code. As you can guess, the Api needs to be executed in the activity’s onCreate or Fragment’s onViewCreated method. This avoids positional anomalies. You can write this in Fragment.

class LocationFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        // ...
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                locationProvider.locationFlow().collect {
                    // New location! Update the map}}// The code will not be executed unless the state is destroyed}}}Copy the code

Important: Fragments should always use the viewLifecycleOwner to trigger UI updates. But this does not apply to DialogFragment. It may not have a View. For DialogFragment. You should use lifecycleOwner

Just a quick explanation of how this works

RepeatOnLifecycle is a hang method. When the state reaches the specified value, an internal coroutine is started to execute the code in the block. When the state falls below the given state, the coroutine is taken to uncollect/subscribe the Flow. This is because internal monitoring is always needed to start and cancel. So the code below repeatOnLifecycle method can only be executed if the state reaches the destroyed. The specific implementation code is as follows:

Let me draw a picture of it

RepeatOnLifecycle will prevent you from wasting resources and will prevent app from crashing when lifecycle is received in an inappropriate state.

Flow.flowWithLifecycle

You can also use the flow. flowWithLifecycle operator when you have only one Flow to collect/subscribe to. This API uses repeatOnLifecycle as the underlying implementation.

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

        Collect the usage of a Flow
        lifecycleScope.launch {
            locationProvider.locationFlow()
                .flowWithLifecycle(this, Lifecycle.State.STARTED)
                .collect {
                    // New location! Update the map}}// Collect usage of multiple flows
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    flow1.collect { /* Do something */ }   
                }
                
                launch {
                    flow2.collect { /* Do something */ }
                }
            }
        }
    }
}
Copy the code

Bottom producer

Even if you use these apis, if you collect/subscribe to Hot Flow, it still wastes resources even if you don’t have anyone to collect/subscribe to it.

But there is a benefit to this, as subscribers always get the latest data to display after subsequent collections/subscriptions occur, rather than getting old data. But if you really don’t have to keep it active all the time. So here are some measures to prevent it.

MutableStateFlow and its Api provide a subscriptionCount field that you can use to determine whether to produce data.

Comparison with LiveData

You may notice how much the Api behaves like LiveData. It’s a fact! LiveData is lifecycle aware and its behavior is an ideal way to subscribe to data streams from the UI layer. And Lifecycle. RepeatOnLifecycle and Flow. FlowWithLifecycle similar APIs

Using these apis to replace LiveData is unique to the Kotlin project. If you use these apis to collect, LiveData has no advantage over coroutines and flows. After all, Flow can be more combinatorial and transformable, and data can be collected from any Dispatcher. It also supports many operators to meet the requirements of various scenarios. LiveData, by contrast, has very few operators available. And you can only subscribe from the UI thread.

Data Binding supports StateFlow

Also, one possible reason you might use LiveData is because it supports data binding. Now StateFlow is supported. More information can be found here

conclusion

Using Lifecycle. RepeatOnLifecycle or Flow. FlowWithLifecycle Api can be more safe to collect the UI layer to the data

I have read all of them. Pay attention to the public account

If you have any questions, please leave a comment

The original link