头图

The story behind designing the repeatOnLifecycle API

Android开发者
中文

In this article you will learn the design decisions behind Lifecycle.repeatOnLifecycle API, and Why we will remove previously added to Lifecycle-Runtime-KTX several auxiliary library 2.4.0 version of the first alpha version.

Throughout the full text, you will learn about the dangers of using specific coroutine APIs in certain scenarios, the difficulty of naming the APIs, and the reason why we decided to keep only the underlying suspend API in the function library.

At the same time, you will realize that all API decisions need to be weighed based on the complexity, readability, and error-proneness of the API.

Here is a special to 161316c929f49b Adam Powel , Wojtek Kaliciński , Ian Lake and Yigit Boyar , thank you for your feedback and discussion of API.

Note : If you are looking for the user guide for repeatOnLifecycle Use a safer way to collect Android UI data streams

repeatOnLifecycle

Lifecycle.repeatOnLifecycle API was originally designed to collect data streams more securely from the Android UI layer. Its restartable behavior fully considers the UI life cycle, making it the best default API for processing data only when the UI is visible on the screen.

Note : LifecycleOwner.repeatOnLifecycle is also available. It delegates this function to its Lifecycle object for implementation. With this, all codes that LifecycleOwner

repeatOnLifecycle is a suspend function . For its part, it needs to be executed in a coroutine. repeatOnLifecycle will suspend the called coroutine, and then execute a suspended block that you passed in as a parameter in a new coroutine whenever the life cycle enters (or is higher than) the target state. If the life cycle is lower than the target state, the coroutine started by executing the code block will be cancelled. Finally, the repeatOnLifecycle function will not continue the caller's coroutine until the life cycle is in the DESTROYED state.

Let us understand this API in an example. If you have read my previous articles: A safer way to get data stream from Android UI , then you will not be surprised by the following.

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

        // 由于 repeatOnLifecycle 是一个挂起函数,
        // 因此从 lifecycleScope 中创建新的协程
        lifecycleScope.launch {
            // 直到 lifecycle 进入 DESTROYED 状态前都将当前协程挂起。
            // repeatOnLifecycle 每当生命周期处于 STARTED 或以后的状态时会在新的协程中
            // 启动执行代码块,并在生命周期进入 STOPPED 时取消协程。
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 当生命周期处于 STARTED 时安全地从 locations 中获取数据
                // 当生命周期进入 STOPPED 时停止收集数据
                someLocationProvider.locations.collect {
                    // 新的位置!更新地图(信息)
                }
            }
            // 注意:运行到此处时,生命周期已经处于 DESTROYED 状态!
        }
    }
}
Note : If you repeatOnLifecycle interest implementations can be accessed source link .

a suspend function?

Since the calling context can be retained, so suspend function is executed to restart the behavior of best choice . It follows the Job tree when calling the coroutine. Because repeatOnLifecycle uses suspendCancellableCoroutine in the bottom layer when implementing repeatOnLifecycle, it can work together with the cancellation operation: canceling the coroutine that initiated the call can also cancel the repeatOnLifecycle and the code block that it restarts execution.

In addition, we can add more APIs on top of repeatOnLifecycle, such as Flow.flowWithLifecycle data flow operator. More importantly, it also allows you to create helper functions based on this API according to project requirements. And this is what we tried to do when we added the LifecycleOwner.addRepeatingJob API in lifecycle-runtime-ktx:2.4.0-alpha01, but we removed it in alpha02.

Remove the consideration of

LifecycleOwner.addRepeatingJob API, which was added to the first alpha version of the library and has been removed, was implemented as follows:

public fun LifecycleOwner.addRepeatingJob(
    state: Lifecycle.State,
    coroutineContext: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> Unit
): Job = lifecycleScope.launch(coroutineContext) {
    repeatOnLifecycle(state, block)
}

Its effect is: Given LifecycleOwner , you can execute a suspended code block that restarts whenever the life cycle enters or leaves the target state. This API uses LifecycleOwner of lifecycleScope to trigger a new coroutine and calls repeatOnLifecycle in it.

