查看原文
其他

解读Compose的项目中的知识点

麦客奥德彪 郭霖
2024-07-19



/   今日科技快讯   /


苹果公司当地时间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了,最最尴尬的是,这东西学了不用就全忘了,特别是理论的东西,又没有机会写,只能读项目了。


/   读项目吧   /


读之前也要做些了解的,比如:


  1. Compose 开发中的架构问题

  2. 页面应该怎么写

  3. 常用的控件啊 这些无所谓


我选择的项目是官方的nowinandroid, 他的好处在于它是一个教程类项目,教程类项目肯定有很多相关的 “炫技” 在里面。


阅读方式


这没办法扯,就这吧,当然:


  1. 你可以从MainActivity 中一行行读

  2. 也可以跳着读,遇到不懂的技术点时弄明白,直到没有看不懂的技术为止(小递归,如果每次读下来判断条件没有变化,那就不要读了,不要再学习了,转前端吧!转大前端吧!)


所以我记录的是这个项目中我不懂的一个个技术点。


/   知识点 Effect及带出来的问题   /


首页第一个看不明白的就是:


 DisposableEffect(darkTheme) {
                //...
            } 


DisposableEffect, 位于Effect.kt 文件中,(这里有个小技巧,kotlin 作为可顶级函数编程模式,导致很多功能相似或相同的函数都位于同一个文件中)

查阅发现一共有以下几种:


  1. SideEffect

  2. LaunchedEffect

  3. 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 接口包含三个主要的生命周期回调:


  1. onRemembered:当状态对象被创建并进入 Composition 时调用。

  2. onForgotten:当状态对象离开 Composition 时调用。

  3. 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:


  1. SideEffect

  2. LaunchedEffect

  3. 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 更为合适。


推荐阅读:

我的新书,《第一行代码 第3版》已出版!

两台Android 设备同一个局域网下如何自由通信?

Android FCM 推送详解,出海应用必备


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存