(1) 探索 Jetpack Compose × MutableState
本文最后更新于 75 天前,其中的信息可能已经有所发展或是发生改变。

刚从 Android XML 转到 Compose 多少有些不习惯,由于没有任何 Compose 开发的经验,以下仅靠自己搜集各种资料总结出来的方法,并不代表是最高效、快速的实现方法。

这里先给出 Jetpack Compose 的官网文档地址:https://developer.android.google.cn/develop/ui/compose/setup

Theme 主题

先说我最关心的主题吧,在传统的开发中,一般用 stylex.xml 来定义主题,可以自定义主题和文本的颜色等。

而在 Compose 中,在官方默认提供的 Theme.kt 文件中已经为你定义好了主题,进去发现一个 @Composable 函数,函数名为 ${projectName}Theme(这里我的 App 名字是 ShadowCat)

@Composable
fun ShadowCatTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

记住这个 MateriaTheme,之后会经常在组件中用到,可以简单理解为这个类就是你的主题,而 colorScheme 中包含了所有你可以用的颜色。

现在 Compose 提供了更加方便的主题管理,只需要在这个组件中设定好你的主题,然后就可以在任何可组合式函数(@Composable)内使用了。

常用颜色

下面列举出 colorScheme 中所有可用的颜色:

  • primary: 应用的主要颜色,通常用于按钮、标题栏、选中的项目等主要元素上。
  • onPrimary: 显示在primary颜色背景上的文本或图标的颜色,通常是对比色(如白色或黑色),确保内容清晰可见。
  • primaryContainer: primary颜色的容器版本,通常用于强调次要背景的元素,较primary颜色稍微淡一些。
  • onPrimaryContainer: 显示在primaryContainer背景上的文本或图标的颜色。
  • inversePrimary: 反转的primary颜色,通常在深色模式下用作主要颜色。
  • secondary: 应用的次要颜色,用于次要操作按钮、标签或其他非主要的交互元素。
  • onSecondary: 显示在secondary颜色背景上的文本或图标的颜色。
  • secondaryContainer: secondary颜色的容器版本,用于次要背景的元素,较secondary颜色稍微淡一些。
  • onSecondaryContainer: 显示在secondaryContainer背景上的文本或图标的颜色。
  • tertiary: 应用的第三种颜色,通常用于强调某些特定的界面元素,例如通知或选择状态。
  • onTertiary: 显示在tertiary颜色背景上的文本或图标的颜色。
  • tertiaryContainer: tertiary颜色的容器版本,通常用于更次要或独特的界面元素。
  • onTertiaryContainer: 显示在tertiaryContainer背景上的文本或图标的颜色。
  • background: 应用的背景颜色,通常是主要界面的背景色。
  • onBackground: 显示在background颜色背景上的文本或图标的颜色。
  • surface: 应用的表面颜色,通常用于卡片、弹出框等元素的背景色。
  • onSurface: 显示在surface颜色背景上的文本或图标的颜色。
  • surfaceVariant: surface颜色的变体,通常用于区分不同层次的表面。
  • onSurfaceVariant: 显示在surfaceVariant背景上的文本或图标的颜色。
  • surfaceTint: 用于应用表面上的色调,这种颜色通常用来提供轻微的阴影效果或颜色渐变。
  • inverseSurface: 反转的surface颜色,通常在深色模式下使用。
  • inverseOnSurface: 显示在inverseSurface颜色背景上的文本或图标的颜色。
  • error: 错误状态下使用的颜色,通常用于表示表单验证失败或操作错误。
  • onError: 显示在error颜色背景上的文本或图标的颜色。
  • errorContainer: 错误状态的容器颜色,用于强调错误的背景。
  • onErrorContainer: 显示在errorContainer背景上的文本或图标的颜色。
  • outline: 用于元素边框或轮廓的颜色。
  • outlineVariant: outline颜色的变体,用于提供更细微的边框或轮廓。
  • scrim: 用于遮罩效果的颜色,通常用于模糊背景。
  • surfaceBright: 明亮的表面颜色,通常用于高亮表面。
  • surfaceDim: 昏暗的表面颜色,通常用于低调的背景。
  • surfaceContainer: 容器的表面颜色,用于界面中的容器或卡片。
  • surfaceContainerHigh: 高优先级容器的表面颜色,适用于更重要的容器。
  • surfaceContainerHighest: 最高优先级容器的表面颜色,用于最重要的容器或界面部分。
  • surfaceContainerLow: 低优先级容器的表面颜色,用于不太重要的容器。
  • surfaceContainerLowest: 最低优先级容器的表面颜色,用于最次要的容器或背景。

