In Android applications, you typically need to collect Kotlin data streams from the UI layer in order to display data updates on the screen. At the same time, you will want to collect this data flow to avoid unnecessary operations and waste of resources (both CPU and memory), as well as data leakage when the View enters the background.

. This article will take you to learn how to use LifecycleOwner addRepeatingJob, Lifecycle. RepeatOnLifecycle and Flow. The flowWithLifecycle API to avoid the waste of resources; It also explains why these apis are suitable as the default choice when collecting data flows at the UI layer.

Waste of resources

Regardless of the implementation of the data Flow producer, we recommend exposing the Flow API from the lower level of the application. However, you should also ensure that the data flow collection operation is secure.

Using some existing apis (such as CoroutineScope. Launch, Flow. LaunchIn or LifecycleCoroutineScope launchWhenX) collection based on the channel or use with buffer operators (such as Buffer, conflate, flowOn, or shareIn) is not safe unless you manually cancel the Job that started the coroutine when the Activity entered the background. These apis keep items alive when internal producers send them to the buffer in the background, which wastes resources.

Note:Cold flowIs a type of data stream that executes a producer’s block of code on demand while new subscribers collect data.

For example, the following example uses callbackFlow to send a flow of location updates:

// Cold flow based on Channel implementation, can send location updates
fun FusedLocationProviderClient.locationFlow(a) = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?).{ result ? :return
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
        .addOnFailureListener { e ->
            close(e) // Close Flow when an exception occurs
        }
    // Clean up at the end of Flow collection
    awaitClose {
        removeLocationUpdates(callback)
    }
}
Copy the code

Note: callbackFlow is used internallychannelImplementation, its concept and blockingThe queueVery similar, and the default capacity is 64.

Using any of the above apis to collect this data stream from the UI layer will cause it to keep sending location information, even if the view no longer shows the data! The following is an example:

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

        // Collect data from the data stream when the View is in the STARTED state
        // Full resume collect operation by Full resume when life cycle is in STOPPED state.
        // Cancel data flow collection when the View is in the DESTROYED state.
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // New location! Update the map}}// The same problem exists:
        // -lifecyclescope.launch {/* Collect data from locationFlow() here */}
        // - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)}}Copy the code

LifecycleScope. LaunchWhenStarted suspends the execution of coroutines. Although the new location information is not processed, the callbackFlow producer continues to send location information. Using the Lifecyclescope. launch or launchIn API is even more dangerous because the view will continue to consume location information, even if it’s in the background! This situation may cause your application to crash.

To solve the problems that these apis bring, you need to manually cancel the collection when the view goes into the background to cancel callbackFlow and avoid the location provider continuously sending items and wasting resources. For example, you can do something like this:

class LocationActivity : AppCompatActivity() {

    // Location coroutine listener
    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 data when the view enters the backgroundlocationUpdatesJob? .cancel()super.onStop()
    }
}
Copy the code

It’s a good solution, but it’s a bit long. If there’s one universal fact about Android developers in the world, it’s that none of us like writing template code. One of the biggest benefits of not having to write template code is that the less code you write, the less chance of errors!

LifecycleOwner.addRepeatingJob

Now that we’re in the same boat and we know what the problem is, it’s time to find a solution. Our solutions need to be: 1. Simple; 2. Friendly or easy to remember and understand; More importantly 3. Safety! Regardless of the implementation details of the data flow, it should be able to handle all use cases.

Before, you should use the API is the lifecycle – runtime – LifecycleOwner provided KTX repository. AddRepeatingJob. Please refer to the following code:

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

        // Collect data from the data stream when the View is in the STARTED state
        // The collection operation is STOPPED when the lifecycle enters the STOPPED state.
        // It automatically starts data collection when the lifecycle is in the STARTED state again.
        lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // New location! Update the map}}}}Copy the code

The addRepeatingJob takes lifecycle. State as an argument and uses it with the incoming code block to automatically create and start a new coroutine when the Lifecycle reaches that State; It also cancellations running coroutines when the life cycle falls below this state.

Because addRepeatingJob automatically cancellations coroutines when they are no longer needed, you can avoid creating template code associated with cancellations. As you might have guessed, to avoid unexpected behavior, this API needs to be called in the onCreate method on the Activity or the onViewCreated method on the Fragment. Here’s an example with fragments:

class LocationFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        // ...
        viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // New location! Update the map}}}}Copy the code

Note: These apis are available inLifecycle: lifecycle - runtime - KTX: 2.4.0 - alpha01Library or a later version of it.

Using repeatOnLifecycle

And save for a more flexible API calls the CoroutineContext purpose, we also provide Lifecycle. The suspended function repeatOnLifecycle for your use. RepeatOnLifecycle suspends the call to its coroutine, re-executes the code block as it enters or exits the target state, and finally resumes calling its coroutine when Lifecycle enters the destroy state.

