头图

In Android applications, it is usually necessary to collect Kotlin data stream from the UI layer in order to display data updates on the screen. At the same time, you also want to collect these data streams to avoid unnecessary operations and waste of resources (including CPU and memory), and prevent data leakage when View enters the background.

This article will take you to learn how to use the LifecycleOwner.addRepeatingJob , Lifecycle.repeatOnLifecycle and Flow.flowWithLifecycle APIs to avoid waste of resources; at the same time, it will also introduce why these APIs are suitable as the default choice when collecting data streams at the UI layer.

Resource waste

Regardless of the specific implementation of the data stream producer, recommends expose the Flow<T> API from the lower level of the application. However, you should also ensure the security of data stream collection operations.

Using some of the existing API (eg CoroutineScope.launch , Flow <T> .launchIn or LifecycleCoroutineScope.launchWhenX ) collected based Channel operator or with the use of a buffer (e.g. Buffer , conflate , flowOn or shareIn ) The cold flow data is unsafe , unless you manually cancel the job that started the coroutine when the Activity enters the background. These APIs will keep them active when internal producers send items to the buffer in the background, and this wastes resources.

Note: cold flow is a data stream type, this data stream will execute the code block of the producer on demand when a new subscriber collects data.

For example, in the following example, callbackFlow send location update data flow:

// 基于 Channel 实现的冷流,可以发送位置的更新
fun FusedLocationProviderClient.locationFlow() = 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) // 在出现异常时关闭 Flow
        }
    // 在 Flow 收集结束时进行清理操作 
    awaitClose {
        removeLocationUpdates(callback)
    }
}
Note: callbackFlow internally uses channel achieve, its concept is queue ), and the default capacity is 64.

Using any of the aforementioned APIs to collect this data stream from the UI layer will cause it to continuously send location information, even if the view no longer displays data, it will not stop! Examples are as follows:

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

        // 最早在 View  处于 STARTED 状态时从数据流收集数据,并在
        // 生命周期进入 STOPPED 状态时 SUSPENDS(挂起)收集操作。
        // 在 View 转为 DESTROYED 状态时取消数据流的收集操作。
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // 新的位置!更新地图
            } 
        }
        // 同样的问题也存在于:
        // - lifecycleScope.launch { /* 在这里从 locationFlow() 收集数据 */ }
        // - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
    }
}

lifecycleScope.launchWhenStarted suspended the execution of the coroutine. Although the new location information has not been processed, the callbackFlow producer will continue to send location information. Using lifecycleScope.launch or launchIn API will be more dangerous, because the view will continue to consume location information, even if it is in the background, it will not stop! This situation may cause your app to crash.

In order to solve the problems caused by these APIs, you need to manually cancel the collection operation when the view callbackFlow to the background to cancel 060f4eabc6f29f and prevent the location provider from continuously sending items and wasting resources. For example, you can do it like the following example:

class LocationActivity : AppCompatActivity() {

    // 位置的协程监听器
    private var locationUpdatesJob: Job? = null

    override fun onStart() {
        super.onStart()
        locationUpdatesJob = lifecycleScope.launch {
            locationProvider.locationFlow().collect {
                // 新的位置!更新地图。
            } 
        }
    }

    override fun onStop() {
       // 在视图进入后台时停止收集数据
        locationUpdatesJob?.cancel()
        super.onStop()
    }
}

This is a good solution, but the catch is that it is somewhat lengthy. If there is a general fact about Android developers in this world, it must be that we don't like writing template code. One of the biggest advantages of not having to write template code is that the less code you write, the lower the probability of error!

LifecycleOwner.addRepeatingJob

Now that we are in the same situation and know where the problem is, it is time to find a solution. Our solution needs: 1. Simple; 2. Friendly or easy to remember and understand; More importantly, 3. Safe! Regardless of the implementation details of the data flow, it should be able to handle all use cases.

Without further ado - API you should use a Lifecycle-Runtime-KTX library provided LifecycleOwner.addRepeatingJob . Please refer to the code below:

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

        // 最早在 View  处于 STARTED 状态时从数据流收集数据,并在
        // 生命周期进入 STOPPED 状态时 STOPPED(停止)收集操作。
        // 它会在生命周期再次进入 STARTED 状态时自动开始进行数据收集操作。
        lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // 新的位置!更新地图
            } 
        }
    }
}

addRepeatingJob receives Lifecycle.State as a parameter, and uses it together with the incoming code block. When the life cycle reaches this state, automatically creates and starts a new coroutine ; at the same time, when the life cycle falls below this state cancels the running coroutine .

Since addRepeatingJob will automatically cancel the coroutine when it is no longer needed, it can avoid the generation of template code related to the cancellation operation. As you might have guessed, in order to avoid unexpected behavior, this API needs to be called in the onCreate of the Activity or the onViewCreated method of the Fragment. The following is an example used with Fragment:

class LocationFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ...
        viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // 新的位置!更新地图
            } 
        }
    }
}
Note: These APIs lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 library or its newer version.

uses repeatOnLifecycle

