Compose
Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。
Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。此术语需要一些解释说明,它的含义对应用设计非常重要。
声明性编程范式
长期以来,Android 视图层次结构一直可以表示为界面 widget 树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态。
手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。
在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。
重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减少在这方面耗费的资源,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。
设置 Compose 编译器 Gradle 插件:
1、在 libs.versions.toml 文件中,移除对 Compose 编译器的所有引用
2、在“plugins”部分,添加以下新依赖项
[versions]
kotlin = "2.0.0"
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
// Add this line
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
1、 在项目的根 build.gradle.kts 文件中,将以下内容添加到“plugins”部分:
plugins {
// Existing plugins
alias(libs.plugins.compose.compiler) apply false
}
在使用 Compose 的每个模块中,应用该插件:
plugins {
// Existing plugins
alias(libs.plugins.compose.compiler)
}
将以下定义添加到应用的 build.gradle 文件中:
android {
buildFeatures {
compose true
}
}
最后,将以下部分中您需要的 Compose BoM 和 Compose 库依赖项的子集添加到您的依赖项:
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.10.01")
implementation(composeBom)
androidTestImplementation(composeBom)
// Choose one of the following:
// Material Design 3
implementation("androidx.compose.material3:material3")
// or Material Design 2
implementation("androidx.compose.material:material")
// or skip Material Design and build directly on top of foundational components
implementation("androidx.compose.foundation:foundation")
// or only import the main APIs for the underlying toolkit systems,
// such as input and measurement/layout
implementation("androidx.compose.ui:ui")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
// UI Tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
// Optional - Included automatically by material, only add when you need
// the icons but not the material library (e.g. when using Material3 or a
// custom design system based on Foundation)
implementation("androidx.compose.material:material-icons-core")
// Optional - Add full set of material icons
implementation("androidx.compose.material:material-icons-extended")
// Optional - Add window size utils
implementation("androidx.compose.material3.adaptive:adaptive")
// Optional - Integration with activities
implementation("androidx.activity:activity-compose:1.9.2")
// Optional - Integration with ViewModels
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5")
// Optional - Integration with LiveData
implementation("androidx.compose.runtime:runtime-livedata")
// Optional - Integration with RxJava
implementation("androidx.compose.runtime:runtime-rxjava2")
}
定义可组合函数
添加 @Composable使函数成为可组合函数。Composable 函数只能从其他 Composable 函数的范围内调用。
data class Message(var author: String,val body: String)
@Composable
fun MessageCard(msg: Message){
// Column { //vertical
// Text(text = msg.author)
// Text(text = msg.body)
// }
Row { // horizontal
Text(text = msg.author)
Text(text = msg.body)
}
// Box{ //stack
// Text(text = msg.author)
// Text(text = msg.body)
// }
}
添加预览函数,使用@Preview注解可以在 Android Studio 中预览可组合函数,而无需构建应用并将其安装到 Android 设备或模拟器中。该注解必须用于不接受参数的可组合函数。因此,您无法直接预览 MessageCard 函数,而是需要创建另一个名为 PreviewMessageCard 的函数,由该函数使用适当的参数调用 MessageCard。请在 @Composable 上方添加 @Preview 注解。
@Preview(showBackground = true)
@Composable
fun PreviewMessageCard(){
MessageCard(
msg = Message("LiLei","Body")
)
}
Column 函数可让您垂直排列元素。向 MessageCard 函数中添加一个 Column。使用 Row 水平排列各项,并使用 Box 堆叠元素。
@Composable
fun ShowImage(msg: Message){
Row {
Image(
painter = painterResource(R.drawable.pic),
contentDescription = "Contact profile picture"
)
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
}
配置布局
Compose 使用了修饰符。通过修饰符,您可以更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击。
@Composable
fun EnrichMessageCard(msg: Message){
//add padding around our message
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.pic),
contentDescription = "LiLi",
modifier = Modifier.size(40.dp)
.clip(CircleShape)
//add a border and set a color to the image
.border(1.5.dp,MaterialTheme.colorScheme.primary,CircleShape)
)
//add a horizontal space between image and column
Spacer(modifier = Modifier.width(8.dp))
Column{
Text(text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Surface(shape = MaterialTheme.shapes.medium,
shadowElevation = 2.dp) {
Text(text = msg.body,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(all = 4.dp)
)
}
}
}
}
预览不同的主题
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light mode")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode")
@Composable
fun DarkThemeEnrichMessageCardPreview(){
LiuHanzeKotlinTheme {
Surface {
EnrichMessageCard(msg = Message("Liu","DarkMode"))
}
}
}
列表和动画
使用LazyColumn和LazyRow创建列表。
可组合函数可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项)。这称为重组。
通过使用 Compose 的状态 API(如 remember 和 mutableStateOf),系统会在状态发生任何变化时自动更新界面。
可以根据点击消息时消息的 isExpanded 状态,更改消息内容的背景颜色。您将使用 clickable 修饰符来处理可组合项上的点击事件。您会为背景颜色添加动画效果,使其值逐步从 MaterialTheme.colorScheme.surface 更改为 MaterialTheme.colorScheme.primary(反之亦然),而不只是切换 Surface 的背景颜色。为此,您将使用 animateColorAsState 函数。最后,您将使用 animateContentSize 修饰符顺畅地为消息容器大小添加动画效果。
mutableStateOf 会创建可观察的 MutableState<T>,后者是与 Compose 运行时集成的可观察类型。
interface MutableState<T> : State<T> {
override var value: T
}
对 value 所做的任何更改都会安排对读取 value 的所有可组合函数进行重组。
虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改(例如:屏幕旋转)后保持状态。为此,您必须使用 rememberSaveable。rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。
val mutableState by rememberSaveable { mutableStateOf(default) }
添加到 Bundle 的所有数据类型都会自动保存。如果要保存无法添加到 Bundle 的内容,您有以下几种选择。
最简单的解决方案是向对象添加 @Parcelize 注解。对象将变为可打包状态并且可以捆绑。例如,以下代码会创建可打包的 City 数据类型并将其保存到状态。
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
MapSaver
ListSaver
注意:在 Compose 中将可变对象(如 ArrayList<T> 或 mutableListOf())用作状态会导致用户在您的应用中看到不正确或过时的数据。不可观察的可变对象(如 ArrayList 或可变数据类)不能由 Compose 观察,且在发生变化后不会触发重组。建议您使用可观察的数据存储器(如 State<List<T>>)和不可变的 listOf(),而不是使用不可观察的可变对象。
在可组合项中声明 MutableState 对象的方法有三种:
//by 委托语法需要以下导入:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
//remember的另外一种用法:
val text = remember( state.visualTransformation, state.annotatedString, ) {
state.visualTransformation.filter(state.annotatedString).text
}
//remember() 用来在 Compose 中“记住”某个计算结果,避免每次重组(recomposition)时重新计算。
//它有两个核心要点:
//若 remember() 的 关键参数(keys)没有变化
//→ 不会重新执行 block,而是使用上一次的结果。
//若 key 中任何一个发生变化
//→ remember 内的 lambda 会重新执行,产生新的值。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun EnrichMessageCard(msg: Message){
//add padding around our message
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.pic),
contentDescription = "LiLi",
modifier = Modifier.size(40.dp)
.clip(CircleShape)
//add a border and set a color to the image
.border(1.5.dp,MaterialTheme.colorScheme.primary,CircleShape)
)
//add a horizontal space between image and column
Spacer(modifier = Modifier.width(8.dp))
var isExpanded by remember { mutableStateOf(false) }
//surfaceColor will be updated gradually from one color to other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
)
Column(modifier = Modifier.clickable{isExpanded = !isExpanded}) {
Text(text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(4.dp))
Surface(shape = MaterialTheme.shapes.medium,
shadowElevation = 2.dp,
color = surfaceColor,
modifier = Modifier.animateContentSize().padding(1.dp)) {
Text(text = msg.body,
style = MaterialTheme.typography.bodyMedium,
maxLines = if(isExpanded) Int.MAX_VALUE else 1,
modifier = Modifier.padding(all = 4.dp)
)
}
}
}
}
@Composable
fun ConversationTest(messages: List<Message>){
LazyColumn {
items(messages){
EnrichMessageCard(it)
}
}
}
//调用
ConversationTest(SampleData.conversationSample)
屏幕自适应
使用 Compose Material 3 自适应库的 currentWindowAdaptiveInfo() 顶级函数计算应用的 WindowSizeClass。该函数会返回一个 WindowAdaptiveInfo 实例,其中包含 windowSizeClass。每当窗口大小类发生变化时,应用都会收到更新:
Jetpack Compose 是一种现代的声明式方法,用于构建自适应应用,而不会复制多个布局文件,而且消除了维护方面的负担。
Compose Material 3 自适应库包含用于管理窗口大小类别、导航组件、多窗格布局以及可折叠设备折叠状态和合页位置的可组合项,例如:
NavigationSuiteScaffold:根据应用窗口大小类和设备折叠状态,自动在导航栏和侧边导航栏之间切换。
ListDetailPaneScaffold:实现列表-详情规范布局。
根据应用窗口大小调整布局。在较大窗口大小类别中,在并排窗格中显示列表和列表项的详情;但在紧凑型和中等窗口大小类别中,只显示列表或详情。
SupportingPaneScaffold:实现辅助窗格规范布局。
在“较大”窗口大小类别中显示主要内容窗格和辅助窗格,但在“紧凑”和“中等”窗口大小类别中仅显示主要内容窗格。
Compose Material 3 自适应库是开发自适应应用的必备依赖项。
借助 Jetpack WindowManager 中的 WindowInfoTracker 接口,您可以获取设备的 DisplayFeature 对象列表。显示屏功能包括 FoldingFeature.State,用于指示设备是完全展开还是半开。
Compose Material 3 Adaptive 库提供了 currentWindowAdaptiveInfo() 顶级函数,该函数会返回包含 windowPosture 的 WindowAdaptiveInfo 实例。
用户通常会将外接键盘、触控板、鼠标和触控笔连接到大屏设备。这些外围设备可以提高用户的工作效率、输入精度、表达自我和提高无障碍功能。大多数 ChromeOS 设备都内置键盘和触控板。
自适应应用支持外部输入设备,但大部分工作是由 Android 框架为您完成的:
Jetpack Compose 1.7 及更高版本:默认支持键盘 Tab 键导航以及鼠标或触控板点击、选择和滚动。
Jetpack androidx.compose.material3 库:让用户能够使用触控笔向任何 TextField 组件写入内容。
键盘快捷键帮助程序:让用户能够发现 Android 平台和应用键盘快捷键。通过替换 onProvideKeyboardShortcuts() 窗口回调,在键盘快捷键帮助程序中发布应用的键盘快捷键。
为了全面支持各种尺寸的外形规格,自适应应用支持所有类型的输入。
重组
可组合函数符合跳过条件,除非:
函数的返回值类型不是 Unit
函数带有 @NonRestartableComposable 或 @NonSkippableComposable 注解
必需参数的类型不稳定
某种类型必须符合以下协定,才能被视为稳定类型:
对于相同的两个实例,其 equals 的结果将始终相同。
如果类型的某个公共属性发生变化,组合将收到通知。
所有公共属性类型也都是稳定。
有这样一些归入此协定的重要通用类型,即使未使用 @Stable 注解来显式标记为稳定的类型,Compose 编译器也会将其视为稳定的类型。
所有基元值类型:Boolean、Int、Long、Float、Char 等。
字符串
所有函数类型 (lambda)
所有这些类型都可以遵循稳定协定,因为它们是不可变的。由于不可变类型绝不会发生变化,它们就永远不必通知组合更改方面的信息,因此遵循该协定就容易得多。
Compose 的 MutableState 类型是一种众所周知稳定但可变的类型。如果 MutableState 中存储了值,状态对象整体会被视为稳定对象,因为 State 的 .value 属性如有任何更改,Compose 就会收到通知。
当作为参数传递到可组合项的所有类型都很稳定时,系统会根据可组合项在界面树中的位置来比较参数值,以确保相等性。如果所有值自上次调用后未发生变化,则会跳过重组。
Compose 仅在可以证明稳定的情况下才会认为类型是稳定的。例如,接口通常被视为不稳定类型,并且具有可变公共属性的类型(实现可能不可变)的类型也被视为不稳定类型。
如果 Compose 无法推断类型是否稳定,但您想强制 Compose 将其视为稳定类型,请使用 @Stable 注解对其进行标记。
// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasError: Boolean
get() = exception != null
}
CompositionLocal
CompositionLocal 是一种在 Jetpack Compose 中通过 组合(composition)来管理和提供全局数据的方法。它可以用于传递应用程序的配置、主题信息、语言环境、用户信息等,避免了层层传递参数的麻烦。
CompositionLocal 本质上是 Compose 中的一种 作用域本地存储,数据在组件树中是局部可访问的,而不需要显式传递给每一个组件。
// 定义一个 CompositionLocal 用来传递用户的名称
val LocalUserName = compositionLocalOf { "Guest" }
@Composable
fun AppContent() {
// 使用 CompositionLocalProvider 提供局部数据
CompositionLocalProvider(LocalUserName provides "John Doe") {
UserProfile() // 在这里访问 LocalUserName
}
}
@Composable
fun UserProfile() {
// 访问 LocalUserName
val userName = LocalUserName.current
Text("Hello, $userName!")
}
//CompositionLocal 可以嵌套使用。内层的 CompositionLocalProvider 会覆盖外层的值。
@Composable
fun ParentComponent() {
CompositionLocalProvider(LocalExampleData provides "Outer Value") {
ChildComponent() // 访问 "Outer Value"
CompositionLocalProvider(LocalExampleData provides "Inner Value") {
ChildComponent() // 访问 "Inner Value"
}
}
}
//CompositionLocal 可以用于传递 ViewModel 或其他依赖。
// 定义 CompositionLocal
val LocalViewModel = compositionLocalOf<MyViewModel> { error("No ViewModel provided!") }
// 提供 ViewModel
@Composable
fun App() {
val viewModel = viewModel<MyViewModel>()
CompositionLocalProvider(LocalViewModel provides viewModel) {
Screen()
}
}
// 使用 ViewModel
@Composable
fun Screen() {
val viewModel = LocalViewModel.current
val data = viewModel.data.collectAsState()
Text(text = "Data from ViewModel: ${data.value}")
}
NavController 实现页面跳转
首先,确保在你的 build.gradle 文件中添加了 navigation-compose 依赖:
dependencies {
implementation "androidx.navigation:navigation-compose:2.8.6" // 请使用最新版本
}
一个简单的例子
// HomeScreen
@Composable
fun HomeScreen(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Home Screen", style = MaterialTheme.typography.h4)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { navController.navigate("detail") }) {
Text(text = "Go to Detail Screen")
}
}
}
// DetailScreen
@Composable
fun DetailScreen(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Detail Screen", style = MaterialTheme.typography.h4)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { navController.popBackStack() }) {
Text(text = "Go Back to Home")
}
}
}
在 MainActivity 中,设置 NavController 并使用 NavHost 定义导航图:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavControllerExampleApp()
}
}
}
@Composable
fun NavControllerExampleApp() {
val navController = rememberNavController() // 创建 NavController
NavHost(
navController = navController,
startDestination = "home" // 设置起始目的地
) {
// 定义导航图
composable("home") { // HomeScreen 的路由
HomeScreen(navController = navController)
}
composable("detail") { // DetailScreen 的路由
DetailScreen(navController = navController)
}
}
}
//可以通过路由传递参数。例如,从 HomeScreen 传递一个字符串到 DetailScreen:
// 修改导航图
composable("detail/{message}") { backStackEntry ->
val message = backStackEntry.arguments?.getString("message")
DetailScreen(navController = navController, message = message)
}
// 修改导航操作
navController.navigate("detail/Hello from HomeScreen")
路由参数传递
路由的使用分为 3 步:配置导航图 → 跳转路由 → 提取参数。
1. 第一步:在导航图中配置路由
用 Review.route(带占位符的模板)和 Review.arguments(参数配置)注册路由,关联 ReviewScreen:
// 可以为不同屏幕添加独特逻辑
object Review : Screen("review/{itemId}") {
fun createRoute(itemId: Long) = "review/$itemId"
fun extractItemId(backStackEntry: NavBackStackEntry): Long {
return backStackEntry.arguments?.getLong("itemId") ?: -1L
}
val arguments = listOf(
navArgument("itemId") { type = NavType.LongType }
)
}
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
// 其他路由...
// 配置 Review 路由
composable(
route = Review.route, // 传入带占位符的路由模板:review/{itemId}
arguments = Review.arguments // 传入参数配置:itemId 是 Long 类型
) { backStackEntry ->
// 提取参数(调用封装好的 extractItemId 方法)
val itemId = Review.extractItemId(backStackEntry)
// 跳转到复习页面,传入提取到的 itemId
ReviewScreen(itemId = itemId, navController = navController)
}
}
}
2. 第二步:从其他页面跳转至 Review 路由
用 Review.createRoute(itemId) 生成 “带实际参数的路由字符串”,调用 navController.navigate() 跳转:
// 示例:在 HomeScreen 中点击某个记忆项,跳转至 ReviewScreen
@Composable
fun HomeScreen(navController: NavHostController) {
Button(
onClick = {
val targetItemId = 123L // 实际要传递的参数(如从数据库查询得到)
// 生成实际路由:review/123
val route = Review.createRoute(targetItemId)
// 跳转路由
navController.navigate(route)
}
) {
Text("复习记忆项 123")
}
}
3. 第三步:在 ReviewScreen 中使用参数
通过 extractItemId 提取的 itemId,可以用于查询数据、渲染页面:
@Composable
fun ReviewScreen(itemId: Long, navController: NavHostController) {
// 用 itemId 查询对应的记忆项(如从 Room 数据库获取)
val memoryItem = remember { viewModel.getMemoryItemById(itemId) }
Column {
Text(text = "复习项 ID:$itemId")
Text(text = "标题:${memoryItem?.title ?: "无"}")
// 其他页面内容...
}
}
如果路由有多个参数(如 detail/{id}/{title}),扩展 Review 的写法即可,示例:
object Detail : Screen("detail/{id}/{title}") {
// 多参数构造路由
fun createRoute(id: Long, title: String) = "detail/$id/$title"
// 提取多个参数
fun extractId(backStackEntry: NavBackStackEntry): Long {
return backStackEntry.arguments?.getLong("id") ?: -1L
}
fun extractTitle(backStackEntry: NavBackStackEntry): String {
return backStackEntry.arguments?.getString("title") ?: "未知标题"
}
// 多参数配置
val arguments = listOf(
navArgument("id") { type = NavType.LongType },
navArgument("title") { type = NavType.StringType }
)
}