新建一个 compose 项目
开始前,请下载最新版本的 Android Studio Arctic Fox,然后使用 Empty Compose Activity 模板创建应用。
我们先看看在 app/build.gradle 中是如何配置使用 compose 的。
android{
buildFeatures {
// viewbinding 之类的功能也需要在此开启
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
观察第一个不同之处
默认项目创建的这个 Activity 继承自 ComponentActivity
这个类,而不是我们熟悉的 AppcompatActivity
,这两个类的层级关系如下:
普通的用法:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
}
compose的用法:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
}
}
activity 里的 setContent{}
函数来自androidx.activity:activity-compose
在这个函数中还调用了另一个 setContent{}
函数来自androidx.compose.ui:ui
在 compose 中使用 viewmodel
viewmodel很好用,这毋庸置疑,在 compose 中 我们使用 viewmodel 更一步的简化了,只需要在需要时调用 viewModel<>()
函数即可
这一扩展来自于:androidx.lifecycle:lifecycle-viewmodel-compose
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val appViewModel = viewModel<AppViewModel>()
}
}
插播一个Flow的玩法
flow 是 kotlin 推出的用于在协程中处理多个异步数据返回的工具(异步数据流),地位等同于 Rxjava 但是比 RX 容易很多也简洁很多。
看一段示例代码:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
fun simple(): Flow<Int> = flow { // 流构建器
for (i in 1..3) {
delay(100) // 假装我们在这里做了一些有用的事情
emit(i) // 发送下一个值
}
}
fun main() = runBlocking<Unit> {
// 启动并发的协程以验证主线程并未阻塞
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}
// 收集这个流
simple().collect { value -> println(value) }
}
//sampleEnd
flow函数可以构建一个 Flow,注意 flow
函数是一个 suspend 挂起函数;
- 名为 flow 的 Flow 类型构建器函数。
flow { ... }
构建块中的代码可以挂起。- 函数 simple 不再标有
suspend
修饰符。 - 流使用
emit
函数 发射 值。 - 流使用
collect
函数 收集 值。
流是冷的
冷流只是一个概念,等同于 rx 中,创建的 Observable
,他在被subscribe
之前是不会执行的
Flow 也一样,调用构建函数 flow{}
并不会执行异步流,lambda中的发射函数 emit
也不会执行
直到 flow
函数被 collect()
收集,此时流才开始工作,这也就是所谓冷流的概念。
看一段我以前项目的示例:
private fun syncToDb() {
lifecycleScope.launch {
//sync to db
flow {
val map = mapOf("userObjId" to SpUtils.decodeString(Constants.SP_USER_ID))
val resp = BmobMethods.INSTANCE.getAllNoteByUserId(map.toJson())
emit(resp) // 将网络请求响应发射到流中
}.catch { e ->
XLog.e(e)
}.flatMapConcat {
val allNote = it.toBean<GetAllNoteResp>()
allNote.results.asFlow() // 展平流中的数组,并作为流发射
}.onEach { note ->
// 在此可以一次处理上一步中发射的数组成员
// ....
}.flowOn(Dispatchers.IO).onCompletion {
// 流中的所有数据处理完毕后会回调此处
}.collect {} // 启动流
}
}
这是我以前写的一个示例demo,其中用到了Flow,感兴趣的可以去看一下源码:junerver/CloudNote 注意上面使用到的关键函数:
flow
之前我们说过了,用于构造Flow流,通过 emit
发射数据
catch
捕获异常没啥好说的
flatMapConcat
等同于 RxJava中的 flatMap
,展平,他的最后一行是返回值,因为我的返回值是一个List需要通过 asFlow
转换成 flow 流,完成展平
onEach
对上游发射过来的每一条数据进行处理,该函数的返回值是被我门处理后的数据,不需要我们特别处理
flowOn
约等于 subscribeOn(Schedulers.io())
表示流执行的协程
collect
之前说过 约等于 subscribe()
函数,此事流开始执行,注意 collect
执行在哪个协程就在哪个协程,不像RxJava需要使用 observeOn(AndroidSchedulers.mainThread())
来切换到主线程这样的操作
最简单的流操作就大概如此了,除此以外还有比如 SharedFlow、StateFlow 等进阶内容,以后用到再说
可以参考的文章: 协程进阶技巧 - StateFlow和SharedFlow Kotlin Flow】 一眼看全——Flow操作符大全 不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比
为什么要在 Compose 中说 Flow
Flow 很好用,比如 StateFlow 甚至可以代替 LiveData,在 Compose 中这一点被放大了
在包 androidx.compose.ui:ui-tooling
中依赖了 androidx.compose.runtime:runtime
它为 StateFlow 提供了一个扩展函数 collectAsState()
@Suppress("StateFlowValueCalledInComposition")
@Composable
fun <T> StateFlow<T>.collectAsState(
context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)
该函数返回了一个 State
类型的封装对象,State
对象就是驱动 Compose 组件更新 UI 的关键!
在 Compose 中,State
是只读状态对象,我们可以通过 value
属性来获取其值,MutableState
是可读写状态对象,可以设置其值,来实现修改状态,驱动 UI 更新。
下面是 State 对象的源码,可以看到它是一个简单的接口,实现了 by 委托中获取值的操作符重载
@Stable
interface State<out T> {
val value: T
}
//注意这是by关键字实现
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
在 Compose 中使用 State
对象时我们有两种方法:
- 直接使用,例如:
val state = someFlow().collectAsState()
,这时数据被 State 对象包裹,读取时需要调用state.value
- 使用
by
关键字,State
对象实现了 by 委托,这在需要赋值的场景会很方便
var count = 0
val testStateFlow = MutableStateFlow(count)
@Composable
fun Greeting(name: String) {
//这里要转型为 MutableState,这样state就是可读写的了(不要这么做,这里只是为了演示)
var state by testStateFlow.collectAsState() as MutableState
Column {
Text(text = "Hello $name!${state}")
Button(onClick = {
//模拟流数据变化
testStateFlow.value = ++count
//因为是可变的state,这里我们也可以直接写
state += 1
},
modifier = Modifier
.height(50.dp)
.width(100.dp)
){
Text(text = "点一下")
}
}
}
需要注意的是 collectAsState()
函数的返回值是 State
,通过 by 关键字只能获得到只读属性,我们需要进行一次转型 as MutableState
,之所以能转型为 MutableState
,我们看源码的 produceState
函数源码可以发现实际返回的是一个可变State;
注意:这里其实是不规范的操作,作者在2022年时刚接触 Compose,在现在看来应该明确 State
、MutableState
的职能。如果一个函数返回的是 State
,表明我们不期望函数使用者修改状态,因为这会破坏单一信源原则(即状态可以从多处修改)。
@Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
// 这里可以看到实际生成的state是可变state
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
这里多谢 墨丘比丘 指出
collectAsState()
获取的值使用by
后可以通过转型变成可读写状态。但是我还要指出的一点就是,flow流我们应该视作数据来源,或者状态store,他不应该被直接修改,这里我们只是展示一种读写方式。
对于状态与状态管理可以参考我写的另一篇文章:# Compose学习笔记2 - LaunchedEffect、状态与 状态管理,或者从前端工程对状态管理中中触类旁通:# 从零开始学习React-5:状态与状态管理
这里其实我们可以还可以有别的写法:
var count = 0
val testState = mutableStateOf(count)
@Composable
fun Greeting(name: String) {
Column {
//直接使用State
Text(text = "Hello $name!${testState.value}")
Button(onClick = { testState.value = ++count},
modifier = Modifier
.height(50.dp)
.width(100.dp)
){
Text(text = "点一下")
}
}
}
就是不使用 Flow 的写法,直接使用 compose 提供的 State。
但这些都不是正确的写法!
为什么说他是错误的写法,因为 compose 是函数式的UI构建方式,每一个composable 函数自身应该是不依赖外部变量的,这种写法其实严重的违背了这一原则。
这时候就要介绍我们的 remember
函数了!
/**
* Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
* Recomposition will always return the value produced by composition.
*/
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
机翻:记住由calculation产生的value。calculation只会在组成过程中进行评估。compose 重构时将始终返回由 composition 产生的值。 大致意思就是:该函数可以将块中的值保存起来,当compose视图重构时,可以读取到这个保存的值。
这种用法类似于在 Flutter 中使用StatefulWidget
类来包装控件(或者使用GetX),而在 Kotlin Compose 中,将 FP 风格贯彻的很好,没有那么多类,而是一个个的函数。需要实现这种 Stateful
时也更为简单。
这种写法乍一看来很奇怪,把变量保存在一个函数里,但其实有很大的优势,比如可以 UI 与逻辑混合使用,这一点比起 Flutter 具有巨大的优势。
例如下面这样的写法:
@Composable
fun Greeting(name: String) {
val counter = remember {
mutableStateOf(0)
}
Column {
if (counter.value % 2 == 1) {
Text(text = "Hello $name!${counter.value}")
}
Button(
onClick = {
counter.value += 1
},
modifier = Modifier
.height(50.dp)
.width(100.dp)
) {
Text(text = "点一下")
}
}
}
你可以试一下上面的例子,就能看出在 compose 中 UI 配合逻辑是多么简单。
小tips:如果你需要频繁的用到这个state ,翻来覆去的写 state.value 确实让人很烦,这时通过 by 关键字来获取确实很轻松。
在 MutableState 与 MutableStateFlow中如何选择?
上面我们这两个用于保存状态的类型我们都使用了一编,仅他俩作为 State 使用时看起来区别不大,如何选择其实完全看使用场景:
个人认为,在一些简单的场景,我们只是想要这个值用于 UI 的响应式变更刷新,那么直接使用 State 就可以了;如果数据来源是一个流,并且需要做一系列处理(比如 map
、flatMap
等),最终结果以 State 形式反应在 UI 上,那就用 Flow 流更方便。
补充(25/02/08)
如果你在2025年看到这篇文章,我向你推荐直接使用 ComposeHooks 项目,该项目针对 Compose 状态管理做了很多完善的封装。
欢迎阅读下面的系列文章: