乐趣区

关于android:设计-repeatOnLifecycle-API-背后的故事

通过本文您将会理解到 Lifecycle.repeatOnLifecycle API 背地的设计决策,以及 为什么 咱们会移除此前增加到 lifecycle-runtime-ktx 库 2.4.0 版本首个 alpha 版中的几个辅助函数。

纵观全文,您将理解到在某些场景中应用特定协程 API 的危险水平、为 API 命名的艰难水平以及咱们决定在函数库中只保留底层挂起 API 的起因。

同时,您会意识到所有的 API 决策都须要依据 API 的复杂度、可读性和容易出错水平进行衡量。

这里特地向 Adam Powel、Wojtek Kaliciński、Ian Lake 和 Yigit Boyar 致谢,感激大家对 API 的反馈和探讨。

留神 : 如果您在查找 repeatOnLifecycle 的使用指南,请查阅: 应用更为平安的形式收集 Android UI 数据流

repeatOnLifecycle

Lifecycle.repeatOnLifecycle API 最早是为了实现从 Android UI 层更平安地收集数据流而设计的。它的可重启行为充分考虑了 UI 生命周期,使其成为仅当 UI 在屏幕上处于可见时解决数据的最佳默认 API。

留神 : LifecycleOwner.repeatOnLifecycle 也是可用的。它将此性能委托给其 Lifecycle 对象来实现。借此,所有曾经属于 LifecycleOwner 作用域的代码都能够省略显式的接收器。

repeatOnLifecycle 是一个挂起函数。就其自身而言,它须要在协程中执行。repeatOnLifecycle 会将调用的协程挂起,而后每当生命周期进入 (或高于) 指标状态时在一个新的协程中执行您作为参数传入的一个挂起块。如果生命周期低于指标状态,因执行该代码块而启动的协程就会被勾销。最初,repeatOnLifecycle 函数直到在生命周期处于 DESTROYED 状态时才会持续调用者的协程。

让咱们在实例中理解这个 API 吧。如果您曾经浏览过我此前的文章: 一种更平安的从 Android UI 当中获取数据流的形式,那您将不会对以下内容感到离奇。

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 状态!}
    }
}

留神 : 如果您对 repeatOnLifecycle 的实现形式感兴趣,能够拜访 源代码链接

为什么是一个挂起函数?

因为能够保留调用上下文,所以 挂起函数 是执行重启行为的 最佳抉择。它在调用协程时遵循 Job 树。因为 repeatOnLifecycle 实现时在底层应用了 suspendCancellableCoroutine,它能够与勾销操作独特运作: 勾销发动调用的协程同时也能够勾销 repeatOnLifecycle 和它重启执行的代码块。

此外,咱们能够在 repeatOnLifecycle 之上增加更多的 API,比方 Flow.flowWithLifecycle 数据流操作符。更重要的是,它还容许您依照我的项目需要在此 API 的根底上创立辅助函数。而这也是咱们在 lifecycle-runtime-ktx:2.4.0-alpha01 中退出 LifecycleOwner.addRepeatingJob API 时尝试做的事,不过在 alpha02 中咱们将它移除了。

移除 addRepeatingJob API 的考量

在函数库首个 alpha 版本中退出而目前曾经移除的 LifecycleOwner.addRepeatingJob API,早前是这样实现的:

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

其作用是: 给定了 LifecycleOwner,您能够执行一个每当生命周期进入或来到指标状态时都会重启的挂起代码块。此 API 应用了 LifecycleOwnerlifecycleScope 来触发一个新的协程,并在其中调用 repeatOnLifecycle。

后面的代码应用 addRepeatingJob API 的写法如下:

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

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

一眼望去,您可能感觉代码更加简洁、精简了。然而,如果您不加以留神,其中的一些暗藏陷阱可能会让您搬起石头砸了本人的脚:

  • 尽管 addRepeatingJob 承受一个挂起代码块,addRepeatingJob 自身却 不是 一个挂起函数。因而,您不应该在协程内调用它!
  • 更少的代码?您在少写一行代码的同时,却用了一个容易出错的 API。

