Jetpack Compose 基础知识

1. 准备工作

Jetpack Compose 是一种现代化的工具包,旨在简化 UI 开发。它将响应式编程模型与 Kotlin 编程语言的简洁性和易用性相结合。它完全是声明性的,这意味着您可以通过调用一系列将数据转化为 UI 层次结构的函数来描述 UI。当底层数据发生变化时,框架会自动重新执行这些函数,为您更新 UI 层次结构。

Compose 应用由可组合函数构成,这些函数只是用 @Composable 标记的普通函数,它们可以调用其他可组合函数。函数是创建新 UI 组件所需的全部内容。该注解告知 Compose 向函数添加特殊支持,以便随着时间的推移更新和维护 UI。Compose 可让您将代码构建成小块。可组合函数通常简称为“composables”。

通过创建可复用的小型 composable,您可以轻松构建应用中使用的 UI 元素库。每个元素负责屏幕的一个小部分,并且可以独立编辑。

如需更多支持以帮助您完成此 Codelab,请参阅以下代码演练

注意:代码演练使用的是 Material 2,而本 Codelab 已更新为使用 Material 3。请注意,有些步骤会有所不同。

前提条件

  • 熟悉 Kotlin 语法,包括 lambda

您将执行的操作

在本 Codelab 中,您将学习

  • 什么是 Compose
  • 如何使用 Compose 构建 UI
  • 如何在可组合函数中管理状态
  • 如何创建高性能列表
  • 如何添加动画
  • 如何为应用设置样式和主题

您将构建一个包含 onboarding 屏幕和动画展开列表项的应用

8d24a786bfe1a8f2.gif

您将需要

2. 启动新的 Compose 项目

要启动新的 Compose 项目,请打开 Android Studio。

如果您位于 Welcome to Android Studio 窗口中,请点击 Start a new Android Studio project。如果您已经打开了一个 Android Studio 项目,请从菜单栏中依次选择 File > New > New Project

对于新项目,请从可用模板中选择 Empty Activity

d12472c6323de500.png

点击 Next 并照常配置您的项目,将其命名为“Basics Codelab”。请确保您选择的 minimumSdkVersion 至少为 API 级别 21,这是 Compose 支持的最低 API。

选择 Empty Activity 模板时,项目会为您生成以下代码

  • 项目已配置为使用 Compose。
  • 已创建 AndroidManifest.xml 文件。
  • build.gradle.ktsapp/build.gradle.kts 文件包含 Compose 所需的选项和依赖项。

同步项目后,打开 MainActivity.kt 并查看代码。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

在下一部分,您将看到每个方法的作用,以及如何改进它们以创建灵活且可重用的布局。

Codelab 解决方案

您可以从 GitHub 获取本 Codelab 解决方案的代码

$ git clone https://github.com/android/codelab-android-compose

或者,您可以将仓库下载为 Zip 文件

您可以在 BasicsCodelab 项目中找到解决方案代码。我们建议您按照自己的节奏一步步完成本 Codelab,并在认为必要时查看解决方案。在本 Codelab 中,您将看到需要添加到项目中的代码片段。

3. Compose 入门

浏览 Android Studio 为您生成的与 Compose 相关的不同类和方法。

可组合函数

可组合函数是使用 @Composable 注解标记的普通函数。这使得您的函数能够在其内部调用其他 @Composable 函数。您可以看到 Greeting 函数是如何被标记为 @Composable 的。此函数将生成一段 UI 层次结构,用于显示给定的输入(String)。Text 是库提供的可组合函数。

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

在 Android 应用中使用 Compose

使用 Compose 时,Activity 仍然是 Android 应用的入口点。在我们的项目中,当用户打开应用时,会启动 MainActivity(如 AndroidManifest.xml 文件中所指定)。您可以使用 setContent 定义布局,但与传统 View 系统中使用 XML 文件不同,您可以在其中调用 Composable 函数。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                  modifier = Modifier.fillMaxSize(),
                  color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme 是一种为 Composable 函数设置样式的方式。您将在“为应用设置主题”部分详细了解此内容。要查看文本如何在屏幕上显示,您可以在模拟器或设备中运行应用,或使用 Android Studio 预览功能。