The previous code uses the addRepeatingJob API to be written as follows:

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

        lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            someLocationProvider.locations.collect {
                // 新的位置!更新地图(信息)
            }
        }
    }
}

At a glance, you may feel that the code is more concise and streamlined. However, if you don’t pay attention, some of these hidden traps may make you shoot yourself in the foot:

  • Although addRepeatingJob accepts a suspend code block, addRepeatingJob itself is not a suspend function for Therefore, you should not call it from within a coroutine!
  • Less code? While writing one less line of code, you use an error-prone API.

The first point seems obvious, but developers often fall into the trap. And ironically, it is actually based on the core point of the coroutine concept: structured concurrent .

addRepeatingJob is not a suspend function, so structured concurrency is not supported by default (note that you can coroutineContext parameter). Since the block is a pending Lambda expression, when you share this API with a coroutine, you may easily write such dangerous code:

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

        val job = lifecycleScope.launch {

            doSomeSuspendInitWork()

            // 危险!此 API 不会保留调用的上下文!
            // 它在父级上下文取消时不会跟着被取消!
            addRepeatingJob(Lifecycle.State.STARTED) {
                someLocationProvider.locations.collect {
                    // 新的位置!更新地图(信息)
                }
            }
        }

        //如果出现错误,取消上面已经启动的协程
        try {
            /* ... */
        } catch(t: Throwable) {
            job.cancel()
        }
    }
}

What's wrong with this code? addRepeatingJob performed the work of the coroutine, nothing prevents me from calling it in the coroutine, right?

Because addRepeatingJob creates a new coroutine and uses lifecycleScope (which is implicitly called in the implementation of the API), this new coroutine neither follows the principle of structured concurrency, nor does it retain the current calling context. Therefore, it will not be cancelled when you call job.cancel() This may cause very hidden errors in your application, and it is very difficult to debug .

repeatOnLifecycle is the big winner

In addRepeatingJob implicit use of CoroutineScope is the reason to make this API in some scenarios unsafe. It is a hidden trap that you need to pay special attention to when writing correct code. This is exactly what we argue about whether to avoid providing encapsulated interfaces repeatOnLifecycle

The main advantage of using the suspended repeatOnLifecycle API is that it can perform well in accordance with the principle of structured concurrency by default, but addRepeatingJob does not. It can also help you consider clearly in which scope you want the repeated code to execute. This API is clear at a glance and meets the expectations of developers:

  • Like other suspend functions, it will interrupt the execution of the current coroutine until a specific event occurs. For example, here is to continue execution when the life cycle is destroyed.
  • No surprises! It can work with other coroutine code and will work as you expect.
  • The code above and below the repeatOnLifecycle is more readable and more meaningful 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 lifecycle enters this The execution of this code will be started when the state is in the state."

Flow.flowWithLifecycle

Flow.flowWithLifecycle operator (you can refer to implement ) is built on repeatOnLifecycle , and only when the life cycle is at least minActiveState will the content from the upstream data stream be sent out.

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

        lifecycleScope.launch {
            someLocationProvider.locations
                .flowWithLifecycle(lifecycle, STARTED)
                .collect {
                    // 新的位置!更新地图(信息)
                }
        }
    }
}

Even though this API has some small pitfalls to be aware of, we still keep it because it is a practical Flow operator. For example, it can easy to use Jetpack Compose in . Even if you can produceState and repeatOnLifecycle API in Jetpack Compose, we still keep this API in the library to provide an easier-to-use method.

As explained in the KDoc of the code implementation, this little trap refers to the order in which you add the flowWithLifecycle operators. When the life cycle is lower than minActiveState , all operators applied before the flowWithLifecycle However, the operators applied afterwards will not be cancelled even if no data is sent.

If you are still curious, the name of this API is derived from the Flow.flowOn(CoroutineContext) operator, because Flow.flowWithLifecycle will collect data from the upstream data stream by changing CoroutineContext , but it will not affect the downstream data stream.

we add additional APIs?

Considering that we already have Lifecycle.repeatOnLifecycle , LifecycleOwner.repeatOnLifecycle and Flow.flowWithLifecycle APIs, should we add additional APIs?

