本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Molecule
通过Jetpack Compose构建StateFlow
或者Flow
流.
fun CoroutineScope.launchCounter(): StateFlow<Int> = launchMolecule(clock = ContextClock) {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1_000)
count++
}
}
count
}
简介
Jetpack Compose UI 使得用逻辑构建声明式UI更容易.
val userFlow = db.userObservable()
val balanceFlow = db.balanceObservable()
@Composable
fun Profile() {
val user by userFlow.subscribeAsState(null)
val balance by balanceFlow.subscribeAsState(0L)
if (user == null) {
Text("Loading…")
} else {
Text("${user.name} - $balance")
}
}
不幸的是, 我们正在混合业务逻辑和展示逻辑, 这使得测试比它们分开时更加难以进行. 展示层也直接交互于存储层, 这创建了不受欢迎的耦合. 除此之外, 如果我们想用相同的逻辑(在其它平台是可能的)增强不同的展示, 对不起, 办不到.
业务逻辑解析成类似presenter的对象修复了这3件事.
在Cash应用中, presenter对象像过去一样通过Kotlin协程的Flow
或者RxJava的Observable
, 暴露了展示模型的单一流.
sealed interface ProfileModel {
object Loading : ProfileModel
data class Data(
val name: String,
val balance: Long,
) : ProfileModel
}
class ProfilePresenter(
private val db: Db,
) {
fun transform(events: Flow<Nothing>): Flow<ProfileModel> {
return combine(
db.users().onStart { emit(null) },
db.balances().onStart { emit(0L) },
) { user, balance ->
if (user == null) {
Loading
} else {
Data(user.name, balance)
}
}
}
}
这些代码是可以的, 但组合响应流的仪式将非线性扩展. 这意味着越来越多的使用数据源和越来越复杂的逻辑, 使得响应式代码越来越难以理解.
尽管同步发出了Loading
状态, Compose UI 依然要求初始值用于所有的Flow
或者Observable
. 这是一种分层冲突, 因为视图层无法规定合理的默认值, 因为表示者层控制模型对象.
Molecule修复了这些问题. 我们的presenter能够返回StateFlow<ProfileModel>
, 它的初始状态能够被Compose UI在视图层同步读取. 并且通过使用Compose, 我们也能够通过使用基于Kotlin语言的命令式代码而非包含RxJava包API的响应式代码来构建模型对象.
@Composable
fun ProfilePresenter(
userFlow: Flow<User>,
balanceFlow: Flow<Long>,
): ProfileModel {
val user by userFlow.collectAsState(null)
val balance by balanceFlow.collectAsState(0L)
return if (user == null) {
Loading
} else {
Data(user.name, balance)
}
}
生成模型的可组合函数可以通过launchMolecule
来运行.
val userFlow = db.users()
val balanceFlow = db.balances()
val models: StateFlow<ProfileModel> = scope.launchMolecule(clock = ContextClock) {
ProfilePresenter(userFlow, balanceFlow)
}
运行ProfilePresenter
并且和StateFlow
分享输出的协程会在给定的CoroutineScope
.
在视图层, 消费模型对象的StateFlow
变得无关紧要.
@Composable
fun Profile(models: StateFlow<ProfileModel>) {
val model by models.collectAsState()
when (model) {
is Loading -> Text("Loading…")
is Data -> Text("${model.name} - ${model.balance}")
}
}
更多信息可以查看launchMolecule
文档.
Flow
除了StateFlow
之外, Molecule能够创建常规的Flow
.
这是个presenter例子, 更新了会使用常规的Flow
:
val userFlow = db.users()
val balanceFlow = db.balances()
val models: Flow<ProfileModel> = moleculeFlow(clock = Immediate) {
ProfilePresenter(userFlow, balanceFlow)
}
以及这个计算器例子:
fun counter(): Flow<Int> = moleculeFlow(clock = Immediate) {
val count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1_000)
count++
}
}
count
}
更多信息请查看moleculeFlow
文档.
用法
添加buildscript依赖并且在每一个想要调用launchMolecule
或者定义@Composable
函数来使用Molecule的模块中应用插件.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'app.cash.molecule:molecule-gradle-plugin:0.4.0'
}
}
apply plugin: 'app.cash.molecule'
开发版本的快照在Sonatype的快照仓库可用.
帧时钟
无论任何时候Jetpack Compose重组, 它都要在开始工作之前等待下一帧. 它依赖于CoroutineContext
里面的MonotonicFrameClock
来知晓新帧发送的时候. Molecule就是底层的Jetpack Compose, 所以它也要求帧时钟: 直到帧发送了, 值才会产生并且重组发生.
然而, 不像Jetpack Compose, Molecule有时候也会运行在不提供MonotonicFrameClock
的环境下. 因此Molecule的全部API都要求你指定自己偏好的时钟行为:
RecompositionClock.ContextClock
的行为像Jetpack Compose: 它将从调用的coroutineContext
中提取MonotonicFrameClock
,并使用它进行重新编译. 如果没有MonotonicFrameClock
, 它将会抛出异常.ContextClock
和Android的AndroidUiDispatcher.Main
工作的时候是有用的.Main
拥有内置的MonotonicFrameClock
, 与设置的帧率保持同步. 所以运行在Main
上并拥有ContextClock
的Molecule也会和帧率以相同的锁节奏运行. 干净又利索! 当然你也可以提供自己的BroadcastFrameClock
来实现自己的帧率.RecompositionClock.Immediate
将会构建一个即时时钟. 该时钟会在任何时候封闭流发出数据项时生成一帧. (对于StateFlow
而言情况总是这样.)Immediate
可在没有时钟的情况下使用, 无需任何额外接线. 它对于单元测试或者非主线程运行Molecule有用.
测试
使用moleculeFlow(clock = Immediate)
并且使用Turbine进行测试. moleculeFlow
在Turbine上就像任何其它流一样运行.
@Test fun counter() = runTest {
moleculeFlow(RecompositionClock.Immediate) {
Counter()
}.test {
assertEquals(0, awaitItem())
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
cancel()
}
}
如果你正在Android模块的JVM上单元测试Molecule, 请在项目的AGP配置上设置如下内容.
android {
...
testOptions {
unitTests.returnDefaultValues = true
}
...
}