要使用 Android Studio 预览功能,只需使用 @Preview 注解标记任何无参数的可组合函数或具有默认参数的函数,然后构建您的项目。您可以在 MainActivity.kt 文件中看到一个 Preview Composable 函数。您可以在同一文件中拥有多个预览,并为其命名。

@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

fb011e374b98ccff.png

如果选择了 Code eeacd000622ba9b.png,预览可能不会出现。点击 Split 7093def1e32785b2.png 查看预览。

4. 调整 UI

我们首先为 Greeting 设置不同的背景颜色。您可以通过用 Surface 包装 Text composable 来实现此目的。Surface 可以设置颜色,因此请使用 MaterialTheme.colorScheme.primary

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

嵌套在 Surface 中的组件将绘制在该背景颜色的上方。

您可以在预览中看到新更改

c88121ec49bde8c7.png

您可能错过了一个重要细节:文本现在是白色的。这是何时定义的?

您没有定义!Material 组件(例如 androidx.compose.material3.Surface)旨在处理您可能希望应用中拥有的常见功能(例如为文本选择合适的颜色),从而为您提供更好的体验。我们说 Material 是有“主见”的,因为它提供了大多数应用通用的良好默认值和模式。Compose 中的 Material 组件构建在其他基础组件(位于 androidx.compose.foundation 中)之上,如果您需要更大的灵活性,您的应用组件也可以访问这些基础组件。

在这种情况下,Surface 知道当背景设置为 primary 颜色时,其上的任何文本都应该使用 onPrimary 颜色,该颜色也在主题中定义。您可以在“为应用设置主题”部分详细了解此内容。

修饰符

大多数 Compose UI 元素(例如 SurfaceText)都接受可选的 modifier 参数。修饰符告诉 UI 元素如何在其父布局中布局、显示或表现。您可能已经注意到 Greeting composable 已经有一个默认修饰符,该修饰符随后被传递给 Text

例如,padding 修饰符将在它装饰的元素周围应用一定量的空间。您可以使用 Modifier.padding() 创建一个 padding 修饰符。您还可以通过链式调用添加多个修饰符,因此在本例中,我们可以将 padding 修饰符添加到默认修饰符中:modifier.padding(24.dp)

现在,为屏幕上的 Text 添加 padding

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

有几十种修饰符可用于对齐、动画、布局、使其可点击或可滚动、转换等。有关完整列表,请参阅Compose 修饰符列表。您将在接下来的步骤中使用其中一些修饰符。

5. 重用 composables

向 UI 添加的组件越多,创建的嵌套层级就越多。如果函数变得非常大,这可能会影响可读性。通过创建可重用的小组件,可以轻松构建应用中使用的 UI 元素库。每个元素负责屏幕的一个小部分,并且可以独立编辑。

最佳实践是,您的函数应包含一个 Modifier 参数,该参数默认分配为空 Modifier。将此修饰符转发给您在函数内调用的第一个 composable。这样,调用 site 可以从可组合函数外部调整布局指令和行为。

创建一个名为 MyApp 的 Composable,其中包含问候语。

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

这样,您可以清理 onCreate 回调和预览,因为现在您可以重用 MyApp composable,避免代码重复。

在预览中,让我们调用 MyApp 并移除预览的名称。

您的 MainActivity.kt 文件应如下所示

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. 创建列和行

Compose 中三个基本的标准布局元素是 ColumnRowBox

518dbfad23ee1b05.png

它们是接受 Composable 内容的 Composable 函数,因此您可以在其中放置项目。例如,Column 内的每个子项都将垂直放置。

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

现在尝试更改 Greeting,使其显示包含两个文本元素的列,如本例所示

bf27ee688c3231df.png

请注意,您可能需要移动 padding。

将您的结果与此解决方案进行比较

import androidx.compose.foundation.layout.Column
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose 和 Kotlin

可组合函数可以像 Kotlin 中的任何其他函数一样使用。这使得构建 UI 非常强大,因为您可以添加语句来影响 UI 的显示方式。

例如,您可以使用 for 循环向 Column 添加元素

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

a7ba2a8cb7a7d79d.png

您尚未设置 composables 的尺寸或添加任何约束,因此每行都会占用最小空间,预览也是如此。让我们更改预览以模拟小屏幕手机的常见宽度,即 320dp。像这样向 @Preview 注解添加 widthDp 参数

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

