Jetpack Compose 是以 Kotlin 建構而成,在某些情況下,Kotlin 提供專屬的慣用語,讓您輕鬆撰寫撰寫內容程式碼。如果您使用另一種程式語言思考並將該語言翻譯成 Kotlin,您可能會錯過 Compose 的一些優勢,並且可能會發現難以理解以慣用方式編寫的 Kotlin 代碼。熟悉 Kotlin 的樣式,可協助您避免這些錯誤。
預設引數
編寫 Kotlin 函式時,您可以指定 函式引數的預設值,在呼叫者未明確傳送這些值時使用。這項功能可減少超載函式的需求。
舉例來說,假設您編寫的函式需要繪製正方形。該函式可能含有一個必要參數 sideLength,用於指定每一邊的長度。該參數可能會有數個選用參數,例如 厚度、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 運算式定義函式。上例假設 myClickFunction()
已在其他位置定義。但如果在這裡僅使用該函式,那麼直接以 lambda 運算式內嵌內嵌函式會比較簡單:
Button(
// ...
onClick = {
// do something
// do something else
}
) { /* ... */ }
結尾的 lambda
Kotlin 提供呼叫高階函式的特殊語法,其中 last 參數為 lambda。如要將 lambda 運算式當做參數傳遞,您可以使用交易 lambda 語法。您應將 lambda 運算式放在括號中,然後再接上運算式。這是 Compose 的常見情況,因此您必須熟悉程式碼的外觀。
舉例來說,所有版面配置的最後一個參數 (例如 Column()
可組合函式) 是會發送子 UI 元素函式的 content
。假設您想要建立包含三個文字元素的資料欄,而且必須套用一些格式。這個程式碼有效,但相當麻煩:
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 運算式,這些函式的呼叫會在括號後以大括號指定。
範圍和接收器
部分方法和屬性僅適用於特定範圍。受限制範圍可讓您適時提供需要的功能,並避免在不合適的情況下使用誤用的功能。
假設撰寫時使用範例。當您呼叫 Row
版面配置可組合時,系統會自動在 RowScope
中叫用內容 lambda。這個指令可讓 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 函式通常會顯示這類程式碼:
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
屬性都包含目前主題的值。
字體安全建構工具和 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)
}
}
}
如要進一步瞭解類型安全建構工具和 DSL,請參閱 Kotlin 說明文件。
Kotlin Coroutines
Coroutines 以 Kotlin 的語言層級提供非同步程式設計支援。處理常式能夠「暫停」執行,而不封鎖執行緒。回應式 UI 本身就是非同步,而 Jetpack Compose 會在 API 層級 (而非使用回呼) 採用協同程式來解決。
而 Jetpack Compose 提供的 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
,也是如此。在上一個例子中,除非暫停函式 animateScrollTo
傳回,否則系統不會執行 loadData
。
如要同時執行程式碼,必須建立新的日常安排。在上面的範例中,如要平行捲動至畫面上方並載入 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)
)
}
}
如要進一步瞭解 Coroutines,請參閱 Android 上的 Kotlin 協同程式指南。