Compose 简介
Jetpack Compose 是推荐用于构建原生 Android 界面的声明式UI新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用,具有以下特点:
-
更少的代码:编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分,代码更简洁,出现 bug 的可能性也更小
-
直观:使用声明性 API,只需描述界面,Compose 会负责完成其余工作,应用状态变化时,界面会自动更新
-
加速开发:Compose 与现有代码兼容,您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose
-
功能强大:直接访问Android平台API,内置对Material Design、深色主题、动画等支持
命令式UI VS 声明式UI
- 命令式UI 需要主动让UI更新,使用
findViewById()
等函数遍历树,并通过调用button.setText(String)
、container.addChild(View)
或img.setImageBitmap(Bitmap)
等方法更改节点。手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图,维护成本会大于声明式UI - 声明式UI 数据驱动界面变化,只需要把界面给「声明」出来,而不需要手动更新,只要声明的数据发生了变化,UI就跟着变化。
Compose和XML+Activity/Fragment对比
以一个小需求为例:显示一个文本并为它设置动态内容。可以看到Compose的代码会少很多。
- Compose
kotlin
@Composable
fun ComposeSample() {
val text="文本内容"
Text(text = text)
}
- XML+Activity/Fragment
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_contentwrap_content"/>
</LinearLayout>
kotlin
val textView = findViewById<TextView>(R.id.tv_text)
textView.text="文本内容"
可组合函数
使用@Composable注解的函数,以大驼峰命名函数名,不需要返回值,旨在将数据转换为界面。
kotlin
//只显示一行文字的可组合函数
@Composable
fun TextSample(text:String){
Text(text)
}
可组合函数只能在其他可组合函数中调用
在ComponentActivity有一个setContent扩展方法,接收一个可组合函数参数,作为界面入口
kotlin
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
.....
}
class ComposeTestActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TextSample("简单的可组合函数")
}
}
}
效果如下:
状态(State)
只有状态发生改变,可组合函数才会进行重组操作
如下代码,我们想在点击按钮的时候把按钮的文字改成,但是点击的时候文字却没有发生改变
kotlin
var text = "文本不会变更的按钮"
Button(onClick = { text="变更后的文本" }) {
Text(text = text)
}
使用mutableStateOf()创建一个可观察MutableState类型的状态,通过修改改状态的value就可以触发重组
在可组合函数中,可以用以下三种方式创建可观察类型,以下三种创建方式是等效的
kotlin
val text1 = remember {
mutableStateOf("文本会变更的按钮")
}
Button(onClick = { text1.value="变更后的文本" }) {
Text(text = text1.value)
}
//也可以使用by 委托
var text2 by remember {
mutableStateOf("文本会变更的按钮")
}
Button(onClick = { text2="变更后的文本" }) {
Text(text = text2)
}
//或者
val (text3,setText3) = remember {
mutableStateOf("文本会变更的按钮")
}
Button(onClick = { setText3("变更后的文本") }) {
Text(text = text3)
}
在 Compose 中恢复状态
在compose中使用remember可以在重组时保存状态,但是当重新创建 activity 或进程后,状态会丢失
下面是当屏幕发生旋转后,我们看到从竖屏切换到横屏时按钮的显示又恢复到了初始状态
在重新创建 activity 或进程后,可以使用 rememberSaveable 恢复界面状态
基础类型(可以保存到Bundle的内容)直接用即可
kotlin
var text4 by rememberSaveable {
mutableStateOf("变更前的文本")
}
无法保存到 Bundle
的内容,可用下面三种方式:
1.实现Parcelable
kotlin
@Parcelize
data class City(val name: String, val country: String) : Parcelable
var selectedCity by rememberSaveable {
mutableStateOf(City("广州", "中国"))
}
2.MapSaver
kotlin
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
3.ListSaver
kotlin
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
想到了什么呢? 用ViewModel当做状态容器!
状态提升
- 单一可信来源:通过移动状态,而不是复制状态,我们可确保只有一个可信来源。这有助于避免 bug。
- 封装:只有有状态可组合项能够修改其状态。这完全是内部的。
- 可共享 :可与多个可组合项共享提升的状态。如果想在另一个可组合项中执行
name
操作,可以通过变量提升来做到这一点。 - 可拦截:无状态可组合项的调用方可以在更改状态之前决定忽略或修改事件。
- 解耦:状态可以存储在任何位置。
有状态的可组合函数:
kotlin
@Composable
fun Status() {
var text by rememberSaveable {
mutableStateOf("有状态的可组合函数")
}
Button(onClick = { text = "有状态的可组合函数变更" }) {
Text(text = text)
}
}
无状态的可组合函数:
kotlin
@Composable
fun NoStatus(text:String,onClick: () -> Unit) {
Button(onClick = { onClick() }) {
Text(text = text)
}
}
//状态提升到了调用它的地方
var noStatusText by remember {
mutableStateOf("文本会变更的按钮")
}
NoStatus(text = noStatusText) {
noStatusText= "无状态的可组合函数变更"
}
重组
在上面可组合函数中,如果参数text发生了改变,那么该组合函数会被重新执行,并将新的参数渲染到界面,这个过程称为"重组"
- 可组合函数可以按任何顺序执行。
- 可组合函数可以并行执行。
- 重组会跳过尽可能多的可组合函数和 lambda。
- 重组是乐观的操作,可能会被取消。
- 可组合函数可能会像动画的每一帧一样非常频繁地运行。
只要可组合函数引用的状态发生改变就会重组
kotlin
@Composable
fun Recomposition() {
var text by remember {
mutableStateOf("初始值")
}
LogUtil.e("Recomposition重组了")
RecompositionChild(text)
RecompositionChild1()
Button(onClick = { text = "修改了" }) {
Text(text = "修改值")
}
}
kotlin
@Composable
fun RecompositionChild(text: String) {
LogUtil.e("RecompositionChild重组了")
Text(text = text)
}
@Composable
fun RecompositionChild1() {
LogUtil.e("RecompositionChild1重组了")
Text(text = "无状态变化的")
}
上面的代码,点击按钮后会打印:
可以看到Recomposition和RecompositionChild发生了重组,而RecompositionChild1没有发生重组,因为RecompositionChild1没有状态发生变化。
但是为什么Recomposition会发生重组呢?因为它也引用了text,即便在界面上没有显示,以它当做实参也会被认为引用了text。
尽可能延迟读取
上面例子中Recomposition在点击按钮时发生重组不符合预期,因为Recomposition中没有显示和text相关的内容,应该只重组RecompositionChild就够了。 可以使用lambda延迟读取:
kotlin
fun Recomposition() {
var text1 by remember {
mutableStateOf("初始值")
}
RecompositionChild2 {
return@RecompositionChild2 text1
}
Button(onClick = { text1 = "修改了" }) {
Text(text = "修改Text1值")
}
}
kotlin
@Composable
fun RecompositionChild2(provideText: () -> String) {
LogUtil.e("RecompositionChild2重组了")
Text(text = provideText())
}
添加额外信息以促进智能重组
Compose会使用执行顺序来区分实例
以下面的代码为例,点击一个按钮,在加入一项在列表的起始位置:
kotlin
data class Item(
val id: String = UUID.randomUUID().toString(),
val text: String,
val type: String
)
@Composable
fun RecompositionSample() {
//数据列表
val items = remember {
val items = mutableStateListOf<Item>()
for (i in 0..3) {
items.add(
Item(
text = "第$i 个", type = ""
)
)
}
items
}
Column {
//点击按钮在列表第一个添加一项
Button(onClick = {
items.add(0, Item(text = "添加的一项", type = ""))
}) {
Text("添加一项")
}
items.forEach {
RecompositionItemSample(item = it)
}
}
}
@Composable
fun RecompositionItemSample(item: Item) {
LogUtil.e(item)
Text(text = item.text)
}
点击之后,界面显示没有问题,但是看子项打印的数据:
明明只添加了一项,其他的也都发生了重组,是因为顺序发生了改变,把第N项都变成了第N+1项,导致无法识别。
添加额外的key可以帮助Compose识别:
kotlin
Column {
//点击按钮在列表第一个添加一项
Button(onClick = {
items.add(0, Item(text = "添加的一项", type = ""))
}) {
Text("添加一项")
}
items.forEach {
key(it.id) {
RecompositionItemSample(item = it)
}
}
}
打印情况:
输入未更改,则跳过重组
如果组合中已有可组合项,当所有输入都处于稳定状态且没有变化时,可以跳过重组。
稳定类型必须符合以下协定:
- 对于相同的两个实例,其
equals
的结果将始终相同。 - 如果类型的某个公共属性发生变化,组合将收到通知。
- 所有公共属性类型也都是稳定。
Compose 仅在可以证明稳定的情况下才会认为类型是稳定的。例如,接口通常被视为不稳定类型,并且具有可变公共属性的类型(实现可能不可变)的类型也被视为不稳定类型。
在上面的例子中,我们把Item的text属性改成var可变的
kotlin
data class Item(
val id: String = UUID.randomUUID().toString(),
var text: String,
val type: String
)
点击按钮的时候每一项都会重组:
如果Compose 无法推断类型是否稳定,但想强制 Compose 将其视为稳定类型,使用 @Stable
注解对其进行标记:
kotlin
@Stable
data class Item(
val id: String = UUID.randomUUID().toString(),
var text: String,
val type: String
)
打印情况:
ViewModel重组陷阱
使用 derivedStateOf 限制重组
修饰符Modifier
修饰符可以执行以下操作:
- 更改可组合项的大小、布局、行为和外观
- 添加信息,如无障碍标签
- 处理用户输入
- 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放
kotlin
@Composable
fun ModifierSample() {
Box(
modifier = Modifier
.height(300.dp)//高度
.width(200.dp)//宽度
.padding(16.dp)//边距
.background(color = Color.Red, shape = CircleShape.copy(all = CornerSize(8.dp)))//背景
.border(
width = 1.dp,
color = Color.Blue,
shape = CircleShape.copy(all = CornerSize(8.dp))
)//边框
.clickable {
//todo
}//可点击
.padding(16.dp)//内边框
.background(color = Color.Green)//内背景
) {
Image(
painter = painterResource(R.drawable.bg_empty), contentDescription = "示例",
modifier = Modifier.size(80.dp).align(Alignment.Center),
)
}
}
修饰符顺序
由于每个函数都会对上一个函数返回的 Modifier
进行更改,因此顺序会影响最终结果
如上面的例子把clickable放到最后,那么可点击的区域将只有绿色部分
kotlin
@Composable
fun ModifierSample() {
Box(
modifier = Modifier
.height(300.dp)//高度
.width(200.dp)//宽度
.padding(16.dp)//边距
.background(color = Color.Red, shape = CircleShape.copy(all = CornerSize(8.dp)))//背景
.border(
width = 1.dp,
color = Color.Blue,
shape = CircleShape.copy(all = CornerSize(8.dp))
)//边框
.padding(16.dp)//内边框
.background(color = Color.Green)//内背景
.clickable {
//todo
}//可点击
) {
Image(
painter = painterResource(R.drawable.bg_empty), contentDescription = "示例",
modifier = Modifier.size(80.dp).align(Alignment.Center),
)
}
}
Compose 中的类型安全
有些修饰符仅适用于某些可组合项的子项
提取和重复使用修饰符
有助于提高代码可读性,还有助于提升应用性能,原因如下:
- 对使用修饰符的可组合项进行重组时,不会重新分配修饰符
- 修饰符链可能很长并且非常复杂,因此重复使用相同的链实例可以减轻 Compose 运行时在对比时所需的工作量
- 这种提取方式可提高整个代码库中的代码简洁性、一致性和可维护性
布局
标准布局组件
Column、Row、Box
Material 组件和布局
ConstraintLayout
约束布局
列表和网格
LazyColumn、LazyRow、LazyVerticalGrid 、 LazyHorizontalGrid
kotlin
data class Item(
val id: String = UUID.randomUUID().toString(),
val text: String,
val type: String
)
@HiltViewModel
class ComposeViewModel @Inject constructor() : ViewModel() {
//有状态的列表
val items = mutableStateListOf<Item>()
}
@Composable
private fun ListSample() {
val viewModel: ComposeViewModel = hiltViewModel()
LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
Button(onClick = { viewModel.addItem() }) {
Text(text = "加一项")
}
}
items(viewModel.items, key = {
it.id
}, contentType = {
it.type
}) { item ->
ItemSample(item)
}
}
}
@Composable
fun ItemSample(item: Item) {
if (item.type == "default") {
var state by rememberSaveable {
mutableStateOf(item.text)
}
TextField(value = state, onValueChange = {
state = it
})
} else {
Text(text = item.text, style = MaterialTheme.typography.titleMedium)
}
}
内容边距和间距
边距:contentPadding
间距:verticalArrangement/verticalArrangement = Arrangement.spacedBy()
项键
为每个列表项提供一个稳定的唯一键,为 key
参数提供一个块。提供稳定的键可使项状态在发生数据集更改后保持一致
contentType
当提供 contentType
时,Compose 只能在相同类型的项之间重复使用组合
避免嵌套可向同一方向滚动的组件
CompositionLocal
在 Compose 中,数据以参数形式向下流经整个界面树传递给每个可组合函数。这会使可组合项的依赖项变为显式依赖项。但是,对于广泛使用的常用数据,这可能会很麻烦:
假设有一个需求:应用有多个模块,在每个模块的某几个地方需要把模块名字显示,若这些地方都离模块入口有很多层的距离,如果以参数的方式显式传递,那么这些中间层就都需要接收并传递这个参数。
Compose 提供了 CompositionLocal,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式。
使用CompositionLocal
以上面的需求为例
1.使用 compositionLocalOf 或者 staticCompositionLocalOf 定义LocalModule(名字随便,APP是默认值):
kotlin
val LocalModule = compositionLocalOf { "APP" }
2.使用CompositionLocalProvider 为各个CompositionLocal赋值
kotlin
Column {
//模块1
CompositionLocalProvider(LocalModule provides "Home") {
JustABox()
}
//地图模块
CompositionLocalProvider(LocalModule provides "Map") {
JustABox()
}
}
3.使用CompositionLocal
kotlin
//加一层
@Composable
fun JustABox() {
LogUtil.e("没有使用的地方")
Box {
CompositionLocalUse()
}
}
@Composable
fun CompositionLocalUse() {
LogUtil.e("实际使用用的地方")
Text(text = LocalModule.current)
}
最终效果:
compositionLocalOf 和 staticCompositionLocalOf
compositionLocalOf:在重组期间更改提供的值只会使读取其 current 值的内容无效。
staticCompositionLocalOf:与 compositionLocalOf 不同,Compose 不会跟踪 staticCompositionLocalOf 的读取。更改该值会导致提供 CompositionLocal 的整个 content lambda 被重组,而不仅仅是在组合中读取 current 值的位置。
把上面的代码改成如下:
kotlin
@Composable
fun CompositionLocalSample() {
var module1 by remember {
mutableStateOf("Home")
}
Column {
//模块1
CompositionLocalProvider(LocalModule provides module1) {
JustABox()
}
//地图模块
CompositionLocalProvider(LocalModule provides "Map") {
JustABox()
}
Button(onClick = { module1 = "Mine" }) {
Text(text = "修改第一个CompositionLocal")
}
}
}
使用compositionLocalOf的时候,点击按钮会打印:
而使用staticCompositionLocalOf的时候会打印:
可以看到,当使用staticCompositionLocalOf时,整个被CompositionLocalProvider包裹的可组合项都会重组,而使用compositionLocalOf时只会重组真正使用CompositionLocal的地方
如果为 CompositionLocal 提供的值发生更改的可能性微乎其微或永远不会更改,使用 staticCompositionLocalOf 可提高性能。
使用CompositionLocal实现屏幕适配
compose使用dp为单位,渲染到屏幕时将dp转为px:
px=dp*density
假设设计稿宽度为360dp,可以将density设置为设备宽度/360,180.dp就是屏幕宽度的一半:
kotlin
val fontScale = LocalDensity.current.fontScale
val displayMetrics = LocalContext.current.resources.displayMetrics
val widthPixels = displayMetrics.widthPixels
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 360f,
fontScale = fontScale
)
) {
Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.secondary).width(180.dp).height(30.dp))
}
Material主题
Jetpack Compose 提供了 Material Design 的一种实现方案,通过主题设置轻松地赋予应用一致的外观和风格
注:下面的都是material3的API
通过调用MaterialTheme方法可以使用Material主题
kotlin
@Composable
fun MaterialTheme(
colorScheme: ColorScheme = MaterialTheme.colorScheme,//颜色
shapes: Shapes = MaterialTheme.shapes,//形状
typography: Typography = MaterialTheme.typography,//字体样式
content: @Composable () -> Unit
) {
...
}
以颜色为例
定义颜色
kotlin
//白天模式的颜色
val md_theme_light_primary = Color(0xFF006a60)
val md_theme_light_onPrimary = Color(0xFFffffff)
val md_theme_light_primaryContainer = Color(0xFF73f7e8)
val md_theme_light_onPrimaryContainer = Color(0xFF00201c)
val md_theme_light_secondary = Color(0xFF4a635f)
val md_theme_light_onSecondary = Color(0xFFffffff)
val md_theme_light_secondaryContainer = Color(0xFFcce8e3)
val md_theme_light_onSecondaryContainer = Color(0xFF051f1d)
val md_theme_light_tertiary = Color(0xFF46617a)
val md_theme_light_onTertiary = Color(0xFFffffff)
val md_theme_light_tertiaryContainer = Color(0xFFcbe5ff)
val md_theme_light_onTertiaryContainer = Color(0xFF001d33)
val md_theme_light_error = Color(0xFFba1b1b)
val md_theme_light_errorContainer = Color(0xFFffdad4)
val md_theme_light_onError = Color(0xFFffffff)
val md_theme_light_onErrorContainer = Color(0xFF410001)
val md_theme_light_background = Color(0xFFfafdfb)
val md_theme_light_onBackground = Color(0xFF191c1c)
val md_theme_light_surface = Color(0xFFfafdfb)
val md_theme_light_onSurface = Color(0xFF191c1c)
val md_theme_light_surfaceVariant = Color(0xFFdae5e2)
val md_theme_light_onSurfaceVariant = Color(0xFF3f4947)
val md_theme_light_outline = Color(0xFF6f7977)
val md_theme_light_inverseOnSurface = Color(0xFFeff1ef)
val md_theme_light_inverseSurface = Color(0xFF2d3130)
val md_theme_light_inversePrimary = Color(0xFF51dbcc)
val md_theme_light_shadow = Color(0xFF000000)
//黑夜模式的颜色
val md_theme_dark_primary = Color(0xFF51dbcc)
val md_theme_dark_onPrimary = Color(0xFF003731)
val md_theme_dark_primaryContainer = Color(0xFF005048)
val md_theme_dark_onPrimaryContainer = Color(0xFF73f7e8)
val md_theme_dark_secondary = Color(0xFFb1ccc7)
val md_theme_dark_onSecondary = Color(0xFF1c3532)
val md_theme_dark_secondaryContainer = Color(0xFF324b47)
val md_theme_dark_onSecondaryContainer = Color(0xFFcce8e3)
val md_theme_dark_tertiary = Color(0xFFadc9e6)
val md_theme_dark_onTertiary = Color(0xFF16334a)
val md_theme_dark_tertiaryContainer = Color(0xFF2e4961)
val md_theme_dark_onTertiaryContainer = Color(0xFFcbe5ff)
val md_theme_dark_error = Color(0xFFffb4a9)
val md_theme_dark_errorContainer = Color(0xFF930006)
val md_theme_dark_onError = Color(0xFF680003)
val md_theme_dark_onErrorContainer = Color(0xFFffdad4)
val md_theme_dark_background = Color(0xFF191c1c)
val md_theme_dark_onBackground = Color(0xFFe0e3e1)
val md_theme_dark_surface = Color(0xFF191c1c)
val md_theme_dark_onSurface = Color(0xFFe0e3e1)
val md_theme_dark_surfaceVariant = Color(0xFF3f4947)
val md_theme_dark_onSurfaceVariant = Color(0xFFbec9c6)
val md_theme_dark_outline = Color(0xFF899391)
val md_theme_dark_inverseOnSurface = Color(0xFF191c1c)
val md_theme_dark_inverseSurface = Color(0xFFe0e3e1)
val md_theme_dark_inversePrimary = Color(0xFF006a60)
val md_theme_dark_shadow = Color(0xFF000000)
val seed = Color(0xFF51a49a)
val error = Color(0xFFba1b1b)
val LightThemeColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
)
val DarkThemeColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
)
传入colorScheme
kotlin
@Composable
fun ThemeSample() {
//黑夜或者白天 //也可以用isSystemInDarkTheme()获取系统的模式
var isDark by rememberSaveable {
mutableStateOf(false)
}
val colors = if (isDark) {
DarkThemeColors
} else {
LightThemeColors
}
MaterialTheme(
colorScheme = colors,
typography = AppTypography,
) {
Column {
Button(onClick = { isDark = !isDark }) {
Text(text = "切换模式")
}
Text(text = "示例", color = MaterialTheme.colorScheme.primary)
}
}
}
多主题切换
参照上面定义颜色,定义多组颜色
RedDarkThemeColors、RedLightThemeColors
BlueDarkThemeColors、BlueLightThemeColors
kotlin
var themeType by rememberSaveable {
mutableStateOf("Blue")
}
val colors = when (themeType) {
"Blue" -> {
if (isDark) {
BlueDarkThemeColors
} else {
BlueLightThemeColors
}
}
else -> {
if (isDark) {
RedDarkThemeColors
} else {
RedLightThemeColors
}
}
}
要修改主题,修改themeType值即可
自定义主题
1.定义颜色和形状等
kotlin
data class CustomColors(
//标题颜色
val titleTextColor: Color
)
data class CustomShape(val bigShape:Shape) )
2.使用CompositionLocal创建默认颜色CustomColors
kotlin
val LocalCustomColors = staticCompositionLocalOf {
CustomColors(
titleTextColor = Color.Unspecified,
)
}
3.定义主题
kotlin
object CustomTheme {
val colors: CustomColors
@Composable
get() = LocalCustomColors.current
//todo
//val shape:CustomShape
}
4.自定义主题可组合函数入口
kotlin
@Composable
fun CustomTheme(
customColors: CustomColors = CustomTheme.colors,
//customShapes: CustomShape = CustomTheme.colors,
content: @Composable () -> Unit
) {
CompositionLocalProvider(LocalCustomColors provides customColors) {
content()
}
}
5.在App入口处使用自定义主题
kotlin
MaterialTheme(
colorScheme = colors,
typography = AppTypography,
) {
CustomTheme(customColors = CustomColors(titleTextColor = Color.Red)) {
Text(color = CustomTheme.colors.titleTextColor, text = "")
}
}
副作用
LaunchedEffect:
在某个可组合项的作用域内运行挂起函数
rememberCoroutineScope:
获取组合感知作用域,以便在可组合项外启动协程
导航Navigation
所有屏幕都是可组合项的情况:
在可组合项层次结构中的适当位置创建 NavController
kotlin
val navController = rememberNavController()
创建 NavHost
kotlin
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
导航到可组合项
kotlin
navController.navigate("friendslist")
混合应用-使用基于 fragment 的 Navigation 组件
在Compose入口Fragment获取NavController
kotlin
val fragmentNavController:NavController = findNavController()
将NavController向下传递到合适的位置
kotlin
@Composable
fun MainScreen fragmentNavController: NavController) {
/**/
}
导航到其他Fragment
kotlin
Button(onClick = {
fragmentNavController.navigate(R.id.action_mainFragment_to_composeTestActivity)
}) {
}