a5d5f6cdbdd918a2.png

修饰符在 Compose 中广泛使用,所以让我们通过一个更高级的练习进行实践:尝试使用 fillMaxWidthpadding 修饰符来复制以下布局。

a9599061cf49a214.png

现在将您的代码与解决方案进行比较

import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

请注意

  • 修饰符可以重载,因此例如您可以指定不同的方式来创建 padding。
  • 要向元素添加多个修饰符,只需将它们链式调用即可。

有多种方法可以实现此结果,因此如果您的代码与此代码段不匹配,并不表示您的代码有误。但是,请复制并粘贴此代码以继续本 Codelab。

添加按钮

在下一步中,您将添加一个可点击元素来展开 Greeting,因此我们需要先添加该按钮。目标是创建以下布局

ff2d8c3c1349a891.png

Button 是 material3 软件包提供的一个 composable,它将一个 composable 作为最后一个参数。由于尾随 lambda 可以移到括号外面,因此您可以将任何内容作为子项添加到按钮中。例如,一个 Text

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

要实现此目的,您需要学习如何将 composable 放置在行的末尾。没有 alignEnd 修饰符,因此您可以将一些 weight 赋予开头的 composable。weight 修饰符使元素填充所有可用空间,使其具有*弹性*,有效地推开其他没有 weight 的元素(称为*非弹性*)。它还会使 fillMaxWidth 修饰符变得多余。

现在尝试添加按钮并将其放置在上一张图片所示的位置。

在此处查看解决方案

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Compose 中的状态

在本部分,您将为屏幕添加一些交互功能。到目前为止,您创建的都是静态布局,但现在您将使其对用户更改做出反应以实现此效果

6675d41779cac69.gif

在探讨如何使按钮可点击以及如何调整项目大小之前,您需要在某个地方存储一个值,该值指示每个项目是否已展开(即项目的状态)。由于每个问候语都需要一个这样的值,因此逻辑上应该将其放在 Greeting composable 中。请看这个 expanded 布尔值及其在代码中的使用方式

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

请注意,我们还添加了 onClick 操作和动态按钮文本。稍后会详细介绍。

但是,这不会按预期工作。为 expanded 变量设置不同的值不会使 Compose 将其检测为*状态更改*,因此什么也不会发生。

变异此变量不会触发重组的原因是 Compose 未对其进行跟踪。此外,每次调用 Greeting 时,变量都会被重置为 false。

要向 composable 添加内部状态,可以使用 mutableStateOf 函数,这会使 Compose 重组读取该 State 的函数。

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

但是,您不能直接将 mutableStateOf 赋值给 composable 内的变量。如前所述,重组可能随时发生,这将再次调用 composable,并将状态重置为值为 false 的新 mutable state。

为了在重组之间保留状态,请使用 remember *记住* mutable state。

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember 用于防止重组,因此状态不会重置。

请注意,如果您从屏幕的不同部分调用同一个 composable,您将创建不同的 UI 元素,每个元素都有其自己的状态版本。您可以将内部状态视为类中的私有变量。

可组合函数将自动“订阅”状态。如果状态发生变化,读取这些字段的 composables 将被重组以显示更新。

变异状态和对状态变化的反应

为了改变状态,您可能已经注意到 Button 有一个名为 onClick 的参数,但它不接受值,它接受一个函数

您可以通过为其分配lambda 表达式来定义*点击时*执行的操作。例如,我们切换展开状态的值,并根据该值显示不同的文本。

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

以互动模式运行应用以查看行为。

374998ad358bf8d6.png

点击按钮时,expanded 会切换,从而触发按钮内文本的重组。每个 Greeting 都保持其自己的展开状态,因为它们属于不同的 UI 元素。

93d839b53b7d9bea.gif

截至目前的代码

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

展开项目

现在,让我们在请求时实际展开项目。添加一个取决于我们状态的附加变量

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

您无需记住 extraPadding 以防重组,因为它只进行简单的计算。

现在我们可以为 Column 应用新的 padding 修饰符

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

如果您在模拟器或互动模式下运行,您会看到每个项目都可以独立展开

6675d41779cac69.gif

8. 状态提升

