阅读 2986

【译】Google 是怎么设计一个 API 的,了解一下~

原标题: repeatOnLifecycle API design story
原文地址: repeatOnLifecycle API design story
原文作者:Manuel Vivo

前言

众所周知,Google发布一个新的Library都要经历alpha,beta,rc,release等多个版本的迭代
在这个漫长的迭代过程中,通常会有Bug的修复,代码与功能的增删等等,哪些代码应该该增加,哪些代码应该删除,这就是考验设计者的地方

本文主要讲述了Google在设计及迭代repeatOnLifecycle API过程中的设计与决策
repeatOnLifecycle主要用于在UI中收集flow,关于它的用法可见:使用更为安全的方式收集 Android UI 数据流
通过本文你将了解以下内容:

  1. repeatOnLifecycle API背后的设计决策
  2. 为什么alpha版本中添加的addRepeatingJob API会被移除?
  3. 为什么flowWithLifecycle API会被保留?
  4. 为什么API命名是重要且困难的
  5. 为什么只保留库中最基础的几个 API

译文

repeatOnLifecycle介绍

Lifecycle.repeatOnLifecycle API主要是为了在UI层进行更安全的Flow收集
比如lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED),会在onStart时启动协程,在onStop时取消协程
然后在Activity重新回到onStart时重新启动一个协程

这种特性与UI生命周期的可重启性比较契合,让它成为仅当UI可见时才收集flow的完美默认API

  1. repeatOnLifecycle 是一个挂起函数,repeatOnLifecycle 会挂起调用协程
  2. 每次给定的生命周期达到目标状态或更高时,都会启动一个新的协程,运行传入的block
  3. 如果生命周期状态低于目标,则为块启动的协程将被取消。
  4. 最后,在生命周期被销毁之前,repeatOnLifecycle 函数本身不会恢复调用协程。

下面来看一下这个API的示例,关于它的用法更详细的介绍可见:使用更为安全的方式收集 Android UI 数据流

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

        // 从lifecycleScope创建一个新的协程
        // 因为repeatOnLifecycle 是一个挂起函数
        lifecycleScope.launch {
            // 阻塞协程,直到生命周期到达DESTOYED
	    // repeatOnLifecycle会在每次生命周期处于 STARTED 状态(或更高状态)时,启动一个新的协程运行传入的block
	    // 并在 STOPPED 时取消协程。
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 安全的collect flow当生命周期到达STARTED
                // 当生命周期到达STOPPED 时停止collect
                someLocationProvider.locations.collect {
                    // 收集到新的位置,更新UI
                }
            }
            // 注意,当运行到这的时候,说明lifecycle已经是DESTROYED
        }
    }
}
复制代码

如果你对repeatOnLifecycle是怎么实现的感兴趣,可以查看源码:repeatOnLifecycle源码

为什么repeatOnLifecycle是挂起函数?

针对repeatOnLifecycle可重启的特性,挂起函数是最佳选择

  1. 因为它保留了调用的上下文,即CoroutineContext
  2. 同时repeatOnLifecycle内部使用了suspendCancellableCoroutine,因此它支持取消,当取消协程时,repeatOnLifecycle与它的子协程都会被取消

此外,我们可以在repeatOnLifecycle之上扩展更多API,例如Flow.flowWithLifecycle流操作符。
更重要的是,如果您的项目需要,它还允许您在此API基础上扩展封装辅助函数.这就是我们尝试使用LifecycleOwner.addRepeatingJob API做的事情
我们在 lifecycle-runtime-ktx:2.4.0-alpha01 中添加了该 API,但是在alpha02中删除了该API

为什么移除addRepeatingJob API?

LifecycleOwner.addRepeatingJob APIalpha01中添加,但是在alpha02中被移除了
为什么呢?我们先来看看实现

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

可以看出代码很简单,本质上就是对repeatOnLifecycle的封装,传入stateblock就能实现与repeatOnLifecycle同样的效果
之所以引入这个API是为了简化调用方式,一起看下代码:

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

        lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            someLocationProvider.locations.collect {
                //...
            }
        }
    }
}
复制代码

乍一看,您可能认为这段代码更简洁,需要的代码更少。 但是,如果您不密切注意,使用这个API会带来一些隐藏的陷阱

  • 尽管addRepeatingJob需要传入一个挂起的block,但addRepeatingJob不是挂起函数。 因此,你不应该在协程中调用它!!!
  • 再来看收益,您只节省了一行代码,代价是拥有了一个更容易出错的API

