適用於 Jetpack Compose 的 Kotlin

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

字體安全建構工具和 DSL

Kotlin 提供使用安全類型建構工具的網域專屬語言 (DSL)。DSL 可讓您以更維護且可讀取的方式建構複雜的階層資料結構。

Jetpack Compose 在某些 API 中採用 DSL,例如 LazyRowLazyColumn

@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。請參閱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)
        )
    }
}

如要進一步瞭解 Coroutines,請參閱 Android 上的 Kotlin 協同程式指南。