Jetpack Compose 围绕 Kotlin 构建而成。在某些情况下,Kotlin 提供了一些特殊的惯用语,这使编写良好的 Compose 代码变得更容易。如果您用另一种编程语言思考,并在头脑中将这种语言翻译成 Kotlin,很可能会错失 Compose 的一些优势,还可能会发现很难理解以惯用语编写的 Kotlin 代码。进一步熟悉 Kotlin 的样式可帮助您避免这些隐患。
默认参数
编写 Kotlin 函数时,您可以指定函数参数的默认值;如果调用方未明确传递相应的值,系统就会使用这些默认值。此功能减少了对重载函数的需求。
例如,假设您要编写一个绘制正方形的函数。该函数可能有一个必需参数 side,用于指定每条边的长度。它可能有几个可选参数,如 thickness、edgeColor、fill 等等;如果调用方未指定这些参数的值,该函数就会使用默认值。在其他语言中,您可能需要编写多个函数:
// We don't need to do this in Kotlin!
void drawSquare(int side) {...}
void drawSquare(int side, int thickness) {...}
void drawSquare(int side, int thickness, Color edgeColor ) {...}
// …
在 Kotlin 中,您可以编写一个函数并指定参数的默认值:
fun drawSquare(
side: Int, thickness: Int = 2,
edgeColor: Color = Color.Black,
fill: Color = Color.White
) { ... }
除了让您不必编写多个冗余函数之外,此功能还会使代码读起来更清晰。如果调用方未指定参数的值,这表明它们愿意使用默认值。此外,命名参数可让您更容易看到发生了什么。如果您查看代码并看到这样的函数调用,在不检查 drawSquare()
代码的情况下,您可能不知道参数的含义:
drawSquare(30, 5, Color.Red);
相比之下,以下代码具有自描述性:
drawSquare(side = 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 表达式在该位置定义该函数。前面的示例假设在其他位置定义了 myClickFunction()
。但是,如果您只在此处使用该函数,则使用 lambda 表达式以内嵌方式定义该函数会更简单:
Button( // ...
onClick = {
// do something
// do something else
}
) { /*...*/ }
尾随 lambda
Kotlin 提供了一种特殊语法来调用最后一个参数为 lambda 的高阶函数。如果您要将一个 lambda 表达式作为该参数传递,您可以使用尾随 lambda 语法。您应将 lambda 表达式放在圆括号后面,而不是将其放在圆括号内。这是 Compose 中的一种常见情况,因此您需要熟悉代码是什么样子的。
例如,所有布局的最后一个参数(如 Column()
可组合函数)均为 children
,它是一个发出子界面元素的函数。假设您想要创建一个包含三个文本元素的列,并且需要应用某种格式设置。以下代码行得通,但它非常繁琐:
Column(
modifier = Modifier.padding(16.dp),
content = {
Text("Some text")
Text("Some more text")
Text("Last text")
}
)
由于 children
参数是函数签名中的最后一个参数,并且我们要将其值作为 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
布局可组合项时,系统会自动在 RowScope
中调用内容 lambda。这样一来,Row
就可以提供仅在 Row
中有效的功能。以下示例演示了 Row
如何为 gravity
修饰符提供行专用值:
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()
}
其他代码可以访问此类属性,所用代码如下:
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 函数中看到这样的代码:
ConstraintLayout {
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
属性都包含当前主题背景的值。