使用示例

首先用主题组件包裹你的其他组件:

ShadowCatTheme {
    YourComponentHere()
}

然后以默认的 MainActivity 为例:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ShadowCatTheme {
                Scaffold(
                    modifier = Modifier.fillMaxSize()
                        .background(MaterialTheme.colorScheme.background)
                ) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

现在 Scaffold 被 ShadowCatTheme 包裹了,那么这个组件以及它的子组件都可以用到刚才的 MaterialTheme。

此处就将主题内的背景颜色设置为了 MainActivity 的背景。

补充

关于为什么 MaterialTheme 可以在子组件中使用,实际上 MaterialTheme 是一个 CompositionLocalProvider。

CompositionLocalProvider 可以在它的子 Compose UI 树中传递数据,可以简单理解为一个全局变量容器,但是作用域仅在这个 Provider 组件内。

关于这个 CompositionLocalProvider 以后有机会再深入了解,感兴趣的可以先来这里看:https://developer.android.google.cn/develop/ui/compose/compositionlocal

Composable 组件

本篇文章并非从零开始的入门教程,想知道 Compose 中组件是怎么组合的可以前往此处查看:https://developer.android.google.cn/develop/ui/compose/layouts/basics

在传统的 xml 布局中,经常会用到一些通用的属性,例如 width height weight等,在 Compose 中它们被等价成了修饰符 (Modifier)。

修饰符详解:https://developer.android.google.cn/develop/ui/compose/modifiers

State 状态

接下来就是最重要的状态管理了,在传统的开发中我们经常使用 MVVM 的架构进行应用开发,而在 Compose 中 ViewModel 仍然用来管理应用的状态,主要适用于同 Activity 中不同组件之间的值传递。

MutableState

先说说在单个组件中如何管理状态吧,Compose 为我们提供了属性委托 remember {} 来管理组件内的状态。

下面定义一个状态变量:var greeting by remember { mutableStateOf("Hello $name!") }

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var greeting by remember { mutableStateOf("Hello $name!") }
    
    greeting += " Hello World!"
    
    Text(
        text = greeting,
        modifier = modifier
    )
}

此时这个变量就是动态的了,当这个变量被修改时,Compose UI 也能监听到它的变化并且更新 UI。

同样地,除了 mutableStateOf,还有 mutableStateListOfmutableStateMapOfmutableIntStateOf等。

虽然这个变量也是动态的,但是我们还可以对它监听:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    var greeting by remember { mutableStateOf("Hello $name!") }

    greeting += " Hello World!"

    LaunchedEffect(greeting) {
        println("Greeting text modified: $greeting")
    }

    Text(
        text = greeting,
        modifier = modifier
    )
}

这里的 LaunchedEffect 类似于 vue、react 中的 watchEffect,也就是监听某个变量的变化。

而当 LaunchedEffect(Unit) 无参的时候,就只在组件初始化时运行一次,而 UI 的更新并不会引起组件重新初始化。

常用变量

Context

现在还有一个问题,组件内部并不知道自己的 Context,但是组合式 UI 又无法直接获取到 Activity 的 Context。

显然 Compose 已经考虑到了这点,只需要在组件内使用 LocalContext.current 获取即可,得到的就是这个组件所在的 Context 对象。

CoroutineScope

有时候我们需要使用协程,但是 Android 并不推荐我们直接使用 GlobalScope
// 使用该注解抑制 IDE 警告
@OptIn(DelicateCoroutinesApi::class)
This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.

而 Compose 给我们提供了 rememberCoroutineScope() 方法便于我们得到与组件生命周期相关的 CoroutineScope,当组件结束生命周期,相关的协程也会立刻停止。

而这个 CoroutineScope 和 LaunchEffect 都是副作用 (Side Effect) API,下面是它们的区别:

LaunchEffectrememberCoroutineScope()
范围Composable 与 Composition 内在 Composable 外但在 Composition 内
使用场景当组件初始化或监听值改变时不依赖组件生命周期的事件,例如用户点击等