在可组合函数中,被多个函数读取或修改的状态应位于一个共同的祖先中,这个过程称为状态提升。*提升 (hoist)* 的意思是*举起*或*抬高*。

使状态可提升可避免重复状态和引入 bug,有助于重用 composables,并使 composables 更容易测试。相反,不需要由 composable 的父级控制的状态不应被提升。事实真相的来源属于创建和控制该状态的任何人。

例如,我们为应用创建一个 onboarding 屏幕。

5d5f44508fcfa779.png

将以下代码添加到 MainActivity.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false } 
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

此代码包含许多新特性

  • 您添加了一个新的 composable,名为 OnboardingScreen,还添加了一个新的预览。如果您构建项目,您会注意到您可以同时拥有多个预览。我们还添加了一个固定高度,以验证内容是否正确对齐。
  • Column 可以配置为在其内容显示在屏幕中央。
  • shouldShowOnboarding 使用 by 关键字代替 =。这是一个属性委托,可以省去您每次输入 .value 的麻烦。
  • 点击按钮时,shouldShowOnboarding 被设置为 false,但是您还没有从任何地方读取该状态。

现在我们可以将这个新的 onboarding 屏幕添加到我们的应用中。我们希望在启动时显示它,然后在用户按下“Continue”时隐藏它。

在 Compose 中,您不会隐藏 UI 元素。相反,您只是不将它们添加到 composition 中,因此它们不会添加到 Compose 生成的 UI 树中。您可以使用简单的条件 Kotlin 逻辑来执行此操作。例如,要显示 onboarding 屏幕或问候语列表,您可以这样做

// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}

但是,我们无法访问 shouldShowOnboarding。很明显,我们需要将我们在 OnboardingScreen 中创建的状态与 MyApp composable 共享。

与其以某种方式与父级共享状态值,不如我们将状态提升——简单地将其移至需要访问它的共同祖先。

首先,将 MyApp 的内容移至名为 Greetings 的新 composable 中。同时调整预览以调用 Greetings 方法

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

为新的顶级 MyApp composable 添加预览,以便我们可以测试其行为

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

现在添加逻辑以在 MyApp 中显示不同的屏幕,并提升状态。

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

我们还需要与 onboarding 屏幕共享 shouldShowOnboarding,但我们不打算直接传递它。与其让 OnboardingScreen 变异我们的状态,不如让它在我们单击 *Continue* 按钮时通知我们。

如何向上传递事件?通过向下传递回调。回调是作为参数传递给其他函数并在事件发生时执行的函数。

尝试向 onboarding 屏幕添加一个函数参数,定义为 onContinueClicked: () -> Unit,以便您可以从 MyApp 变异状态。

