Through this article you will learn to Lifecycle. The design decisions behind repeatOnLifecycle API, and why we will remove the previous added to the Lifecycle – runtime – KTX library version 2.4.0 first alpha version of a few auxiliary function.
Throughout, you’ll learn how dangerous it is to use a particular coroutine API in some scenarios, how difficult it is to name the API, and why we decided to keep only the underlying suspended API in the library.
At the same time, you realize that all API decisions need to be weighed against API complexity, readability, and error susceptibility.
A special thanks goes out to Adam Powel, Wojtek Kaliciński, Ian Lake and Yigit Boyar for your feedback and discussion of the API.
Note: If you are looking for instructions for repeatOnLifecycle, see: Collecting Android UI Data Streams in a More secure way.
repeatOnLifecycle
Lifecycle. RepeatOnLifecycle API is the earliest in order to realize the collecting data stream from the Android UI layer more safely. Its returnable behavior takes the UI lifecycle into account, making it the best default API for handling data only when the UI is visible on the screen.
Note: LifecycleOwner repeatOnLifecycle is also available. It delegates this functionality to its Lifecycle object. Thus, all code that is already in the LifecycleOwner scope can omit the explicit sink.
RepeatOnLifecycle is a suspend function. As such, it needs to be executed in coroutines. RepeatOnLifecycle suspends the called coroutine and then executes a pending block that you pass in as a parameter in a new coroutine each time the life cycle enters (or exceeds) the target state. If the life cycle falls below the target state, the coroutine started by executing the code block is cancelled. Finally, repeatOnLifecycle does not continue the caller’s coroutine until the lifecycle is in a DESTROYED state.
Let’s look at this API in an example. If you’ve read my previous article: A More secure way to stream data from the Android UI, you won’t be surprised.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
// Since repeatOnLifecycle is a suspend function,
// So create a new coroutine from lifecycleScope
lifecycleScope.launch {
Lifecycle will suspend the current coroutine until Lifecycle enters the DESTROYED state.
RepeatOnLifecycle is in the new coroutine whenever the life cycle is in the STARTED or later state
// Start executing the code block and remove the coroutine when the life cycle goes to STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Securely retrieve data from Locations while the lifecycle is STARTED
// Data collection stops when the lifecycle goes to STOPPED
someLocationProvider.locations.collect {
// New location! Update maps (information)}}// Note: The lifecycle is in the DESTROYED state!}}}Copy the code
Note: If you are interested in how repeatOnLifecycle is implemented, you can access the source code link.
Why is it a suspend function?
Because the calling context can be preserved, the suspending function is the best choice for performing the restart behavior. It follows the Job tree when it calls the coroutine. Because repeatOnLifecycle implementation used in the underlying suspendCancellableCoroutine, it can be eliminated and operate together: cancel a call of synchronization can also cancel repeatOnLifecycle and resumption of the execution of the code block.
In addition, we can add more APIS on repeatOnLifecycle, such as the flow. flowWithLifecycle data Flow operator. More importantly, it also allows you to create helper functions based on this API as required by your project. That is what we in the lifecycle – runtime – KTX: 2.4.0 – adding LifecycleOwner alpha01. AddRepeatingJob API when trying to do, but in alpha02 we removed it.
Removed addRepeatingJob API considerations
In the library are added in the first alpha version has removed LifecycleOwner. AddRepeatingJob API, earlier is this:
public fun LifecycleOwner.addRepeatingJob(
state: Lifecycle.State,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope. () - >Unit
): Job = lifecycleScope.launch(coroutineContext) {
repeatOnLifecycle(state, block)
}
Copy the code
What it does is: Given the LifecycleOwner, you can execute a pending block of code that restarts every time the lifecycle enters or leaves the target state. This API uses LifecycleOwner’s lifecycleScope to trigger a new coroutine in which repeatOnLifecycle is called.
The previous code is written using the addRepeatingJob API as follows:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
someLocationProvider.locations.collect {
// New location! Update maps (information)}}}}Copy the code
At first glance, you might think the code is cleaner and leaner. However, if you’re not careful, some of these hidden traps can shoot you in the foot:
-
Although the addRepeatingJob accepts a pending block of code, the addRepeatingJob itself is not a pending function. Therefore, you should not call it from within a coroutine!
-
Less code? You’re using an error-prone API while saving a line of code.
The first one may seem obvious, but developers often fall into the trap. And ironically, it’s actually based on the very core of the concept of coroutines: structured concurrency.
AddRepeatingJob is not a suspend function, so structured concurrency is not supported by default (note that you can make it so by using another coroutineContext parameter). Since the block argument is a suspended Lambda expression, when you share this API with coroutines, you could easily write dangerous code like this:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
val job = lifecycleScope.launch {
doSomeSuspendInitWork()
/ / danger! This API does not preserve the context of the call!
// It will not be cancelled when the parent context is cancelled!
addRepeatingJob(Lifecycle.State.STARTED) {
someLocationProvider.locations.collect {
// New location! Update maps (information)}}}// If an error occurs, cancel the coroutine already started above
try {
/ *... * /
} catch(t: Throwable) {
job.cancel()
}
}
}
Copy the code
What’s wrong with this code? The addRepeatingJob does the work of the coroutine, and there’s nothing stopping me from calling it in the coroutine, right?
Because addRepeatingJob creates a new coroutine and uses lifecycleScope (the implicit call used in the IMPLEMENTATION of the API), this new coroutine neither follows the principles of structured concurrency nor retains the current calling context. Therefore, it is not cancelled when you call job.cancel(). This can lead to very subtle bugs in your application that are very difficult to debug.
RepeatOnLifecycle is the big winner
The CoroutineScope used implicitly in addRepeatingJob is what makes this API insecure in some scenarios. It’s a hidden trap that you need to be especially aware of when writing the right code. This was the point of our debate about whether to avoid providing encapsulation interfaces on repeatOnLifecycle in the library.
The main benefit of using the suspended repeatOnLifecycle API is that it performs well with structured concurrency by default, whereas the addRepeatingJob does not. It also helps you figure out in which scope you want the repeated code to be executed. The API is straightforward and meets developers’ expectations:
- Like other suspend functions, it interrupts the execution of the current coroutine until a specific event occurs. For example, the execution continues when the life cycle is destroyed.
- No surprises! It works with other coroutine code and will work as you expect.
- The code around repeatOnLifecycle is more readable and makes more sense for newcomers:“First, I need to start a new coroutine that follows the UI lifecycle. Then I need to call repeatOnLifecycle so that whenever the UI life cycle enters this state it will start executing this code “.
Flow.flowWithLifecycle
The flow. flowWithLifecycle operator (which you can refer to for implementation) is built on repeatOnLifecycle and will only send the content from the upstream data stream if the lifecycle is at least minActiveState.
class LocationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch { someLocationProvider.locations .flowWithlifecycle (lifecycle, STARTED).collect {// New position! Updated map (information)}}}}Copy the code
Even though this API has a few minor pitfalls to watch out for, we left it in because it is a practical Flow operator. For example, it’s easy to use in Jetpack Compose. Even though you can do exactly the same thing with the produceState and repeatOnLifecycle APIS in Jetpack Compose, we kept this API in the library to provide a more easy-to-use approach.
As documented in the KDoc implementation of the code, this little trap refers to the order in which you add the flowWithLifecycle operator. When the life cycle falls below minActiveState, all operators of applications prior to the flowWithLifecycle operator are cancelled. However, operators applied thereafter are not cancelled even if no data is sent.
If you’re still wondering, the name of this API comes from the flow.flowon (CoroutineContext) operator, because flow.flowWithLifecycle collects data from the upstream data stream by changing the CoroutineContext, It does not affect downstream data flow.
Should we add additional apis?
Considering we have Lifecycle. RepeatOnLifecycle, LifecycleOwner. RepeatOnLifecycle and Flow flowWithLifecycle API, should we add the extra API?
The new API may introduce just as much confusion when it comes to solving problems from the beginning of design. There are many ways to support different use cases, and which one is a shortcut depends largely on the context code. What works in your project may not work in other projects.
This is why we don’t want to provide apis for all possible scenarios, because the more apis available, the more confusing it becomes for developers to know which apis should be used for which scenarios. So we decided to keep only the lowest level API. Sometimes, less is more.
Naming is both important and difficult
It’s not just what use cases need to be supported, but how to name these apis! The API name should be the same as expected and follow the Kotlin coroutine naming convention. Here’s an example:
- If the API implicitly uses a
CoroutineScope
(as inaddRepeatingJob
Used inlifecycleScope
Start a new coroutine, which must reflect the scope in its name to avoid misuse! In this way,launch
It should be in the API name. collect
Is a suspend function. If an API is not a hang function, it should not have the word COLLECT.
Note: The collectAsState API for Jetpack Compose is a special example, and we support naming it that way. It is not to be confused with a suspend function because there is no such @composable suspend function in Jetpack Compose.
Actually LifecycleOwner addRepeatingJob API naming it’s hard to say, because it USES lifecycleScope created new coroutines, then it should use the launch as a prefix. However, we want to make it clear that it is independent of the underlying adoption of coroutine implementations, and because it attaches a new lifecycle observer, its name is consistent with the other LifecycleOwner apis.
Its name to a certain extent has also come under the existing LifecycleCoroutineScope. LaunchWhenX hangs the influence of the API. Because launchWhenStarted and repeatOnLifecycle(STARTED) provide completely different functionality (launchWhenStarted interrupts coroutine execution, RepeatOnLifecycle cancelled and restarted the new coroutine), if they were named similarly (such as launchclean as the name of the new API), developers could get confused or inadvertently misuse both apis.
One line of code collects the data stream
LiveData’s Observe function is aware of the life cycle and will only process the sent data after the life cycle has at least started. If you’re migrating from LiveData to a Kotlin data stream, you might want to have a good way to do it with a one-line substitution! You can remove boilerplate code, and migration is straightforward.
Likewise, you can do what Ian Lake did when he first used the repeatOnLifecycle API. He created a handy wrapper function called collectIn, such as the following (I’ll rename it launchAndCollectIn if I want to keep up with previous naming conventions):
inline fun <T> Flow<T>.launchAndCollectIn(
owner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend CoroutineScope. (T) - >Unit
) = owner.lifecycleScope.launch {
owner.repeatOnLifecycle(minActiveState) {
collect {
action(it)
}
}
}
Copy the code
So, you can call it like this in your UI code:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
someLocationProvider.locations.launchAndCollectIn(this, STARTED) {
// New location! Update maps (information)}}}Copy the code
The packaging function, although look very simple and direct as example, but there are also with above LifecycleOwner. AddRepeatingJob API is the same problems: it calls the scope of, and for other coroutines internal potential danger. Further, the original name is very misleading: collectIn is not a hang function! As mentioned earlier, the developers want functions with collect in their name to hang. Perhaps a better name for this wrapper function is flow.launchandCollectin to avoid misuse.
The encapsulation function in IOSCHed
The repeatOnLifecycle API must be used in conjunction with the viewLifecycleOwner in the Fragment. In the open source Google I/O applications, the development team decide in iosched project to create a wrapper to avoid the misuse of the API in fragments, it is called: fragments. LaunchAndRepeatWithViewLifecycle.
Note: Its implementation is very close to the addRepeatingJob API. And when this API is implemented, the alpha01 version of the function library is still used, and the repeatOnLifecycle API syntax checker added in Alpha02 is not available yet.
Do you need to encapsulate functions?
If you need to create wrapper functions on top of the repeatOnLifecycle API to cover more common application scenarios in your application, be sure to ask yourself if you really need it or why you need it. If you are determined to continue doing this, I recommend that you choose a straightforward API name that clearly states what the wrapper does to avoid misuse. In addition, it is recommended that you annotate the document clearly so that when new people join you they fully understand the correct way to use it.
Hopefully, the descriptions in this article will help you understand our internal considerations and decisions for the design and implementation of repeatOnLifecycle, as well as additional auxiliary methods that may be added in the future.
Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!