第一点可能有些同学会奇怪,为什么不应该在协程中调用非挂起函数?
实际上是因为协程最核心的概念之一:结构化并发

什么是结构化并发?

要了解结构化并发,我们先来看看线程,线程的并发是非结构化的
可以想想这几个问题在线程中要怎么解决:
1.结束一个线程时,怎么同时结束这个线程中创建的子线程?
2.当某个子线程在执行时需要结束兄弟线程要做怎么做?
3.如何等待所有子线程都执行完了再结束父线程?

当然这些问题,都可以通过共享标记位等方式解决,但是这几个问题说明,线程间没有级联关系;
所有线程执行的上下文都是整个进程,多个线程的并发是相对整个进程的,而不是相对某一个父线程。
这就是线程并发的「非结构化」。

但与此同时,业务的并发通常是结构化的
通常,每个并发操作都是在处理一个任务单元,这个任务单元可能属于某个父任务单元,同时它也可能有子任务单元。
而每个任务单元都有自己的生命周期,子任务的生命周期理应继了父任务的生命周期。
这就是业务的「结构化」。

因此协程中引入结构化并发的概念,在结构化并发中,每个并发操作都有自己的作用域,并且:
1.在父作用域内新建作用域都属于它的子作用域;
2.父作用域和子作用域具有级联关系;
3.父作用域的生命周期持续到所有子作用域执行完;
4.当主动结束父作用域时,会级联结束它的各个子作用域。

Kotlin的协程就是结构化的并发,它有 「协程作用域(CoroutineScope)」 的角色。
全局的 GlobalScope 是一个作用域,每个协程自身也是一个作用域。新建的协程对象和父协程保持着级联关系。

关于结构化并发的更多内容可见:什么是 结构化并发 ?

addRepeatingJob的问题

addRepeatingJob不是挂起函数,因此默认情况下不支持结构化并发
由于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 {
                    // 更新ui
                }
            }
        }

        // 如果发生错误,取消上面启动的协程
        try {
            /* ... */
        } catch(t: Throwable) {
            job.cancel()
        }
    }
}
复制代码

这段代码有什么问题? addRepeatingJob 是处理协程相关的东西,没有什么能阻止我在协程中调用它,对吧?

因为addRepeatingJob不是挂起函数,在内部实现中会调用lifecycleScope来启动一个新的协程
因此不会保留调用协程的上下文,也不支持结构化并发,即当调用job.cancel()时不会取消addRepeatingJob中创建的协程
这是非常不符合预期的,也很容易导致难以调试的不可预知的BUG

addRepeatingJob内部隐式调用了CoroutineScope,导致这个API在某些情况下使用是不安全的。
用户如果要正确使用这个API,还需要了解一下额外的知识,这是不可接收的,这也是移除这个API的原因

repeatOnLifecycle的主要好处在于它默认支持结构化并发,它还可以帮助您思考您希望重复工作在哪个生命周期内发生。 API一目了然,符合开发人员的期望

为什么保留Flow.flowWithLifecycle?

Flow.flowWithLifecycle 运算符构建在repeatOnLifecycle之上
并且仅在生命周期至少处于minActiveState时才发出上游流发送的元素,当生命周期低于minActiveState会取消上游流

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

        lifecycleScope.launch {
            someLocationProvider.locations
                .flowWithLifecycle(lifecycle, STARTED)
                .collect {
                    //更新UI
                }
        }
    }
}
复制代码

尽管这个API也有一些隐式的需要注意的问题,但我们决定保留它,因为它作为一个Flow操作符很实用。
例如,它可以轻松地用于Jetpack Compose。尽管您可以通过使用produceStaterepeatOnLifecycle APICompose 中实现相同的功能,我们将此API 保留在库中,作为一个替代方案。

flowWithLifecycle需要注意的问题是,添加flowWithLifecycle运算符的顺序很重要。
当生命周期低于minActiveState时,在flowWithLifecycle运算符之前添加的运算符将被取消。
但是,即使没有发送任何元素,在flowWithLifecycle之后添加的运行符也不会被取消。

因此,这个API命名参照了Flow.flowOn(CoroutineContext)运算符
因为此API更改了用于收集上游流的CoroutineContext,同时使下游不受影响,与flowOn类似。

我们应该添加更多的API吗?

