適用於 Jetpack Compose 的 Kotlin

Jetpack Compose 是以 Kotlin 建構而成,在某些情況下,Kotlin 可提供專屬的慣用語,助您輕鬆撰寫品質良好的 Compose 程式碼。如果您考慮使用另一種程式語言並將該語言轉換成 Kotlin,您可能會錯過 Compose 的一些優勢,並且可能會發現難以理解透過慣用語編寫的 Kotlin 代碼。熟悉 Kotlin 的樣式,可協助您避免產生這些錯誤。

預設引數

編寫 Kotlin 函式時,您可以指定「函式引數的預設值」,在呼叫者未明確傳送這些值時使用。這項功能可減少對超載函式的需求。

例如,假設您想要編寫可繪製正方形的函式。該函式可能必須包含一個參數 sideLength,用於指定每一邊的長度。該參數可能包含數個選用參數,例如 thicknessedgeColor 等;如果呼叫端未指定這些參數,則函式會使用預設值。在其他語言中,您可能需要編寫數個函式:

// 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() 可組合函式) 的最後一個參數是 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 版面配置可組合函式時,系統會自動在 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 函式中經常會顯示這類程式碼:

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.colorsshapestypography 屬性都包含目前主題的值。

型別安全建構工具和 DSL

Kotlin 支援使用型別安全建構工具建立網域特定語言 (DSL)。DSL 可助您以更容易維護及更容易讀取的方式建構複雜的階層式資料結構。

Jetpack Compose 在某些 API (例如 LazyRowLazyColumn) 中採用 DSL。

@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 協同程式

Kotlin 協同程式在語言層級提供非同步程式設計支援。協同程式能夠「暫停」執行函式,而不封鎖執行緒。回應式 UI 本身的性質為非同步,Jetpack Compose 解決這個問題的方法是在 API 級別採用協同程式,而不使用回呼。

您可以運用 Jetpack Compose 提供的 API,在 UI 層中安全地使用協同程式。rememberCoroutineScope 函式會傳回 CoroutineScope,您可以借助該函式建立協同程式,並呼叫 Compose 暫停 API。請參閱下列使用 ScrollStateanimateScrollTo 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 修飾符號與 Animation 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 協同程式指南。