Android Jetpack Compose基础

Compose 简介

Jetpack Compose 是推荐用于构建原生 Android 界面的声明式UI新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用,具有以下特点:

  1. 更少的代码:编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分,代码更简洁,出现 bug 的可能性也更小

  2. 直观:使用声明性 API,只需描述界面,Compose 会负责完成其余工作,应用状态变化时,界面会自动更新

  3. 加速开发:Compose 与现有代码兼容,您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose

  4. 功能强大:直接访问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")

在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)
  }) {

 }
相关推荐
一航jason8 天前
Android Jetpack Compose 现有Java老项目集成使用compose开发
android·java·android jetpack
帅次9 天前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
IAM四十二11 天前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
Wgllss13 天前
那些大厂架构师是怎样封装网络请求的?
android·架构·android jetpack
x0241 个月前
Android Room(SQLite) too many SQL variables异常
sqlite·安卓·android jetpack·1024程序员节
alexhilton1 个月前
深入理解观察者模式
android·kotlin·android jetpack
Wgllss1 个月前
花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
android·性能优化·android jetpack
上官阳阳1 个月前
使用Compose创造有趣的动画:使用Compose共享元素
android·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-15.属性变化控制Service
android·android studio·android jetpack
IAM四十二1 个月前
Android Jetpack Core
android·android studio·android jetpack