Jetpack Compose 基础

1. 开始之前

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

Compose 应用由可组合函数组成 - 只是用 @Composable 标记的普通函数,可以调用其他可组合函数。一个函数就是创建新 UI 组件所需的一切。该注释告诉 Compose 为函数添加特殊支持,以便随着时间的推移更新和维护您的 UI。Compose 允许您将代码构建成小的块。可组合函数通常简称为“可组合项”。

通过创建小的可重用可组合项,可以轻松构建应用程序中使用的 UI 元素库。每个元素负责屏幕的一部分,并且可以独立编辑。

在您完成本 Codelab 的过程中,请查看以下代码演练以获得更多支持

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

先决条件

  • 熟悉 Kotlin 语法,包括 Lambda 表达式

您将做什么

在本 Codelab 中,您将学习

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

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

8d24a786bfe1a8f2.gif

您需要什么

2. 创建新的 Compose 项目

要创建新的 Compose 项目,请打开 Android Studio。

如果您在“欢迎使用 Android Studio”窗口中,请点击“启动新的 Android Studio 项目”。如果您已打开 Android Studio 项目,请从菜单栏中选择“文件 > 新建 > 新建项目”。

对于新项目,请从可用模板中选择“空活动”。

d12472c6323de500.png

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

选择“空活动”模板后,将在您的项目中为您生成以下代码

  • 项目已配置为使用 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 标记的。此函数将生成一段显示给定输入 String 的 UI 层次结构。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 文件不同,您在其中调用可组合函数。

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 是一种设置可组合函数样式的方法。您将在“设置应用主题”部分了解有关此内容的更多信息。要查看文本如何在屏幕上显示,您可以 either 在模拟器或设备上运行应用,或者使用 Android Studio 预览。

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

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

fb011e374b98ccff.png

如果选择了“代码” eeacd000622ba9b.png,预览可能不会出现。点击“拆分” 7093def1e32785b2.png 以查看预览。

4. 微调 UI

让我们首先为 Greeting 设置不同的背景颜色。您可以通过将 Text 可组合项包装在 Surface 中来实现此目的。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 是 opinionated(有主见的),因为它提供了大多数应用通用的良好默认值和模式。Compose 中的 Material 组件构建在其他基础组件(在 androidx.compose.foundation 中)之上,如果您需要更多灵活性,也可以从您的应用组件访问这些组件。

在这种情况下,Surface 理解到,当背景设置为 primary 颜色时,在其上方的任何文本都应该使用 onPrimary 颜色,该颜色也在主题中定义。您可以在为您的应用设置主题部分了解更多相关信息。

修饰符

大多数 Compose UI 元素(如 SurfaceText)都接受一个可选的 modifier 参数。修饰符告诉 UI 元素如何在父布局中进行布局、显示或行为。您可能已经注意到 Greeting 可组合项已经有一个默认修饰符,然后将其传递给 Text

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

现在,在屏幕上为您的 Text 添加填充。

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. 重用可组合项

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

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

创建一个名为 MyApp 的可组合项,其中包含问候语。

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

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

在预览中,让我们调用 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

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

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

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

bf27ee688c3231df.png

请注意,您可能需要重新调整填充位置。

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

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

您尚未设置尺寸或向可组合项的大小添加任何约束,因此每一行都占用其可以占用的最小空间,预览也执行相同的操作。让我们更改我们的预览以模拟小型手机的常见宽度,即 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)
        }
    }
}

请注意

  • 修饰符可以有重载,因此,例如,您可以指定不同的方法来创建填充。
  • 要向元素添加多个修饰符,只需将它们链接起来。

有多种方法可以实现此结果,因此,如果您的代码与此代码段不匹配,并不意味着您的代码有错。但是,复制并粘贴此代码以继续学习代码实验室。

添加按钮

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

ff2d8c3c1349a891.png

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

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