If you need to perform a configuration task once before the repetitive work, and you want the task to remain suspended until the repetitive work begins, this API can help you do so. The following is an example:

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

        lifecycleScope.launch {
            // A single configuration task
            val expensiveObject = createExpensiveObject()

            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // When the life cycle enters the STARTED state, repetitive tasks are STARTED and stopped when the life cycle enters the STOPED state
                // Operate the expensiveObject
            }

            // When the coroutine resumes, 'lifecycle' is in the DESTROY state. RepeatOnLifecycle in
            // Suspend execution of the coroutine before entering the DESTROYED state}}}Copy the code

Flow.flowWithLifecycle

The flow.flowwithLifecycle operator can also be used when you need to collect only one data Flow. Also within this API using suspend Lifecycle. RepeatOnLifecycle function realization, and will enter and leave the life cycle of the target state sent items and to eliminate internal producer.

class LocationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) locationProvider.locationFlow() .flowWithLifecycle(this, Lifecycle.state.started).oneach {// New location! Update map}.launchin (lifecycleScope)}}Copy the code

Note:Flow.flowWithLifecycleThe API is named afterFlow.flowOn(CoroutineContext)As it will modify the collection of upstream data streams without affecting downstream data flowsCoroutineContext. withflowOnSimilarly,Flow.flowWithLifecycleBuffers have also been added to prevent consumers from failing to keep up with producers. This feature stems from the use in its implementationcallbackFlow.

Configuring internal Producers

Even if you use these apis, be careful of heat flows that can waste resources, even if they are not collected! While there are some appropriate use cases for these heat flows, pay attention and document them as necessary. On the other hand, in some cases, even if it can be a waste of resources, keeping an internal data flow producer active in the background can benefit use cases where you need to refresh the available data immediately, rather than fetching and temporarily displaying stale data. You can determine whether the producer needs to be always active based on the use case.

You can control them using the subscriptionCount field exposed in both the MutableStateFlow and MutableSharedFlow apis, and when the field value is 0, the internal producer stops. By default, as long as the object holding an instance of the data flow is in memory, they remain active as a producer. There are also appropriate use cases for these apis, such as using StateFlow to expose UiState from the ViewModel to the UI. This is appropriate because it means that the ViewModel always needs to provide the View with the latest UI state.

Similarly, the flow. stateIn and flow. shareIn operators can be configured for this type of operation using a sharestart policy. Whilesubscribe () will stop the internal producer when there is no active subscriber! Accordingly, whether the data streams are active or Lazily, their internal producers remain active as long as the CoroutineScope they use is active.

Note: The apis described in this article work well as the default way to collect data flows from the UI, and should be used regardless of how the data flows are implemented. These apis do what they are meant to do: stop collecting the flow of data from the UI when it is not visible on the screen. Whether a data flow should always be active depends on its implementation.

Collect data streams securely in Jetpack Compose

The flow. collectAsState function can collect data flows from the Composable in Compose and can represent the value as State so that it can update the Compose UI. Even though Compose does not recompose the UI while the host Activity or Fragment is in the background, the data stream producer remains active and wastes resources. Compose may experience the same problems as the View system.

In Compose, the flow. flowWithLifecycle operator can be used, as shown in the following example:

@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {

    val lifecycleOwner = LocalLifecycleOwner.current
    val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
        locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
    }

    val location by locationFlowLifecycleAware.collectAsState()
    
    // The current position, which can be used to do some operations
}
Copy the code

Note that you need to remember that the lifecycle aware data flow uses locationFlow and lifecycleOwner as keys so that the same data flow is always used unless one of the keys changes.

Compose (Side – effect) is a must in the Side effects of a controlled environment, therefore, use LifecycleOwner. AddRepeatingJob unsafe. Instead, you can use LaunchedEffect to create coroutines that follow the Composable lifecycle. In its code block, if you need to host life cycle in a State to perform a code block, can hang function called Lifecycle. RepeatOnLifecycle.

Contrast LiveData

You might think that these apis behave much like LiveData — and they do! LiveData is aware of Lifecycle and its restart behavior makes it ideal for observing the flow of data from the UI. Similarly LifecycleOwner addRepeatingJob, suspend Lifecycle. RepeatOnLifecycle and Flow. The flowWithLifecycle etc API as well.

Using these apis is a natural alternative to collecting data streams from LiveData in pure Kotlin applications. If you use these apis to collect data streams, switching to LiveData (as opposed to using coroutines and flows) doesn’t bring any additional benefits. And since Flow can collect data from any Dispatcher, it can also pass through itsThe operatorGet more functionality, so Flow is more flexible. In contrast, LiveData has a limited number of operators available, and it always observes data from the UI thread.

Data binding support for StateFlow

On the other hand, one of the reasons you might want to use LiveData is that it is supported by data binding. But so does StateFlow! For more information on data binding support for StateFlow, see the official documentation.

In Android development, Please use the LifecycleOwner addRepeatingJob, suspend Lifecycle. RepeatOnLifecycle or Flow. FlowWithLifecycle safely collected data stream from the UI layer.