The new API may introduce as much confusion as it solves the problems at the beginning of the design. There are many ways to support different use cases, and which one is a shortcut depends largely on the context code. The methods that can be used in your project may no longer be applicable in other projects.

That's why we do not want to provide for all possible scenarios reasons API, the more available API, the more confusion for developers, do not know what should be what scene use what API . So we decided to keep only the lowest level API. Sometimes less is more.

Naming is both important and difficult

We need to focus not only on what use cases need to be supported, but also how to name these APIs! The name of the API should be the same as the developers expected, and follow the naming convention of Kotlin coroutines. for example:

  • If this API implicitly uses a CoroutineScope (such as addRepeatingJob used in lifecycleScope ), it must reflect this scope in the name to avoid misuse! In this way, launch should exist in the API name.
  • collect is a suspend function. If an API is not a suspend function, it should not have the word collect.
Note : Jetpack Compose the collectAsState .collectAsState (kotlin.coroutines.CoroutineContext)) API is a special case, and we support it so named. It will not be confused with the suspend function, because there is no such suspend function @Composable

In fact, the LifecycleOwner.addRepeatingJob API is difficult to determine, because it uses lifecycleScope create a new coroutine, so it should be launch as the prefix. However, we want to show that it has nothing to do with the underlying implementation of the coroutine, and because it adds a new life cycle observer, its naming is also consistent with other LifecycleOwner APIs.

To some extent, its naming is also affected by the existing LifecycleCoroutineScope.launchWhenX suspension API. Because launchWhenStarted and repeatOnLifecycle(STARTED) provide completely different functions ( launchWhenStarted will interrupt the execution of the repeatOnLifecycle , and 061316c929fe00 cancels and restarts the new coroutine), if their names are similar (for example, use launchWhenever as the name of the new API), then develop The readers may be confused, or even misuse the two APIs due to negligence.

One line of code to collect data stream

LiveData's observe function can be aware of the life cycle, and will only process the sent data after the life cycle has been started at least. If you are about to from LiveData to Kotlin data stream , then you may want a good way to replace it with one line! You can remove the boilerplate code, and the migration is straightforward.

Similarly, you can do it like Ian Lake when using the repeatOnLifecycle API for the first time. He created a convenient wrapper function called collectIn, such as the following code (if you want to conform to the previous naming convention, I will rename it to launchAndCollectIn):

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)
            }
        }
    }

So, you can call it like this in the UI code:

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

        someLocationProvider.locations.launchAndCollectIn(this, STARTED) {
            // 新的位置!更新地图(信息)
        }
    }
}

Although this wrapper function looks very concise and straightforward as in the example, it also has the same LifecycleOwner.addRepeatingJob API above: it does not matter the scope of the call, and it is potentially dangerous when used inside other coroutines. Furthermore, the original name is very misleading: collectIn is not a suspend function! As mentioned earlier, the developer hopes that the function with collect in the name can hang. Perhaps a better name for this wrapper function is Flow.launchAndCollectIn, so that it can avoid misuse.

Wrap function in

repeatOnLifecycle API in Fragment, it must be viewLifecycleOwner together with 061316c929ff9a. In the open source Google I/O application, the development team decided iosched project to avoid misuse of this API in Fragment. It is called: Fragment.launchAndRepeatWithViewLifecycle .

Note that : Its implementation is very close to addRepeatingJob And when this API, is still used in libraries alpha01 version, alpha02 added in repeatOnLifecycle API grammar checker is not yet available.

Do you need wrapper functions?

If you need to repeatOnLifecycle API to cover more common application scenarios in your application, please ask yourself if you really need it or why you need it. If you are determined to continue doing this, I suggest you choose a straightforward API name to clearly explain the role of this wrapper, so as to avoid misuse. In addition, it is recommended that you clearly mark the document so that when newcomers join, you can fully understand the correct way to use it.

I hope that the description in this article can help you understand our internal repeatOnLifecycle , as well as more auxiliary methods that may be added in the future.

Welcome to click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!

阅读 1.6k

Android_开发者
Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。...

331 声望
1.4k 粉丝
0 条评论

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。...

331 声望
1.4k 粉丝
文章目录
宣传栏