要实现此目的,您需要学习如何在行的末尾放置可组合项。没有 alignEnd 修饰符,因此,您可以改为向开头的可组合项赋予一些 weightweight 修饰符使元素填充所有可用空间,使其变得灵活,有效地推开没有权重的其他元素,这些元素称为不灵活元素。它还使 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 可组合项中。看看此 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。

要向可组合项添加内部状态,您可以使用 mutableStateOf 函数,该函数使 Compose 重新组合读取该 State 的函数。

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

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

但是,您不能只将 mutableStateOf 分配给可组合项内的变量。如前所述,重新组合可以在任何时间发生,这将再次调用可组合项,并将状态重置为值为 false 的新可变状态。

要跨重新组合保留状态,请使用 remember 记住可变状态。

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

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

remember 用于保护免受重新组合的影响,因此状态不会重置。

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

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

更改状态和对状态更改做出反应

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

您可以通过将 lambda 表达式 分配给它来定义点击时要执行的操作。例如,让我们切换 expanded 状态的值,并根据该值显示不同的文本。

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

在交互模式下运行应用程序以查看行为。

374998ad358bf8d6.png

单击按钮时,expanded 将被切换,从而触发按钮内部文本的重新组合。每个 Greeting 都维护其自己的 expanded 状态,因为它们属于不同的 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

@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. 状态提升

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

使状态可提升可以避免重复状态和引入错误,有助于重用可组合项,并使可组合项更容易测试。相反,不需要由可组合项的父级控制的状态不应该被提升。**真相来源**属于创建和控制该状态的任何一方。

例如,让我们为我们的应用程序创建一个 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()
    }
}

此代码包含大量新功能

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

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

在 Compose 中,**您不会隐藏 UI 元素**。相反,您只需不将它们添加到组合中,因此它们不会添加到 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可组合项共享。

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

首先,将MyApp的内容移动到一个名为Greetings的新可组合项中。还要调整预览以调用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 可组合项添加一个预览,以便我们可以测试其行为

@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修改我们的状态,不如让它在用户点击“继续”按钮时通知我们。

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

尝试向定义为onContinueClicked: () -> Unit的 onboarding 屏幕添加一个函数参数,以便您可以从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传递一个函数而不是状态,我们使这个可组合项更易于重用,并防止状态被其他可组合项修改。一般来说,它使事情变得简单。一个很好的例子是 onboarding 预览现在需要如何修改才能调用OnboardingScreen

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

onContinueClicked分配给一个空 lambda 表达式意味着“不做任何事”,这对于预览来说是完美的。

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

25915eb273a7ef49.gif

MyApp可组合项中,我们第一次使用了by属性委托来避免每次都使用 value。让我们在 Greeting 可组合项中也为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函数**仅在可组合项保留在 Composition 中时才有效**。当您旋转时,整个活动将重新启动,因此所有状态都将丢失。这也会发生在任何配置更改和进程死亡时。

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

现在用rememberSaveable替换shouldShowOnboardingremember的使用

    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可组合项。它返回一个 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))

    // ...

    )
}

请注意,我们还确保填充永远不会为负,否则可能会导致应用程序崩溃。这引入了一个细微的动画错误,我们将在后面的**收尾工作**中修复。

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. 样式和主题化您的应用程序

到目前为止,您没有为任何可组合项设置样式,但您仍然获得了不错的默认样式,包括深色模式支持!让我们看看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会使用主题中定义的属性进行样式设置。在任何后代的可组合项中,都可以检索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可组合项设置了一个新的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可组合项与子Icon一起使用。
  • 使用Icons.Filled.ExpandLessIcons.Filled.ExpandMore,它们在material-icons-extended构件中可用。将以下行添加到app/build.gradle.kts文件中的依赖项中。
implementation("androidx.compose.material:material-icons-extended")
  • 修改填充以修复对齐方式。
  • 为辅助功能添加内容描述(请参阅下面的“使用字符串资源”)。

使用字符串资源。

“显示更多”和“显示更少”的内容描述应该存在,您可以使用简单的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 可组合项可以准确地做到这一点: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 学习路径上的其他代码实验室。

进一步阅读