第一点看起来比拟不言而喻,但开发者们往往会掉入陷阱。而且讥刺的是,实际上它就是基于协程概念中最外围的一点: 结构化并发

addRepeatingJob 不是一个挂起函数,因而默认也就不反对结构化并发 (须要留神的是您能够通过应用另一个 coroutineContext 参数来让其反对)。因为 block 参数是一个挂起的 Lambda 表达式,当您将这个 API 与协程共用时,您可能很容易地写出这样的危险代码:

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

这段代码出了什么问题?addRepeatingJob 执行了协程的工作,没有什么会阻止我在协程当中调用它,对吗?

因为 addRepeatingJob 创立了一个新的协程,并应用了 lifecycleScope (隐式调用于该 API 的实现中),这个新的协程既不会遵循结构化并发准则,也不会保留以后的调用上下文。因而,当您调用 job.cancel() 的时候它也不会被勾销。这可能会导致您利用中存在十分荫蔽的谬误,并且十分不好调试

repeatOnLifecycle 才是大赢家

addRepeatingJob 隐式应用的 CoroutineScope 正是让这个 API 在某些场景下不平安的起因。它是您在编写正确的代码时须要特地留神的暗藏陷阱。这一点正是咱们对于是否要在函数库中防止在 repeatOnLifecycle 之上提供封装接口的争执所在。

应用挂起的 repeatOnLifecycle API 的次要益处是它默认能很好地依照结构化并发的准则执行,然而 addRepeatingJob 却不会这样。它也能够帮忙您思考分明您想要这个反复执行的代码在哪一个作用域执行。此 API 高深莫测,并且合乎开发者们的冀望:

  • 同其余的挂起函数一样,它会将以后协程的执行中断,直到特定事件产生。比方这里是当生命周期被销毁时继续执行。
  • 没有意外惊吓!它能够与其余协程代码独特作用,并且会依照您的预期工作。
  • 在 repeatOnLifecycle 高低的代码可读性高,并且对于新人来说更有意义: “ 首先,我须要启动一个追随 UI 生命周期的新协程。而后,我须要调用 repeatOnLifecycle 使得每当 UI 生命周期进入这个状态时会启动执行这段代码 ”。

Flow.flowWithLifecycle

Flow.flowWithLifecycle 操作符 (您能够参考 具体实现) 是构建在 repeatOnLifecycle 之上的,并且仅当生命周期至多处于 minActiveState 时才会将来自上游数据流的内容发送进来。

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

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

即便这个 API 也有一些小陷阱须要当心,咱们依然将其保留了,因为它是一个实用的 Flow 操作符。举个例子,它能够 在 Jetpack Compose 中轻松应用。即使您在 Jetpack Compose 中可能通过 produceState 和 repeatOnLifecycle API 实现完全相同的性能,咱们依然将这个 API 保留在库中,以提供一种更加易用的办法。

如代码实现的 KDoc 中用文档阐明的那样,这个小陷阱指的是您增加 flowWithLifecycle 操作符的程序是有考究的。当生命周期低于 minActiveState 时,在 flowWithLifecycle 操作符之前的利用的所有操作符都会被勾销。然而,在其后利用的操作符即便没有发送任何数据也不会被勾销。

如果您依然感到好奇,此 API 的名字源于 Flow.flowOn(CoroutineContext) 操作符,因为 Flow.flowWithLifecycle 会通过扭转 CoroutineContext 来收集上游数据流的数据,却不会影响到上游数据流。

咱们该不该增加额定的 API?

思考到咱们曾经有了 Lifecycle.repeatOnLifecycleLifecycleOwner.repeatOnLifecycleFlow.flowWithLifecycle API 了,咱们该不该再增加额定的 API 呢?

新的 API 在解决设计之初的问题时,还可能会引入同样多的困惑。有许多的形式来反对不同的用例,并且哪一种是捷径很大水平取决于上下文代码。在您的我的项目中能用上的形式,在其余我的项目中可能不再实用。

这就是咱们不想为所有可能的场景提供 API 的起因,越多可用的 API,对于开发者来说就越困惑,不晓得到底应该 何种场景 应用 何种 API。因而咱们决定仅保留最底层的 API。有时候,少即是多。

命名既重要又艰难