For the purpose of providing a more flexible API and saving the calling CoroutineContext , we also provide the suspension function Lifecycle.repeatOnLifecycle for your use. repeatOnLifecycle will suspend the coroutine that called it, and will re-execute the code block when entering and leaving the target state, and finally resume calling its coroutine when Lifecycle

If you need to perform a configuration task before repetitive work, and you want the task to remain suspended before the repetitive work starts, this API can help you achieve such an operation. Examples are as follows:

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

        lifecycleScope.launch {
            // 单次配置任务
            val expensiveObject = createExpensiveObject()

            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
                // 对 expensiveObject 进行操作
            }

            // 当协程恢复时,`lifecycle` 处于 DESTROY 状态。repeatOnLifecycle 会在
            // 进入 DESTROYED 状态前挂起协程的执行
        }
    }
}

Flow.flowWithLifecycle

When you only need to collect one data stream, you can also use the Flow.flowWithLifecycle operator. The internal part of this API is also suspend Lifecycle.repeatOnLifecycle function, and will send items and cancel internal producers when the life cycle enters and leaves the target state.

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

        locationProvider.locationFlow()
            .flowWithLifecycle(this, Lifecycle.State.STARTED)
            .onEach {
                // 新的位置!更新地图
            }
            .launchIn(lifecycleScope) 
    }
}
Note: The naming of the Flow.flowWithLifecycle Flow.flowOn(CoroutineContext) CoroutineContext collects the upstream data stream without affecting the downstream data stream. Another point similar to flowOn Flow.flowWithLifecycle has also added a buffer to prevent consumers from being unable to keep up with producers. This feature stems from the callbackFlow used in its implementation.

configure internal producer

Even if you use these APIs, beware of the heat flow that may waste resources, even if they are not collected! Although there are some suitable use cases for these heat flows, you still need to pay more attention and record when necessary. On the other hand, in some cases, even if it may cause a waste of resources, keeping the internal data stream producer in the background active will be beneficial to certain use cases, such as: You need to refresh the available data immediately, instead of acquiring and temporarily Show stale data. You can decide whether the producer needs to be always active according to the use case .

You can use subscriptionCount field exposed in the two APIs MutableStateFlow and MutableSharedFlow to control them. When the field value is 0, the internal producer will stop. By default, as long as the objects holding the data stream instance are still in memory, they will keep the producer active. There are also some suitable use cases for these APIs, such as using StateFlow to UiState from the ViewModel to the UI. This is appropriate because it means that the ViewModel always needs to provide the latest UI state to the View.

shared start strategy can also be used for this type of operation to configure Flow.stateIn and Flow.shareIn operators. WhileSubscribed() will stop the internal producer when there are no active subscribers! Correspondingly, no matter the data stream is (active) or Lazily (inert), as long as the CoroutineScope they use is still active, their internal producers will remain active.

Note: The APIs described in this article can be used as the default way to collect data streams from the UI, and they should be used regardless of how the data stream is implemented. These APIs do what they do: stop collecting its data stream when the UI is not visible on the screen. As for whether the data stream should always be active, it depends on its implementation.

Collect data streams safely in Jetpack Compose

Flow.collectAsState function can collect the data stream from composable in Compose, and can express the value as State<T> so as to be able to update the Compose UI. Even if Compose does not reorganize the UI when the host Activity or Fragment is in the background, the data stream producer will still remain active and cause a waste of resources. Compose may experience the same problems as the View system.

When collecting data streams in Compose, you can use the Flow.flowWithLifecycle operator, an example is as follows:

@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()
    
    // 当前位置,可以拿它做一些操作
}

Note that you need to remember lifecycle-aware data stream using locationFlow and lifecycleOwner as the key to always use the same data stream, unless one of the key change.

The side-effect of Compose is that it must be in a controlled environment . Therefore, LifecycleOwner.addRepeatingJob not safe to use 060f4eabc6f868. As an alternative, you can use LaunchedEffect to create a coroutine that follows the composable life cycle. In its code block, if you need to re-execute a code block when the host life cycle is in a certain State, you can call the suspension function Lifecycle.repeatOnLifecycle .

vs. LiveData

You might think that the performance of these APIs is LiveData -it is! LiveData can perceive Lifecycle, and its restart behavior makes it very suitable for observing the data flow from the UI. The same is true for APIs such as LifecycleOwner.addRepeatingJob , suspend Lifecycle.repeatOnLifecycle Flow.flowWithLifecycle

In pure Kotlin applications, using these APIs can naturally replace LiveData to collect data streams. If you use these APIs to collect data streams, changing to LiveData (as opposed to using coroutines and Flow) will not bring any additional benefits. And because Flow can collect data from any Dispatcher, and can also operator , Flow is also more flexible. In contrast, LiveData has limited available operators, and it always observes data from the UI thread.

Data binding support for

On the other hand, LiveData may be that it is supported by data binding. But StateFlow is the same! For more information about data binding support for StateFlow, please refer to official document .

In Android development, please use LifecycleOwner.addRepeatingJob , suspend Lifecycle.repeatOnLifecycle or Flow.flowWithLifecycle to securely collect data streams from the UI layer.


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。