也就是说,如果你需要处理用户的点击事件,这个事件显然不能因为组件的生命周期结束而停止运行,可以使用 rememberCoroutineScope()。

创建 ViewModel

接下来就是跨组件的状态管理了,首先创建一个 ViewModel,以 SettingsActivityViewModel 为例:

import androidx.lifecycle.ViewModel

class SettingsActivityViewModel : ViewModel() {
}

你可以在里面保存这个 Activity 的状态,例如:

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class SettingsActivityViewModel : ViewModel() {
    private val _isDarkMode = MutableLiveData(false)
    val isDarkMode get() = _isDarkMode
    
    fun setDarkMode(bool: Boolean) {
        // this._isDarkMode.value = bool
        this._isDarkMode.postValue(bool)
    } 
}

这里添加了一个 LiveData 成员变量,这里的实现稍微麻烦了一点,因为我不希望外部组件直接调用 _isDarkMode,而是需要通过 setDarkMode() 函数来设置变量的值,而在 setDarkMode() 中可以做一些其他操作。

如果你确信一个状态变量只需要控制它的值而无需其他操作,也可以直接将 MutableLiveData<T> 公开。

使用 ViewModel

刚才在 ViewModel 中定义了一个变量,现在该如何在 Activity 和组件中使用呢?

获取 ViewModel

首先我们需要获取到这个 ViewModel 对象,下面有两种方式:

  1. 在 Activity 中获取
  2. 在 Composable 组件中获取
  3. 直接 new 一个 ViewModel 对象 (不推荐)

首先,在 Activity 中,直接用 ComponentActivity 提供的属性委托方法 viewModels() 即可,如下所示:

class SettingsActivity : ComponentActivity() {
    private val viewModel: SettingsActivityViewModel by viewModels()
    // ......
}

这样就直接以 SettingsActivity 的生命周期创建了这个 ViewModel。

接下来在组件中获取 ViewModel,方法也很简单,只需要给函数加一个参数即可:viewModel: SettingsActivityViewModel = viewModel() (当然你也可以写在函数体里面)

@Composable
fun Greeting(
    name: String, 
    modifier: Modifier = Modifier, 
    viewModel: SettingsActivityViewModel = viewModel()
)

通过这种方式得到的 ViewModel,其实就是以它的父组件为Owner 的 ViewModel,因此通过这两种方式获取到的 ViewModel 其实是同一个对象。(前提是这个 Greeting 是在 SettingsActivity 中使用的)

监听 LiveData

监听也非常简单,调用 LiveData<T>.observeAsState() 即可,返回的结果也是一个 MutableState。

val isDarkMode = viewModel.isDarkMode.observeAsState()
注意:请不要在 Composable 中使用来自 Activity 的 ViewModel

由于 Composable 组件是可组合的,因此它们实际上不属于任何 Activity,所以将 Composable 写在 Activity Class 内是不推荐的做法。但如果确实需要,避免使用 Activity 中通过委托 viewModels() 得到的 ViewModel,否则会导致 UI 预览渲染问题。

注意:通过 observeAsState() 得到的状态是单向的

如果找不到这个方法,请检查是否引入 LifeCycler 依赖:implementation(libs.androidx.lifecycle.runtime.ktx)

# libs.versions.toml
[versions]
lifecycleRuntimeKtx = "2.8.4"

[libraries]
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }

下面具体使用一下这个动态变量吧:

@Composable
fun Greeting(
    name: String,
    modifier: Modifier = Modifier,
    viewModel: SettingsActivityViewModel = viewModel()
) {
    val isDarkMode = viewModel.isDarkMode.observeAsState()

    var greeting by remember { mutableStateOf("Hello $name!") }

    LaunchedEffect(isDarkMode) {
        greeting += " Current theme is ${if (isDarkMode.value == true) "dark" else "light"}"
    }

    Text(
        text = greeting,
        modifier = modifier
    )
}

需要注意的是,从 ViewModel 获取到的值可能是 null,也就是说这里的 isDarkMode 实际上是 State<Boolean?>。

结束语

本期介绍了 Compose 中主题的使用以及组件状态管理,所有内容仅代表我个人理解,如有错误欢迎指出。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