解决方案

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {


    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

通过向 OnboardingScreen 传递一个函数而不是一个状态,我们使这个 composable 更具可重用性,并保护状态不被其他 composables 变异。总的来说,这使事情变得简单。一个很好的例子是现在如何修改 onboarding 预览以调用 OnboardingScreen

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

onContinueClicked 赋值给一个空的 lambda 表达式意味着“什么都不做”,这非常适合预览。

这越来越像一个真正的应用了,做得好!

25915eb273a7ef49.gif

MyApp composable 中,我们第一次使用了 by 属性委托,以避免每次都使用 value。让我们也在 Greeting composable 的 expanded 属性中使用 by 而不是 =。确保将 expandedval 更改为 var

截至目前完整代码

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. 创建高性能惰性列表

现在让我们让姓名列表更真实。到目前为止,您已在 Column 中显示了两个问候语。但是,它能处理成千上万个吗?

Greetings 参数中的默认列表值更改为使用另一个列表构造函数,该构造函数允许设置列表大小并使用其 lambda 中包含的值填充它(此处 $it 表示列表索引)

names: List<String> = List(1000) { "$it" }

这会创建 1000 个问候语,即使是那些不适合屏幕的问候语也会创建。显然,这没有性能。您可以尝试在模拟器上运行它(警告:此代码可能会使您的模拟器冻结)。

要显示可滚动的列,我们使用 LazyColumnLazyColumn 仅渲染屏幕上可见的项目,从而在大列表渲染时提高性能。

在其基本用法中,LazyColumn API 在其作用域内提供一个 items 元素,可以在其中编写单个项目的渲染逻辑

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" } 
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

284f925eb984fb56.gif

10. 持久化状态

我们的应用有两个问题

持久化 onboarding 屏幕状态

如果您在设备上运行应用,点击按钮然后旋转,onboarding 屏幕会再次显示。remember 函数仅在 composable 保留在 Composition 中时才有效。当您旋转时,整个 Activity 会重启,因此所有状态都会丢失。任何配置更改和进程死亡时也会发生这种情况。

除了使用 remember,您还可以使用 rememberSaveable。这将保存每个状态,使其在配置更改(例如旋转)和进程死亡后仍然存在。

现在将 shouldShowOnboarding 中使用的 remember 替换为 rememberSaveable

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

运行、旋转、更改为深色模式或杀死进程。除非您之前退出过应用,否则 onboarding 屏幕不会显示。

持久化列表项的展开状态

如果您展开某个列表项,然后滚动列表直至该项目移出视图,或者旋转设备然后返回到已展开的项目,您会看到该项目现在已恢复到其初始状态。

解决此问题的方法是对展开状态也使用 rememberSaveable

   var expanded by rememberSaveable { mutableStateOf(false) }

到目前为止,您已经使用了大约 120 行代码,就能够显示一个长且性能良好的滚动列表,每个项目都拥有自己的状态。此外,您还可以看到,您的应用无需额外的代码行就可以拥有完全正确的深色模式。您稍后会学习主题设置。

11. 为列表添加动画

在 Compose 中,有多种方法可以为您的 UI 添加动画:从用于简单动画的高级 API 到用于完全控制和复杂过渡的低级方法。您可以在文档中了解它们。

在本部分,您将使用一个低级 API,但请放心,它们也可以非常简单。让我们为我们已经实现的大小更改添加动画

9efa14ce118d3835.gif

为此,您将使用 animateDpAsState composable。它返回一个 State 对象,其 value 将由动画持续更新,直到完成。它接受一个类型为 Dp 的“目标值”。

创建一个取决于展开状态的动画 extraPadding

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

运行应用并尝试动画。

animateDpAsState 接受一个可选的 animationSpec 参数,您可以通过它自定义动画。让我们做一些更有趣的事情,比如添加一个基于弹簧的动画

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring


@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    // ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    // ...

    )
}

请注意,我们还确保 padding 永远不会是负数,否则可能会导致应用崩溃。这引入了一个细微的动画 bug,我们稍后将在“最后的润色”中修复它。

The spring 规范不接受任何与时间相关的参数。相反,它依靠物理属性(阻尼和刚度)使动画更自然。现在运行应用以尝试新的动画

9efa14ce118d3835.gif

使用 animate*AsState 创建的任何动画都是可中断的。这意味着如果在动画进行过程中目标值发生变化,animate*AsState 会重新启动动画并指向新值。中断在基于弹簧的动画中看起来尤其自然

d5dbf92de69db775.gif

如果您想探索不同类型的动画,请尝试 spring 的不同参数、不同的规范(tweenrepeatable)以及不同的函数:animateColorAsState不同类型的动画 API

