我是一个非常喜欢学习的人,在这行业中摸爬滚打这么多年,靠的就是技术栈从未落后过。
然而,不幸的是,公司不允许使用kotlin, 更别提compose了
这就尴尬了,如果是我刚毕业那会儿,这无所吊谓,因为我有大把的时间弥补欠缺,之后的工作中补回来就OK了
更尴尬的是,我他妈30了
最最尴尬的是,这东西学了不用就全忘了,特别是理论的东西,又没有机会写,只能读项目了。
一、读项目吧
读之前也要做些了解的,比如
- Compose 开发中的架构问题
- 页面应该怎么写
- 常用的控件啊 这些无所吊谓
我选择的项目是官方的nowinandroid, 他的好处在于它是一个教程类项目,教程类项目肯定有很多相关的 “炫技” 在里面。
1.1 阅读方式
这没办法扯,就这吧,当然
- 你可以从MainActivity 中一行行读
- 也可以跳着读,遇到不懂的技术点时弄明白,直到没有看不懂的技术为止(小递归,如果每次读下来判断条件没有变化,那就不要读了,不要再学习了,转前端吧!转大前端吧!)
所以我记录的是这个项目中我不懂的一个个技术点。
二、知识点 Effect及带出来的问题
首页第一个看不明白的就是
DisposableEffect(darkTheme) {
//...
}
DisposableEffect, 位于Effect.kt 文件中,(这里有个小技巧,kotlin 作为可顶级函数编程模式,导致很多功能相似或相同的函数都位于同一个文件中)
查阅发现一共有以下几种:
- SideEffect
- LaunchedEffect
- DisposableEffect
LaunchedEffect 分析
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
2.1 remember 是什么?
remember 是一个函数,用于在 Composable 函数中记住(缓存)某个值,并在 UI 重新组合时保持该值不变
@Composable
inline fun <T> remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T {
@Suppress("UNCHECKED_CAST")
return rememberedValue().let {
if (invalid || it === Composer.Empty) {
val value = block()
updateRememberedValue(value)
value
} else it
} as T
}
当currentComposer.changed(key1) 的值发生变化时,会更新存储并且执行calculation
2.2 在LaunchedEffect中的rember 值变化时执行的LaunchedEffectImpl 是什么?
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
// This should never happen but is left here for safety
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
override fun onAbandoned() {
job?.cancel(LeftCompositionCancellationException())
job = null
}
}
实现了RememberObserver
2.3 RememberObserver
RememberObserver 是一个接口,用于管理 remember 状态对象的生命周期。通过实现这个接口,可以在状态对象的创建、进入和离开 Composition 的时候执行特定的逻辑。
2.3.1 RememberObserver 详解
RememberObserver 接口包含三个主要的生命周期回调:
- onRemembered:当状态对象被创建并进入 Composition 时调用。
- onForgotten:当状态对象离开 Composition 时调用。
- onAbandoned:当状态对象由于 Composition 中止或重启而被遗弃时调用。
这些回调使你能够在特定的生命周期阶段执行特定的逻辑,例如启动和清理资源。
2.3.2 接口定义
interface RememberObserver {
fun onRemembered()
fun onForgotten()
fun onAbandoned()
}
2.3.3 示例
以下是一个实现 RememberObserver 的简单示例,用于在组件的生命周期内记录日志:
import androidx.compose.runtime.*
class LoggingRememberObserver(private val tag: String) : RememberObserver {
override fun onRemembered() {
println("$tag: Remembered")
}
override fun onForgotten() {
println("$tag: Forgotten")
}
override fun onAbandoned() {
println("$tag: Abandoned")
}
}
@Composable
fun RememberObserverExample() {
val observer = remember { LoggingRememberObserver("MyObserver") }
DisposableEffect(observer) {
onDispose { /* Optionally perform cleanup here */ }
}
// Your UI content
Text("RememberObserver Example")
}
@Preview
@Composable
fun PreviewRememberObserverExample() {
RememberObserverExample()
}
2.3.4 解释
-
LoggingRememberObserver:
- 实现
RememberObserver接口,在每个生命周期回调中记录日志。 onRemembered、onForgotten、onAbandoned分别在不同的生命周期阶段调用。
- 实现
-
RememberObserverExample:
- 使用
remember创建LoggingRememberObserver实例,并将其与 Composition 关联。 DisposableEffect可以用于处理在 Composition 离开时的清理操作。
- 使用
-
UI 内容:
- 显示一个简单的文本,表示示例的 UI 内容。
2.3.5 RememberObserver 的实际应用
RememberObserver 主要用于需要在状态对象的生命周期内执行特定操作的场景。例如:
- 资源管理:打开和关闭数据库连接、注册和取消注册监听器等。
- 副作用管理:启动和停止定时任务、管理网络请求的生命周期等。(而Effect 正是利用这个完成他们的功能的)
- 性能优化:在适当的时候初始化和销毁昂贵的资源。
2.4 Composition 是什么
Composition 是一个核心概念,它描述了如何将 UI 的状态与其界面元素关联起来。具体来说,Composition 是一个将可组合函数(Composables)及其状态在运行时结合在一起的过程。
2.4.1 关键点
-
声明式 UI:
- 在 Jetpack Compose 中,UI 是声明式的,即 UI 是当前状态的函数。UI 会根据状态的变化自动重组(recompose)。
-
Composable 函数:
@Composable注解的函数可以定义 UI 元素和布局。- 这些函数可以组合在一起,形成复杂的 UI 层次结构。
-
Recomposition:
- 当与 UI 相关的状态发生变化时,Compose 会重新运行相应的可组合函数,以更新 UI。
- 这种重新运行称为重组(Recomposition)。
-
Composition 的生命周期:
- Composition 的生命周期与 UI 的生命周期密切相关。当某个 Composable 函数第一次执行时,它进入 Composition。当状态变化导致 UI 需要更新时,该函数会重新组合。
2.4.2 示例
以下是一个简单的 Jetpack Compose 示例,展示了 Composition 的基本概念:
import androidx.compose.runtime.*
import androidx.compose.material.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.*
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "Count: $count", style = MaterialTheme.typography.h4)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Column {
Greeting(name = "Compose")
Counter()
}
}
2.4.3 解释
-
Greeting Composable 函数:
- 定义了一个简单的文本显示函数。
Greeting(name: String)会显示一个带有name参数值的文本。
-
Counter Composable 函数:
- 定义了一个计数器组件,显示当前计数并有一个按钮来增加计数。
var count by remember { mutableStateOf(0) }声明并记住状态count,当count变化时,UI 会自动重组。Button(onClick = { count++ }):按钮点击事件会增加count。
-
DefaultPreview Composable 函数:
- 用于预览,展示
Greeting和Counter组件的组合。
- 用于预览,展示
- Composition 是 Jetpack Compose 中将 UI 与其状态结合在一起的过程。
- 可组合函数 是构建 UI 的基本单元,通过组合这些函数,可以构建复杂的 UI。
- 重组 是当状态变化时重新运行可组合函数以更新 UI 的过程。
2.5 解释Effect问题
结合上述的知识点,这个组件的作用就是根据组件的Composition状态,做一些Compose之外的工作。
在 Jetpack Compose 中,Effects 是用于处理副作用(side effects)的工具。副作用是指在 Compose 之外的环境中执行的操作,比如网络请求、数据库访问、订阅和取消订阅等。Compose 提供了一组用于处理这些操作的 API,确保这些操作在 UI 状态变化时能正确执行。
2.5.1 Compose 中的主要 Effects API
SideEffectLaunchedEffectDisposableEffect
1. SideEffect
SideEffect 是一个简单的效果处理器,用于在每次重组(recomposition)后执行一些操作。它通常用于调试和日志记录。
@Composable
fun SideEffectExample(counter: Int) {
SideEffect {
println("The counter value is $counter")
}
Text("Counter: $counter")
}
2. LaunchedEffect
LaunchedEffect 用于启动协程来处理异步操作。它会在 Composable 进入 Composition 时启动,并在离开 Composition 时取消。它的主要特点是可以依赖于传入的键,当键发生变化时重新启动协程。
@Composable
fun LaunchedEffectExample(data: String) {
var result by remember { mutableStateOf("Loading...") }
LaunchedEffect(data) {
// 模拟网络请求
delay(1000L)
result = "Result for $data"
}
Text(result)
}
3. DisposableEffect
DisposableEffect 用于处理在 Composition 生命周期内需要清理的副作用。它会在 Composable 进入 Composition 时启动,在离开时执行清理操作。
@Composable
fun DisposableEffectExample(userId: String) {
DisposableEffect(userId) {
println("Start observing $userId")
onDispose {
println("Stop observing $userId")
}
}
Text("Observing user: $userId")
}
4. 使用示例综合
以下是一个综合示例,展示了如何在实际应用中使用这些 Effects API:
@Composable
fun ComprehensiveExample(userId: String, counter: Int) {
var userName by remember { mutableStateOf("Loading...") }
// LaunchedEffect 用于异步加载数据
LaunchedEffect(userId) {
// 模拟网络请求
delay(1000L)
userName = "User: $userId"
}
// DisposableEffect 用于订阅和取消订阅用户状态
DisposableEffect(userId) {
println("Start observing $userId")
onDispose {
println("Stop observing $userId")
}
}
Column {
Text(userName)
Text("Status: $userStatus")
// SideEffect 用于记录日志
SideEffect {
println("Rendering ComprehensiveExample with userId: $userId and counter: $counter")
}
}
}
@Preview
@Composable
fun PreviewComprehensiveExample() {
ComprehensiveExample(userId = "42", counter = 5)
}
5. 总结
SideEffect:用于简单的副作用,如日志记录。LaunchedEffect:用于在 Composition 中启动和管理协程,适合异步操作。DisposableEffect:用于在 Composition 生命周期内处理需要清理的副作用。
三、知识点 CompositionLocalProvider
CompositionLocalProvider 是 Jetpack Compose 中用于在局部范围内提供数据的工具。它允许你定义和传递局部数据,而不需要通过参数一级一级地传递。这种机制类似于 React 中的 Context API,但在 Jetpack Compose 中使用的是 CompositionLocal。
3.1 CompositionLocalProvider 详解
CompositionLocalProvider 可以在 Compose 的 Composition 树中设置局部数据,这些数据可以在树中的任何子组件中访问。它通常与 CompositionLocal 一起使用。
3.2 核心概念
CompositionLocal:这是一个可以在 Composition 树中任何地方访问的局部数据的容器。CompositionLocalProvider:这是用于提供CompositionLocal数据的 Composable 函数。
3.3 定义 CompositionLocal
首先,需要定义一个 CompositionLocal:
import androidx.compose.runtime.compositionLocalOf
data class User(val name: String, val age: Int)
val LocalUser = compositionLocalOf<User> { error("No user provided") }
在上面的代码中,我们定义了一个 CompositionLocal,用于存储 User 对象。
3.4 使用 CompositionLocalProvider
接下来,可以使用 CompositionLocalProvider 在局部范围内提供 User 数据:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.material.Text
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun UserInfo() {
val user = LocalUser.current
Text("Name: ${user.name}, Age: ${user.age}")
}
@Composable
fun UserScreen() {
val user = User(name = "John Doe", age = 30)
CompositionLocalProvider(LocalUser provides user) {
UserInfo()
}
}
@Preview
@Composable
fun PreviewUserScreen() {
UserScreen()
}
3.5 解释
-
定义
LocalUser:compositionLocalOf<User>定义了一个CompositionLocal,它存储User类型的值。默认情况下,如果没有提供值,会抛出异常。
-
UserInfoComposable:- 使用
LocalUser.current获取当前的User对象,并显示其姓名和年龄。
- 使用
-
UserScreenComposable:- 创建一个
User对象,并使用CompositionLocalProvider提供LocalUser。 - 在
CompositionLocalProvider的作用范围内,LocalUser的值将是我们提供的user对象。
- 创建一个
3.6 多个 CompositionLocalProvider
可以在同一个 CompositionLocalProvider 中提供多个 CompositionLocal,如下所示:
import androidx.compose.runtime.staticCompositionLocalOf
val LocalThemeColor = staticCompositionLocalOf { Color.Black }
@Composable
fun ThemedUserInfo() {
val user = LocalUser.current
val color = LocalThemeColor.current
Text("Name: ${user.name}, Age: ${user.age}", color = color)
}
@Composable
fun ThemedUserScreen() {
val user = User(name = "Jane Doe", age = 28)
val themeColor = Color.Blue
CompositionLocalProvider(
LocalUser provides user,
LocalThemeColor provides themeColor
) {
ThemedUserInfo()
}
}
@Preview
@Composable
fun PreviewThemedUserScreen() {
ThemedUserScreen()
}
3.7 解释
-
定义
LocalThemeColor:staticCompositionLocalOf定义了一个静态的CompositionLocal,它存储Color类型的值。
-
ThemedUserInfoComposable:- 使用
LocalUser.current获取当前的User对象。 - 使用
LocalThemeColor.current获取当前的颜色值,并将其应用到文本颜色上。
- 使用
-
ThemedUserScreenComposable:- 使用
CompositionLocalProvider同时提供LocalUser和LocalThemeColor。
- 使用
只要是在 CompositionLocalProvider 的 content 块中调用的所有 Composable 函数,都可以访问 CompositionLocal 提供的数据。这是因为 CompositionLocalProvider 会在其 content 块内设置一个作用域,该作用域内所有的 Composable 都可以访问通过 CompositionLocal 提供的数据。
3.8 CompositionLocal的方式都有哪些
有两种主要的方式来定义 CompositionLocal,分别是 compositionLocalOf 和 staticCompositionLocalOf。下面是它们的详细介绍:
1. compositionLocalOf
compositionLocalOf 是一种在运行时动态创建 CompositionLocal 的方式。它允许你使用一个默认值来定义 CompositionLocal,并在需要的时候通过提供不同的值来替换它。
使用方法:
val LocalUser = compositionLocalOf<User> { error("No user provided") }
LocalUser:定义的CompositionLocal变量。compositionLocalOf<User>:创建一个CompositionLocal,其值的类型是User。{ error("No user provided") }:提供一个默认值,当没有提供具体值时将抛出错误。
示例:
val LocalUser = compositionLocalOf<User> { error("No user provided") }
@Composable
fun UserInfo() {
val user = LocalUser.current
Text("Name: ${user.name}")
}
@Composable
fun UserScreen() {
val user = User(name = "John Doe", age = 30)
CompositionLocalProvider(LocalUser provides user) {
UserInfo()
}
}
2. staticCompositionLocalOf
staticCompositionLocalOf 是一种在编译时静态创建 CompositionLocal 的方式。它在声明时就提供了一个默认值,并且该值是不可修改的。
使用方法:
val LocalThemeColor = staticCompositionLocalOf { Color.Black }
LocalThemeColor:定义的CompositionLocal变量。staticCompositionLocalOf:创建一个静态的CompositionLocal。{ Color.Black }:提供一个默认值,该值在编译时确定,不可更改。
示例:
val LocalThemeColor = staticCompositionLocalOf { Color.Black }
@Composable
fun ThemedUserInfo() {
val color = LocalThemeColor.current
Text("User Info", color = color)
}
@Composable
fun ThemedUserScreen() {
CompositionLocalProvider(LocalThemeColor provides Color.Blue) {
ThemedUserInfo()
}
}
3. 区别和适用场景
-
动态 vs. 静态:
compositionLocalOf提供了动态创建CompositionLocal的方式,适用于需要在运行时根据条件提供不同值的情况。staticCompositionLocalOf提供了在编译时确定默认值的方式,适用于值是固定的情况。
-
默认值的处理:
compositionLocalOf允许在提供默认值时执行代码逻辑,例如抛出异常等。staticCompositionLocalOf只能在编译时确定一个不可修改的默认值。
-
可变性:
- 无论是动态还是静态创建的
CompositionLocal,其值都是可变的,可以在需要的时候通过CompositionLocalProvider提供不同的值。
- 无论是动态还是静态创建的
一般来说,如果需要在运行时根据条件提供不同的默认值,或者需要在提供默认值时执行一些逻辑,那么使用 compositionLocalOf 更为适合。如果你的值是固定的,不会在运行时改变,那么使用 staticCompositionLocalOf 更为合适。