咱们要关注的不仅仅是须要反对哪些用例,还有怎么命名这些 API!API 的名字应该与开发者们的预期雷同,并且遵循 Kotlin 协程的命名习惯。举个例子:

  • 如果此 API 隐式应用某个 CoroutineScope (比方在 addRepeatingJob 中用到的 lifecycleScope) 启动的新协程,它必须要在名称上反馈进去这个作用域,以防止误用!这样一来,launch 就应该存在于 API 名字中。
  • collect 是一个挂起函数。如果某个 API 不是挂起函数,就不应该带有 collect 字样。

留神 : Jetpack Compose 的 collectAsState.collectAsState(kotlin.coroutines.CoroutineContext)) API 是一个非凡的例子,咱们反对它这样命名。它不会和挂起函数混同,因为在 Jetpack Compose 当中没有这样的 @Composable 的挂起函数。

其实 LifecycleOwner.addRepeatingJob API 命名很难定夺,因为它应用 lifecycleScope 创立了新的协程,那么它就应该用 launch 作为前缀命名。然而,咱们想要表明它与底层采纳协程实现无关,并且因为它附加上了新的生命周期观察者,其命名也与其余的 LifecycleOwner API 放弃了统一。

其命名在某种程度上也受到了现有的 LifecycleCoroutineScope.launchWhenX 挂起 API 的影响。因为 launchWhenStartedrepeatOnLifecycle(STARTED) 提供了齐全不同的性能 (launchWhenStarted 会中断协程的执行,而 repeatOnLifecycle 勾销和重启了新的协程),如果它们的命名很类似 (比方用 launchWhenever 作为新 API 的名字),那么开发者们可能会感到困惑,甚至是因忽略而张冠李戴误用两个 API。

一行代码收集数据流

LiveData 的 observe 函数能够感知生命周期,并且只会在生命周期至多曾经启动之后才会解决发送的数据。如果您正要 从 LiveData 迁徙到 Kotlin 数据流,那么您可能会想要有一种用一行替换就实现的好方法!您能够移除样板代码,迁徙其实间接明了。

同样地,您能够像 Ian Lake 首次应用 repeatOnLifecycle API 时那样做。他创立了一个不便的封装函数,名字叫作 collectIn,比方上面的代码 (如果要合乎此前的命名习惯,我会将其更名为 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)
            }
        }
    }

于是,您能够在 UI 代码中这样调用它:

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

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

这个封装函数,尽管如同例子里那样看起来十分简洁和间接,但也存在同上文的 LifecycleOwner.addRepeatingJob API 一样的问题: 它不论调用的作用域,并且在用于其余协程外部时有潜在的危险。进一步说,原来的名字非常容易产生误导: collectIn 不是一个挂起函数!如前文提到的那样,开发者心愿名字里带 collect 的函数可能挂起。或者,这个封装函数更好的名字是 Flow.launchAndCollectIn,这样就能防止误用了。

iosched 中的封装函数

在 Fragment 中应用 repeatOnLifecycle API 时必须同 viewLifecycleOwner 一道应用。在开源的 Google I/O 利用中,开发团队决定在 iosched 我的项目中创立一个封装器来防止于 Fragment 中误用此 API,它叫做: Fragment.launchAndRepeatWithViewLifecycle。

留神 : 它的实现与 addRepeatingJob API 十分靠近。并且当这个 API 实现时,应用的依然是函数库的 alpha01 版本, alpha02 中退出的 repeatOnLifecycle API 语法查看器尚不可用。

您须要封装函数吗?

如果您须要在 repeatOnLifecycle API 之上创立封装函数以涵盖您的利用中更常见的利用场景,请肯定问问本人是否真的须要它,或者是为什么须要它。如果您决意要持续这样做,我建议您抉择一个间接明了的 API 名字来分明阐明这个封装器的作用,从而防止误用。另外,建议您分明地进行文档标注,当新人退出时就能齐全明确应用它的正确办法了。

心愿通过本文的形容,能够帮忙您理解咱们外部对设计和实现 repeatOnLifecycle 时的考量和决策,以及将来可能会退出的更多的辅助办法。

欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!

退出移动版