新建一个 compose 项目 开始前,请下载最新版本的 Android Studio Arctic Fox,然后使用 Empty Compose Activity 模板创建应用。
我们先看看在 app/build.gradle 中是如何配置使用 compose 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 android{ buildFeatures { 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
,这两个类的层级关系如下:
普通的用法:
1 2 3 4 5 @Override public void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_webview); }
compose的用法:
1 2 3 4 5 6 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
1 2 3 4 5 6 7 override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false ) setContent { val appViewModel = viewModel<AppViewModel>() } }
插播一个Flow的玩法 flow 是 kotlin 推出的用于在协程中处理多个异步数据返回的工具(异步数据流),地位等同于 Rxjava 但是比 RX 容易很多也简洁很多。
看一段示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import kotlinx.coroutines.*import kotlinx.coroutines.flow.*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) } }
flow函数可以构建一个 Flow,注意 flow
函数是一个 suspend 挂起函数;
名为 flow 的 Flow 类型构建器函数。
flow { ... }
构建块中的代码可以挂起。
函数 simple 不再标有 suspend
修饰符。
流使用 emit
函数 发射 值。
流使用 collect
函数 收集 值。
流是冷的 冷流只是一个概念,等同于 rx 中,创建的 Observable
,他在被subscribe
之前是不会执行的 Flow 也一样,调用构建函数 flow{}
并不会执行异步流,lambda中的发射函数 emit
也不会执行 直到 flow
函数被 collect()
收集,此时流才开始工作,这也就是所谓冷流的概念。
看一段我以前项目的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 private fun syncToDb () { lifecycleScope.launch { 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 -> val objId = note.objectId val dbBean = NoteUtils.queryNoteById(objId) if (dbBean == null ) { val entity = note.toEntity() entity.save() XLog.d("本地没有该数据 新建\n$entity " ) } dbBean?.let { if (it.isLocalDel) { val resp = BmobMethods.INSTANCE.delNoteById(it.objId) if (resp.contains("ok" )) { it.delete() XLog.d("远端删除成功 本地删除\n$resp " ) } return @let } when { note.updatedTime > it.updatedTime -> { it.update(note) XLog.d("使用云端数据更新本地数据库\n$it \n$note " ) } note.updatedTime < it.updatedTime -> { note.update(it) val resp = BmobMethods.INSTANCE.putNoteById( objId, note.toJson(excludeFields = Constants.DEFAULT_EXCLUDE_FIELDS) .createJsonRequestBody() ) val putResp = resp.toBean<PutResp>() XLog.d("使用本地数据更新云端数据 \n$it \n$note " ) } else -> { XLog.d("本地数据与云端数据相同\n$it \n$note " ) return @let } } it.isSync = true it.save() } }.flowOn(Dispatchers.IO).onCompletion { XLog.d("Bmob☁️同步执行完毕,开始同步本地数据到云端" ) syncToBmob() }.collect { } } } private suspend fun syncToBmob () { NoteUtils.listNotSync().asFlow() .onEach { XLog.d(it) if (it.objId.isEmpty()) { val note = it.toBmob() val resp = BmobMethods.INSTANCE.postNote( note.toJson(excludeFields = Constants.DEFAULT_EXCLUDE_FIELDS) .createJsonRequestBody() ) val postResp = resp.toBean<PostResp>() it.objId = postResp.objectId XLog.d("本地有云端无 新建数据" ) } else { it.isCloudDel = true XLog.d("不太可能出现的一种情况\n$it " ) } it.isSync = true it.save() }.flowOn(Dispatchers.IO).onCompletion { listAllFromDb() }.collect { XLog.d(it) } }
这是我以前写的一个示例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()
1 2 3 4 5 @Suppress("StateFlowValueCalledInComposition" ) @Composable fun <T> StateFlow<T> .collectAsState ( context: CoroutineContext = EmptyCoroutineContext ) : State<T> = collectAsState(value, context)
这里看起来函数返回了 一个State类型的封装对象,其实不然
1 2 3 4 5 6 7 8 @Stable interface State <out T > { val value: T } @Suppress("NOTHING_TO_INLINE" ) inline operator fun <T> State<T> .getValue (thisObj: Any ?, property: KProperty <*>) : T = value
这是一个非常屌的包装 返回的这个 State 我们使用的时候无需任何解包装,直接就可以使用,而且他似乎还是 Stateful 的。
当然使用的时候不能直接使用这个对象,需要通过 by 来实现获取到value的效果,这样就不需要调用 state.value
这样的写法了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var count = 0 val testStateFlow = MutableStateFlow(count)@Composable fun Greeting (name: String ) { var state by testStateFlow.collectAsState() as MutableState Column { Text(text = "Hello $name !${state} " ) Button(onClick = { testStateFlow.value = ++count state += 1 }, modifier = Modifier .height(50. dp) .width(100. dp) ){ Text(text = "点一下" ) } } }
需要注意的是 collectAsState()
函数的返回值是 State
,通过 by 关键字只能获得到只读属性,我们需要进行一次转型 as MutableState
,之所以能转型为 MutableState
,我们看源码的 produceState
函数源码可以发现实际返回的是一个可变State;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Composable fun <T> produceState ( initialValue: T , key1: Any ?, key2: Any ?, @BuilderInference producer: suspend ProduceStateScope <T >.() -> Unit ) : State<T> { val result = remember { mutableStateOf(initialValue) } LaunchedEffect(key1, key2) { ProduceStateScopeImpl(result, coroutineContext).producer() } return result }
这里多谢 墨丘比丘 指出collectAsState()
获取的值使用 by
后可以通过转型变成可读写状态。
但是我还要指出的一点就是,flow流我们应该视作数据来源,或者状态store,他不应该被直接修改,这里我们只是展示一种读写方式。
对于状态与状态管理可以参考我写的另一篇文章:# Compose学习笔记2 - LaunchedEffect、状态与 状态管理 ,或者从前端工程对状态管理中中触类旁通:# 从零开始学习React-5:状态与状态管理
这里其实我们可以还可以有别的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var count = 0 val testState = mutableStateOf(count)@Composable fun Greeting (name: String ) { Column { 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
函数了!
1 2 3 4 5 6 7 @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 具有巨大的优势。
例如下面这样的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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 流更方便。