刚从 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
,还有 mutableStateListOf
、mutableStateMapOf
、mutableIntStateOf
等。
虽然这个变量也是动态的,但是我们还可以对它监听:
@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
// 使用该注解抑制 IDE 警告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.
@OptIn(DelicateCoroutinesApi::class)
而 Compose 给我们提供了 rememberCoroutineScope() 方法便于我们得到与组件生命周期相关的 CoroutineScope,当组件结束生命周期,相关的协程也会立刻停止。
而这个 CoroutineScope 和 LaunchEffect 都是副作用 (Side Effect) API,下面是它们的区别:
LaunchEffect | rememberCoroutineScope() | |
范围 | 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 对象,下面有两种方式:
- 在 Activity 中获取
- 在 Composable 组件中获取
- 直接 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,所以将 Composable 写在 Activity Class 内是不推荐的做法。但如果确实需要,避免使用 Activity 中通过委托 viewModels() 得到的 ViewModel,否则会导致 UI 预览渲染问题。
如果找不到这个方法,请检查是否引入 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 中主题的使用以及组件状态管理,所有内容仅代表我个人理解,如有错误欢迎指出。