Jetpack Compose 基于 Kotlin 构建。在某些情况下,Kotlin 提供了一些特殊的习惯用法,使编写良好的 Compose 代码变得更容易。如果您使用其他编程语言进行思考,并将该语言在脑海中转换为 Kotlin,那么您可能会错过 Compose 的一些优势,并且可能难以理解习惯性编写的 Kotlin 代码。更熟悉 Kotlin 的风格可以帮助您避免这些陷阱。
默认参数
编写 Kotlin 函数时,您可以为函数参数指定默认值,如果调用方没有显式传递这些值,则使用这些值。此功能减少了对重载函数的需求。
例如,假设您想编写一个绘制正方形的函数。该函数可能只有一个必需的参数 sideLength,指定每条边的长度。它可能还有几个可选参数,如 thickness、edgeColor 等;如果调用方未指定这些参数,则函数使用默认值。在其他语言中,您可能希望编写多个函数
// We don't need to do this in Kotlin! void drawSquare(int sideLength) { } void drawSquare(int sideLength, int thickness) { } void drawSquare(int sideLength, int thickness, Color edgeColor) { }
在 Kotlin 中,您可以编写一个函数并为参数指定默认值
fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { }
除了让您不必编写多个冗余函数之外,此功能还可以使您的代码更易于阅读。如果调用方未为参数指定值,则表示他们愿意使用默认值。此外,命名参数使更容易查看正在发生的事情。如果您查看代码并看到如下函数调用,则可能无法在不检查 drawSquare()
代码的情况下了解参数的含义
drawSquare(30, 5, Color.Red);
相比之下,此代码具有自文档功能
drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)
大多数 Compose 库使用默认参数,并且在您编写的可组合函数中也建议这样做。此做法使您的可组合函数可自定义,但仍然使默认行为易于调用。因此,例如,您可以创建如下所示的简单文本元素
Text(text = "Hello, Android!")
该代码与以下更冗长的代码具有相同的效果,在该代码中,更多地显式设置了Text
参数
Text( text = "Hello, Android!", color = Color.Unspecified, fontSize = TextUnit.Unspecified, letterSpacing = TextUnit.Unspecified, overflow = TextOverflow.Clip )
第一个代码段不仅更简单、更易于阅读,而且还具有自文档功能。通过仅指定 text
参数,您记录了对于所有其他参数,您希望使用默认值。相比之下,第二个代码段暗示您希望显式设置这些其他参数的值,尽管您设置的值恰好是函数的默认值。
高阶函数和 lambda 表达式
Kotlin 支持高阶函数,即接收其他函数作为参数的函数。Compose 基于此方法构建。例如,Button
可组合函数提供了一个 onClick
lambda 参数。该参数的值是一个函数,当用户单击按钮时,按钮会调用该函数
Button( // ... onClick = myClickFunction ) // ...
高阶函数与lambda 表达式自然地配对,lambda 表达式求值为函数。如果您只需要使用一次函数,则无需在其他地方定义它以将其传递给高阶函数。相反,您只需使用 lambda 表达式在该处定义函数即可。前面的示例假设 myClickFunction()
在其他地方定义。但是,如果您仅在此处使用该函数,则使用 lambda 表达式内联定义函数会更简单
Button( // ... onClick = { // do something // do something else } ) { /* ... */ }
尾随 lambda
Kotlin 提供了一种特殊的语法来调用其最后一个参数为 lambda 的高阶函数。如果要将 lambda 表达式作为该参数传递,可以使用尾随 lambda 语法。您无需将 lambda 表达式放在括号内,而是将其放在后面。这在 Compose 中很常见,因此您需要熟悉代码的外观。
例如,所有布局的最后一个参数(例如 Column()
可组合函数)是 content
,一个发出子 UI 元素的函数。假设您想创建一个包含三个文本元素的列,并且需要应用一些格式。此代码可以工作,但它非常繁琐
Column( modifier = Modifier.padding(16.dp), content = { Text("Some text") Text("Some more text") Text("Last text") } )
因为 content
参数是函数签名中的最后一个参数,并且我们将其值作为 lambda 表达式传递,所以我们可以将其从括号中提取出来
Column(modifier = Modifier.padding(16.dp)) { Text("Some text") Text("Some more text") Text("Last text") }
这两个示例具有完全相同的含义。花括号定义传递给 content
参数的 lambda 表达式。
实际上,如果您传递的唯一参数是该尾随 lambda(即,如果最后一个参数是 lambda,并且您没有传递任何其他参数),则可以完全省略括号。因此,例如,假设您不需要向 Column
传递修饰符。您可以像这样编写代码
Column { Text("Some text") Text("Some more text") Text("Last text") }
此语法在 Compose 中非常常见,尤其是在 Column
等布局元素中。最后一个参数是一个 lambda 表达式,用于定义元素的子元素,这些子元素在函数调用后的花括号中指定。
作用域和接收者
某些方法和属性仅在特定作用域中可用。有限的作用域允许您在需要的地方提供功能,并避免意外地在不合适的地方使用该功能。
考虑 Compose 中使用的示例。当您调用 Row
布局可组合函数时,您的内容 lambda 会自动在 RowScope
中调用。这使 Row
能够公开仅在 Row
内有效的函数。以下示例演示了 Row
如何为 align
修饰符公开了特定于行的值
Row { Text( text = "Hello world", // This Text is inside a RowScope so it has access to // Alignment.CenterVertically but not to // Alignment.CenterHorizontally, which would be available // in a ColumnScope. modifier = Modifier.align(Alignment.CenterVertically) ) }
某些 API 接受在接收者作用域中调用的 lambda。这些 lambda 可以访问在其他地方定义的属性和函数,具体取决于参数声明
Box( modifier = Modifier.drawBehind { // This method accepts a lambda of type DrawScope.() -> Unit // therefore in this lambda we can access properties and functions // available from DrawScope, such as the `drawRectangle` function. drawRect( /*...*/ /* ... ) } )
有关更多信息,请参阅 Kotlin 文档中的带有接收者的函数文字。
委托属性
Kotlin 支持委托属性。这些属性的调用方式与字段相同,但其值由动态计算表达式决定。您可以通过 by
语法的使用来识别这些属性
class DelegatingClass { var name: String by nameGetterFunction() // ... }
其他代码可以使用如下代码访问该属性
val myDC = DelegatingClass() println("The name property is: " + myDC.name)
当 println()
执行时,将调用 nameGetterFunction()
返回字符串的值。
这些委托属性在处理状态支持的属性时特别有用
var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true
解构数据类
如果您定义了一个数据类,则可以使用解构声明轻松访问数据。例如,假设您定义了一个 Person
类
data class Person(val name: String, val age: Int)
如果您有该类型的对象,则可以使用如下代码访问其值
val mary = Person(name = "Mary", age = 35) // ... val (name, age) = mary
您经常会在 Compose 函数中看到这种代码
Row { val (image, title, subtitle) = createRefs() // The `createRefs` function returns a data object; // the first three components are extracted into the // image, title, and subtitle variables. // ... }
数据类提供了许多其他有用的功能。例如,当您定义数据类时,编译器会自动定义有用的函数,如 equals()
和 copy()
。您可以在数据类文档中找到更多信息。
单例对象
Kotlin 使声明单例变得容易,单例始终只有一个实例。这些单例使用object
关键字声明。Compose 经常使用此类对象。例如,MaterialTheme
被定义为一个单例对象;MaterialTheme.colors
、shapes
和 typography
属性都包含当前主题的值。
类型安全的构建器和 DSL
Kotlin 允许使用类型安全的构建器创建特定领域语言 (DSL)。DSL 允许以更易于维护和阅读的方式构建复杂的分层数据结构。
Jetpack Compose 在一些 API 中使用了 DSL,例如 LazyRow
和 LazyColumn
。
@Composable fun MessageList(messages: List<Message>) { LazyColumn { // Add a single item as a header item { Text("Message List") } // Add list of messages items(messages) { message -> Message(message) } } }
Kotlin 使用 带接收者的函数字面量 来保证类型安全的构建器。如果我们以 Canvas
可组合项为例,它接收一个函数作为参数,该函数以 DrawScope
作为接收者,onDraw: DrawScope.() -> Unit
,允许代码块调用在 DrawScope
中定义的成员函数。
Canvas(Modifier.size(120.dp)) { // Draw grey background, drawRect function is provided by the receiver drawRect(color = Color.Gray) // Inset content by 10 pixels on the left/right sides // and 12 by the top/bottom inset(10.0f, 12.0f) { val quadrantSize = size / 2.0f // Draw a rectangle within the inset bounds drawRect( size = quadrantSize, color = Color.Red ) rotate(45.0f) { drawRect(size = quadrantSize, color = Color.Blue) } } }
在 Kotlin 文档 中了解更多关于类型安全构建器和 DSL 的信息。
Kotlin 协程
协程在 Kotlin 中提供了语言级别的异步编程支持。协程可以在不阻塞线程的情况下挂起执行。响应式 UI 本质上是异步的,Jetpack Compose 通过在 API 层级拥抱协程而不是使用回调来解决这个问题。
Jetpack Compose 提供了可在 UI 层安全使用协程的 API。 rememberCoroutineScope
函数返回一个 CoroutineScope
,您可以使用它在事件处理程序中创建协程并调用 Compose 挂起 API。请参见下面使用 ScrollState
的 animateScrollTo
API 的示例。
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Create a new coroutine that scrolls to the top of the list // and call the ViewModel to load data composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }
默认情况下,协程按顺序执行代码块。调用挂起函数的正在运行的协程会挂起其执行,直到挂起函数返回。即使挂起函数将执行转移到不同的 CoroutineDispatcher
,情况也是如此。在前面的示例中,loadData
只有在挂起函数 animateScrollTo
返回后才会执行。
要并发执行代码,需要创建新的协程。在上面的示例中,要将滚动到屏幕顶部和从 viewModel
加载数据并行化,需要两个协程。
// Create a CoroutineScope that follows this composable's lifecycle val composableScope = rememberCoroutineScope() Button( // ... onClick = { // Scroll to the top and load data in parallel by creating a new // coroutine per independent work to do composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }
协程使组合异步 API 变得更容易。在以下示例中,我们将 pointerInput
修饰符与动画 API 结合使用,以便在用户点击屏幕时为元素的位置设置动画。
@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier .fillMaxSize() .pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to // where the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) }
要了解有关协程的更多信息,请查看 Android 上的 Kotlin 协程 指南。
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- Material 组件和布局
- Compose 中的副作用
- Compose 布局基础