鉴于我们已经拥有了Lifecycle.repeatOnLifecycleLifecycleOwner.repeatOnLifecycleFlow.flowWithLifecycle API
我们还应该添加任何其他API吗?

API可能会带来与它们解决的问题一样多的混乱。支持不同用例的方式有多种,最好的方式取决于你的业务代码是怎样的.对您的项目有效的方法可能对其他人无效。

这就是为什么我们不想为所有可能的情况提供API,可用的 API 越多,开发人员就越不知道何时使用什么。
因此,我们决定只保留最底层的 API。有时,少即是多

API命名是重要且困难的

API命名是重要的,命名应符合开发人员的期望并遵循Kotlin协程的约定。例如:

  • 如果API中隐式地使用CoroutineScope启动新的协程,则必须在名称中反映出来,以避免错误的期望!在这种情况下,launch 应该以某种方式包含在命名中。
  • collect是一个挂起函数。如果API不是挂起函数,则不要在API命名中加上collect

LifecycleOwner.addRepeatingJob API也很难命名。API内部使用CoroutineScope创建新的协程时,看起来它应该以launch 为前缀。
但是,我们想将此API与内部的协程分离开来,同时因为它添加了一个新的Lifecycle Observer,因此命名与其他LifecycleOwner API更加一致。

命名也受到现有的LifecycleCoroutineScope.launchWhenX API的影响。
因为launchWhenStartedrepeatOnLifecycle(STARTED)提供完全不同的功能(launchWhenStarted挂起协程的执行,而repeatOnLifecycle取消并重新启动一个新的协程)
如果新API的名称相似(例如,使用launchWhenever作为重新启动的API) ,开发人员可能会感到困惑,甚至在没有注意到的情况下混淆使用它们。

一行代码实现flow收集

目前的收集方法还是有些烦琐的,如果你是从LiveData迁移到Flow,你可能会觉得要是可以一行代码实现collect就好了
这样你就可以删除模板代码,并且使迁移变得简单

因此,您可以像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) {
            // 更新UI
        }
    }
}
复制代码

这个包装器在这个例子中看起来很好很简单,但遇到了我们之前提到的LifecycleOwner.addRepeatingJob相同的问题。它不支持结构化并发,在其他协程中使用可能很危险。
此外,原来的名称确实具有误导性:collectIn不是挂起函数!如前所述,开发人员预期collect会挂起。
也许,这个包装器的更好名称可能是Flow.launchAndCollectIn 以防止不良用法。

你需要一个API包装器吗?

如果您需要在repeatOnLifecycle API之上创建包装器来方便开发,请问问自己是否真的需要它,以及为什么需要它。
如果你确信需要,我建议你选择一个非常明确的API命名来清楚地定义包装器的行为,以避免误用。此外,要非常清楚地记录它,以便新手可以完全理解使用它的含义。

阅读源码的小技巧

当我们查看源码的时候,已经是API完成的状态了,其实我们可以查看API迭代开发过程的源码,看看在迭代过程中都发生了什么,这些都是开源的
比如repeatOnLifecycle API是在lifecycle-runtime-ktx库中,我们可以看下它的git log,lifecycle-runtime-ktx库git历史如下图所示:

从上面我们可以看出repeatOnLifecycle功能是怎样一步一步被引入并修改的

我们甚至可以看看代码的review过程,看看reviewr有提出什么意见,比如addRepeatingJob Review过程如下图所示:

通过查看功能引入的git log,我们可以学习Google是怎么一步一步引入一个新功能并迭代的,相信对我们学习或者开发API都有所帮助

总结

本文主要讲解了repeatOnLifecycle API在开发与迭代过程中的一些设计与思考,总结如下:

  1. API决策通常需要在复杂性、可读性以及API容易出错的程度方面进行一些权衡思考
  2. 之所以移除addRepeatingJob API是因为它不支持结构化并发,在协程中使用可能会带来不可预期的错误
  3. API命名是重要且困难的,命名应符合开发人员的期望并遵循原有API的规范
  4. 我们不可能为所有情况提供API, 可用的API 越多,开发人员反倒不知道何时使用什么,因此我们只需保留最底层的API,有时少就是多
  5. 我们可以通过查看新API引入的git log,来学习理解新API的引入与迭代过程

更文不易,如果本文对你有所帮助,欢迎点赞收藏~

文章分类
Android
文章标签