適用於 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() 可組合函式) 是會輸出子項 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 運算式,這些子項會在函式呼叫後,用大括號指定。

範圍和接收器

部分方法和屬性僅適用於特定範圍。受限制範圍可讓您適時提供需要的功能,並避免在不合適的情況下誤用相關功能。

考慮在 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 修飾符與動畫 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 協同程式指南。