本部分的完整代码

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {


    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. 为应用设置样式和主题

到目前为止,您还没有为任何 composable 设置样式,但您已经获得了不错的默认样式,包括深色模式支持!让我们看看 BasicsCodelabThemeMaterialTheme 是什么。

如果您打开 ui/theme/Theme.kt 文件,您会看到 BasicsCodelabTheme 在其实现中使用了 MaterialTheme

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme 是一个可组合函数,它反映了Material Design 规范中的样式原则。样式信息向下级联到其 content 中的组件,这些组件可能会读取该信息以自行设置样式。在您的 UI 中,您已经按如下方式使用 BasicsCodelabTheme

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

因为 BasicsCodelabTheme 在内部包装了 MaterialTheme,所以 MyApp 带有主题中定义的属性样式。从任何后代 composable 中,您都可以检索 MaterialTheme 的三个属性:colorSchemetypographyshapes。使用它们为您的一个 Text 设置标题样式

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

上面示例中的 Text composable 设置了一个新的 TextStyle。您可以创建自己的 TextStyle,或者通过使用 MaterialTheme.typography(推荐)来检索主题定义的样式。这种结构让您可以访问 Material 定义的文本样式,例如 displayLarge、headlineMedium、titleSmall、bodyLarge、labelMedium 等。在您的示例中,您使用了主题中定义的 headlineMedium 样式。

现在构建以查看我们新样式的文本

673955c38b076f1c.png

一般来说,最好将您的颜色、形状和字体样式保存在 MaterialTheme 中。例如,如果您硬编码颜色,实现深色模式将非常困难,并且需要大量容易出错的工作来修复。

但是,有时您需要稍微偏离颜色和字体样式的选择。在这种情况下,最好基于现有颜色或样式。

为此,您可以使用 copy 函数修改预定义样式。使数字额外加粗

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

这样,如果您需要更改 headlineMedium 的字体系列或任何其他属性,您就不必担心那些细微的偏差。

现在这应该是预览窗口中的结果

b33493882bda9419.png

设置深色模式预览

目前,我们的预览只显示应用在浅色模式下的外观。向 GreetingPreview 添加一个额外的 @Preview 注解,使用 UI_MODE_NIGHT_YES

import android.content.res.Configuration.UI_MODE_NIGHT_YES


@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

这会在深色模式下添加一个预览。

2c94dc7775d80166.png

调整应用主题

您可以在 ui/theme 文件夹中的文件中找到与当前主题相关的所有内容。例如,我们目前使用的默认颜色在 Color.kt 中定义。

我们首先定义新颜色。将这些添加到 Color.kt

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

现在将它们分配给 Theme.ktMaterialTheme 的调色板

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

如果您回到 MainActivity.kt 并刷新预览,预览颜色实际上没有变化!这是因为默认情况下,您的预览将使用动态颜色。您可以在 Theme.kt 中看到添加动态颜色的逻辑,使用 dynamicColor 布尔参数。

要查看非自适应版本的配色方案,请在 API 级别低于 31 的设备(对应于引入自适应颜色的 Android S)上运行应用。您将看到新的颜色

493d754584574e91.png

Theme.kt 中,定义深色调色板

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

现在当我们运行应用时,我们将看到深色在实际中的效果

84d2a903ffa6d8df.png

Theme.kt 的最终代码

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. 最后的润色!

在这一步中,您将应用您已知的知识,并在少量提示下学习新概念。您将创建这个

8d24a786bfe1a8f2.gif

用图标替换按钮

  • IconButton composable 与子项 Icon 一起使用。
  • 使用 Icons.Filled.ExpandLessIcons.Filled.ExpandMore,它们在 material-icons-extended artifact 中可用。将以下行添加到 app/build.gradle.kts 文件中的 dependencies 中。
implementation("androidx.compose.material:material-icons-extended")
  • 修改 padding 以修复对齐问题。
  • 添加可访问性的内容描述(参见下方的“使用字符串资源”)。

使用字符串资源

应存在“显示更多”和“显示更少”的内容描述,您可以使用简单的 if 语句添加它们

contentDescription = if (expanded) "Show less" else "Show more"

然而,硬编码字符串是一种不好的做法,您应该从 strings.xml 文件中获取它们。

您可以使用 Android Studio 中“上下文操作”中可用的“提取字符串资源”对每个字符串执行此操作。

或者,打开 app/src/res/values/strings.xml 并添加以下资源

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

显示更多

“Composem ipsum”文本出现和消失,触发每张卡片的大小变化。

  • Greeting 内的 Column 中添加一个新的 Text,当项目展开时显示该文本。
  • 移除 extraPadding,而是将 animateContentSize 修饰符应用于 Row。这将自动化创建动画的过程,手动创建动画会很困难。此外,它还消除了对 coerceAtLeast 的需求。

添加阴影和形状

  • 您可以将 shadow 修饰符与 clip 修饰符结合使用以实现卡片外观。但是,有一个 Material composable 可以做到这一点:Card。您可以通过调用 CardDefaults.cardColors 并覆盖要更改的颜色来更改 Card 的颜色。

最终代码

package com.example.basicscodelab

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. 恭喜您

恭喜您!您已了解 Compose 的基础知识!

Codelab 解决方案

您可以从 GitHub 获取本 Codelab 解决方案的代码

$ git clone https://github.com/android/codelab-android-compose

或者,您可以将仓库下载为 Zip 文件

接下来?

请查看Compose 学习路径上的其他 Codelab

延伸阅读