解读Compose的项目中的知识点
/ 今日科技快讯 /
苹果公司当地时间6月10日在WWDC24开发者大会上宣布与OpenAI构建合作伙伴关系,由GPT-4o提供支持的ChatGPT集成将于今年晚些时候登陆iOS、iPadOS和macOS。用户无需创建帐户即可免费访问它,ChatGPT订阅者可以直接从这些体验中连接他们的帐户并访问付费功能。苹果的Siri语音助手可以连接ChatGPT。OpenAI称,在Siri和写作工具中访问ChatGPT时,OpenAI不会存储请求,并且用户的IP地址会被遮盖。
/ 作者简介 /
本篇文章转自麦客奥德彪的博客,文章主要分享了他如何阅读Compose项目中的知识点,相信会对大家有所帮助!
原文地址:
https://juejin.cn/post/7377439806135795764
/ 前言 /
我是一个非常喜欢学习的人,在这行业中摸爬滚打这么多年,靠的就是技术栈从未落后过。然而,不幸的是,公司不允许使用kotlin, 更别提compose了。
这就尴尬了,如果是我刚毕业那会儿,这无所谓,因为我有大把的时间弥补欠缺,之后的工作中补回来就OK了。
更尴尬的是,我30了,最最尴尬的是,这东西学了不用就全忘了,特别是理论的东西,又没有机会写,只能读项目了。
/ 读项目吧 /
读之前也要做些了解的,比如:
Compose 开发中的架构问题
页面应该怎么写
常用的控件啊 这些无所谓
我选择的项目是官方的nowinandroid, 他的好处在于它是一个教程类项目,教程类项目肯定有很多相关的 “炫技” 在里面。
阅读方式
这没办法扯,就这吧,当然:
你可以从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) }
}
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。
在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。
RememberObserver
RememberObserver 是一个接口,用于管理 remember 状态对象的生命周期。通过实现这个接口,可以在状态对象的创建、进入和离开 Composition 的时候执行特定的逻辑。
RememberObserver 详解
RememberObserver 接口包含三个主要的生命周期回调:
onRemembered:当状态对象被创建并进入 Composition 时调用。
onForgotten:当状态对象离开 Composition 时调用。
onAbandoned:当状态对象由于 Composition 中止或重启而被遗弃时调用。
这些回调使你能够在特定的生命周期阶段执行特定的逻辑,例如启动和清理资源。
接口定义
interface RememberObserver {
fun onRemembered()
fun onForgotten()
fun onAbandoned()
}
示例
以下是一个实现 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()
}
解释
LoggingRememberObserver
实现 RememberObserver 接口,在每个生命周期回调中记录日志。
onRemembered、onForgotten、onAbandoned 分别在不同的生命周期阶段调用。
RememberObserverExample
使用 remember 创建 LoggingRememberObserver 实例,并将其与 Composition 关联。
DisposableEffect 可以用于处理在 Composition 离开时的清理操作。
UI 内容
显示一个简单的文本,表示示例的 UI 内容。
RememberObserver 的实际应用
RememberObserver 主要用于需要在状态对象的生命周期内执行特定操作的场景。例如:
资源管理:打开和关闭数据库连接、注册和取消注册监听器等。
副作用管理:启动和停止定时任务、管理网络请求的生命周期等。(而Effect 正是利用这个完成他们的功能的)
性能优化:在适当的时候初始化和销毁昂贵的资源。
Composition 是什么
Composition 是一个核心概念,它描述了如何将 UI 的状态与其界面元素关联起来。具体来说,Composition 是一个将可组合函数(Composables)及其状态在运行时结合在一起的过程。
关键点
声明式 UI
在 Jetpack Compose 中,UI 是声明式的,即 UI 是当前状态的函数。UI 会根据状态的变化自动重组(recompose)。
Composable 函数
@Composable 注解的函数可以定义 UI 元素和布局。这些函数可以组合在一起,形成复杂的 UI 层次结构。
Recomposition
当与 UI 相关的状态发生变化时,Compose 会重新运行相应的可组合函数,以更新 UI。这种重新运行称为重组(Recomposition)。
Composition 的生命周期
Composition 的生命周期与 UI 的生命周期密切相关。当某个 Composable 函数第一次执行时,它进入 Composition。当状态变化导致 UI 需要更新时,该函数会重新组合。
示例
以下是一个简单的 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()
}
}
解释
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 的过程。
解释Effect问题
结合上述的知识点,这个组件的作用就是根据组件的Composition状态,做一些Compose之外的工作。
在 Jetpack Compose 中,Effects 是用于处理副作用(side effects)的工具。副作用是指在 Compose 之外的环境中执行的操作,比如网络请求、数据库访问、订阅和取消订阅等。Compose 提供了一组用于处理这些操作的 API,确保这些操作在 UI 状态变化时能正确执行。
Compose 中的主要 Effects API:
SideEffect
LaunchedEffect
DisposableEffect
SideEffect
SideEffect 是一个简单的效果处理器,用于在每次重组(recomposition)后执行一些操作。它通常用于调试和日志记录。
@Composable
fun SideEffectExample(counter: Int) {
SideEffect {
println("The counter value is $counter")
}
Text("Counter: $counter")
}
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)
}
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")
}
使用示例综合
以下是一个综合示例,展示了如何在实际应用中使用这些 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)
}
总结
SideEffect:用于简单的副作用,如日志记录。
LaunchedEffect:用于在 Composition 中启动和管理协程,适合异步操作。
DisposableEffect:用于在 Composition 生命周期内处理需要清理的副作用。
/ 知识点 CompositionLocalProvider /
CompositionLocalProvider 是 Jetpack Compose 中用于在局部范围内提供数据的工具。它允许你定义和传递局部数据,而不需要通过参数一级一级地传递。这种机制类似于 React 中的 Context API,但在 Jetpack Compose 中使用的是 CompositionLocal。
CompositionLocalProvider 可以在 Compose 的 Composition 树中设置局部数据,这些数据可以在树中的任何子组件中访问。它通常与 CompositionLocal 一起使用。
核心概念
CompositionLocal:这是一个可以在 Composition 树中任何地方访问的局部数据的容器。
CompositionLocalProvider:这是用于提供 CompositionLocal 数据的 Composable 函数。
定义 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 对象。
使用 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()
}
解释
定义 LocalUser
compositionLocalOf<User> 定义了一个 CompositionLocal,它存储 User 类型的值。默认情况下,如果没有提供值,会抛出异常。
UserInfo Composable
使用 LocalUser.current 获取当前的 User 对象,并显示其姓名和年龄。
UserScreen Composable
创建一个 User 对象,并使用 CompositionLocalProvider 提供 LocalUser。在 CompositionLocalProvider 的作用范围内,LocalUser 的值将是我们提供的 user 对象。
多个 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()
}
解释
定义 LocalThemeColor
staticCompositionLocalOf 定义了一个静态的 CompositionLocal,它存储 Color 类型的值。
ThemedUserInfo Composable
使用 LocalUser.current 获取当前的 User 对象。
使用 LocalThemeColor.current 获取当前的颜色值,并将其应用到文本颜色上。
ThemedUserScreen Composable
使用 CompositionLocalProvider 同时提供 LocalUser 和 LocalThemeColor。
只要是在 CompositionLocalProvider 的 content 块中调用的所有 Composable 函数,都可以访问 CompositionLocal 提供的数据。这是因为 CompositionLocalProvider 会在其 content 块内设置一个作用域,该作用域内所有的 Composable 都可以访问通过 CompositionLocal 提供的数据。
CompositionLocal的方式都有哪些
有两种主要的方式来定义 CompositionLocal,分别是 compositionLocalOf 和 staticCompositionLocalOf。下面是它们的详细介绍:
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()
}
}
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()
}
}
区别和适用场景
动态 vs. 静态
compositionLocalOf 提供了动态创建 CompositionLocal 的方式,适用于需要在运行时根据条件提供不同值的情况。
staticCompositionLocalOf 提供了在编译时确定默认值的方式,适用于值是固定的情况。
默认值的处理
compositionLocalOf 允许在提供默认值时执行代码逻辑,例如抛出异常等。
staticCompositionLocalOf 只能在编译时确定一个不可修改的默认值。
可变性
无论是动态还是静态创建的 CompositionLocal,其值都是可变的,可以在需要的时候通过 CompositionLocalProvider 提供不同的值。
一般来说,如果需要在运行时根据条件提供不同的默认值,或者需要在提供默认值时执行一些逻辑,那么使用 compositionLocalOf 更为适合。如果你的值是固定的,不会在运行时改变,那么使用 staticCompositionLocalOf 更为合适。
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注