Jetpack Compose 基础知识

1. 开始前的准备工作

Jetpack Compose 集响应式编程模型与简明易用的 Kotlin 编程语言于一身,是旨在简化界面开发的一款现代化工具包。其采用完全声明式的方法,这意味着您可通过调用一系列将数据转变为界面层次结构的函数来描述界面。当底层数据发生更改时,框架将自动重新调用这些函数,从而为您更新视图层次结构。

Compose 应用由可组合函数构成,其中的函数只是带有 @Composable 标记的常规函数,可调用其他可组合函数。仅需一个函数即可创建新的界面组件。您可通过注释,让 Compose 向函数添加特殊支持,以用于随着时间推移来更新和维护界面。您可通过 Compose 将代码组织成小块。可组合函数通常简称为"可组合件"。

您可通过编写小型的可复用可组合件,轻松构建应用中使用的界面元素库。每个可组合件负责屏幕的一个部分,可单独进行编辑。

先决条件

  • 有使用 Kotlin 语法的经验,包括 lambdas

您的任务

在本 Codelab 中,您将学习:

  • 什么是 Compose
  • 如何使用 Compose 构建界面
  • 如何管理可组合函数中的状态
  • Compose 中的数据流原则

您需要用到的工具

2. 新建 Compose 项目

如要新建 Compose 项目,请打开 Android Studio Canary 版,按如下所示选择 Start a new Android Studio project(新建 Android Studio 项目)

e0251a4909548d94.png

如果未出现以上画面,请点击 File(文件)> New(新建)> New Project(新建项目)。

新建项目时,从可用模板中选择 Empty Compose Activity(空 Compose 活动)。

d6d1f1ed5f94bd7e.png

点击 Next(下一步),然后像平常一样配置您的项目。确保您在 minimumSdkVersion 中选择 API 级别 21 或更高级别*,*这是 Compose 支持的最低 API 级别。

选择 **Empty Compose Activity(空 Compose 活动)**模板后,系统将在项目中为您生成以下代码。已将此项目配置为使用 Compose。系统将创建 AndroidManifest.xml 文件、在app/build.gradle(或 build.gradle (Module: YourApplicationName.app)) 文件中导入 Compose 依赖项并支持 Android Studio 使用带有 buildFeatures { compose true } 标记的 Compose。

打开 app/build.gradle 文件,该文件应如下所示:

android {
    ...
    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.4.0'
    }
}

dependencies {
    ...
     implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.ui:ui-tooling:$compose_version"
    ...
}

要更新至最近的 Compose 版本,请打开根 build.gradle 文件,并将 compose_version 更改为 1.0.0-alpha04

buildscript {
    ext {
        compose_version = '1.0.0-alpha04'
    }
    ...
}

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

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

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

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

下一部分中,您将看到各种操作法方法,以及如何改进这些方法以打造灵活、可复用的布局。

关于 Codelab 的解决方案

您可以从 GitHub 中获取此 Codelab 的解决方案的代码:

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

或者,您可以以 zip 文件的形式下载此代码库:

您将在 BasicsCodelab 项目中找到解决方案代码。我们建议您按照自己的进度逐步学习 Codelab,如果觉得有必要,可以查看该解决方案。在 Codelab 中,系统将显示您需要添加到项目的代码片段。

3. Compose 入门指南

浏览模板为您生成的与 Compose 有关的不同的类和方法。

可组合函数

可组合函数是带有 @Composable 注释的常规函数,可支持您的函数调用其中的其他 @Composable 函数。您可以了解如何将 Greeting 函数标记为 @Composable。该函数将生成一个界面层次结构,并在其中显示给定的输入和 StringText 是库提供的一个可组合函数。

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

Android 应用中的 Compose

使用 Compose 时,Activity 仍然是 Android 应用的入口点。在我们的项目中,系统会在用户打开应用时启动 MainActivity(已在 AndroidManifest.xml 文件中指定)。您可以使用 setContent 来定义布局,但与在传统视图系统中使用 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(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme 是设置可组合函数样式的方式。您可在 **Theming your app(设置应用主题)**部分中查看与之相关的更多内容。如要查看屏幕上的文本显示方式,您可以在模拟器或设备中运行应用,也可以使用 Android Studio 预览功能。

如要使用 Android Studio 预览功能,只需通过 @Preview 注释标记任意无参数可组合函数或带默认参数的函数,然后构建项目即可。您已经可以在 MainActivity.kt 文件中看到 Preview Composable 函数。您可以在同一文件中建立多个预览,并为这些预览命名。

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

9bdebeaaf5a1a5b4.png

如果未选择 Code(代码) bcf00530a220eea9.png,系统可能不会显示预览。点击 Split(分块) aadde7eea0921d0f.png 以查看预览。

4. 说明性界面

如果要为 Greeting 设置不同的背景颜色,您需要定义一个将其包含在内的 Surface

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        Text (text = "Hello $name!")
    }
}

系统会将 Surface 中嵌套的组件显示在背景颜色上方(除非其他 Surface 另行指定)。

向项目添加此代码时,您将在 Android Studio 右上角看到 **Build & Refresh(构建并刷新)**按钮。点按该按钮或构建项目,以在预览中查看新的更改。

ab0ffaac888dc715.png

您可以在预览中查看新的更改:

fe689f24410234d1.png

辅助键

您可为大多数 Compose 界面元素(像 SurfaceText)应用可选的辅助键参数。辅助键参数将指示界面元素在其父布局中的布局、显示和行为方式。辅助键是常规的 Kotlin 对象。

您可以向其分配变量并进行复用。您还可以通过利用工厂扩展函数或已合并为单个参数的过载运算符plus,将多个此类辅助键依次链接起来。

padding 辅助键将应用其所装饰元素周围的大量空间。为增加屏幕上的文本边距,您可向 Text 添加辅助键:

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

点击 Build & Refresh 以查看新的更改。

8d7df323515c93ea.png

Compose 可复用性

与代码库中的其他函数类似,您向界面中添加的元素越多,您创建的嵌套级数就越多。如果函数规模大到一定程度,则可能会影响可读性。通过创建较小的可复用组件,您能轻松构建可在应用中使用的界面元素库。每个组件会负责一小块屏幕,并可单独进行编辑。

请注意,MainActivity.kt 中的可组合函数在 MainActivity 类外部,且系统已将其声明为顶级函数。Activity 外部的代码越多,可共享和复用的内容就越多。

首先,重构代码以提高其可复用性,并创建包含特定于此 Activity 的 Compose 界面逻辑的全新 @Composable MyApp 函数。

其次,将应用的背景颜色放置在可复用的 Greeting 可组合件上没有意义。该配置应该应用于此屏幕上的每个界面,因此,要将 SurfaceGreeting 移至新的 MyApp 函数:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

@Composable
fun MyApp() {
    BasicsCodelabTheme {
        Surface(color = Color.Yellow) {
            Greeting(name = "Android")
        }
    }
}

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

@Preview
@Composable
fun DefaultPreview() {
    MyApp()
}

您应该复用其他 Activity 中的 MyApp 可组合函数,因为这些函数定义了可在多处使用的顶级配置。但是,其当前状态不支持这样做,因为其中嵌入了 Greeting。请继续阅读,以了解如何创建保存常见应用配置的容器。

创建容器函数

如果要创建具备您应用中所有常见配置的容器,该怎么做?

要创建一般容器,请创建采用可组合函数(此处称为 content)作为参数的可组合函数,该函数将返回 Unit。您将返回 Unit,因为您可能已经注意到,可组合函数不会返回界面组件,而是会发出此类组件。这就是这些函数必须返回 Unit 的原因:

@Composable
fun MyApp(content: @Composable () -> Unit) {
    BasicsCodelabTheme {
        Surface(color = Color.Yellow) {
            content()
        }
    }
}

在函数内定义您希望容器提供的所有共享配置,然后调用传递的子级可组合件。本例中,您应该应用 MaterialTheme 和黄色界面,然后调用 content()

您可以按以下方式来使用它(利用 Kotlin 的末尾 lambda 语法):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                Greeting("Android")
            }
        }
    }
}

@Preview("Text preview")
@Composable
fun DefaultPreview() {
    MyApp {
        Greeting("Android")
    }
}

此代码与您在前一部分中所设的内容相同,但现在更加灵活。创建容器可组合函数是很好 的做法,可以改进可读性,提高代码复用率。

使用"布局"多次调用可组合函数

您可将界面组件提取到可组合函数,这样即可在不复制代码的情况下进行复用。在以下示例中,您可展示两条问候语,其分别复用了具有不同参数的同一可组合函数。

要垂直放置各项,请使用 Column 可组合函数(与垂直 LinearLayout 相似)。

@Composable
fun MyScreenContent() {
    Column {
        Greeting("Android")
        Divider(color = Color.Black)
        Greeting("there")
    }
}

如果您想让用户在打开应用时看到 MyScreenContent,那就必须对 MainActivity 代码作出相应更改。您还可以修改预览代码,这样便能更快地在 Android Studio 中进行迭代,且无需将应用部署到设备或模拟器上。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

如果您刷新预览内容,则会看到垂直放置的各项内容:

2ef5172b5ea7d30e.png

Compose 和 Kotlin

Compose 函数可像 Kotlin 中的任何其他函数一样进行调用。因为您可以添加语句来影响界面的显示方式,所以这会让构建界面的功能变得更加强大。

例如,您可以使用 for 循环来向 MyScreenContentColumn 中添加元素:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}

a4e12f30f6551798.png

5. Compose 中的状态

应对状态更改是 Compose 中非常核心的功能。Compose 应用可通过调用可组合函数来将数据转换为界面。如果数据发生更改,您则可使用新数据重新调用这些函数,创建更新后的界面。Compose 可提供工具来监测应用数据中的更改,此类工具会自动重新调用您的函数,我们将这一过程称为重新组合。Compose 还可查看各可组合件需要哪些数据,这样就只需要重新组合数据已更改的组件即可,并可跳过不受影响的组件组合。

Compose 会在后台使用自定义的 Kotlin 编译器插件,因此,当底层数据发生更改时,其可以重新调用可组合函数来更新界面层次结构。

例如,调用 MyScreenContent 可组合函数中的 Greeting("Android") 时,可以对输入内容做 ("Android") 硬编码处理,这样一来,系统便可一次性将 Greeting 添加到界面树中并不再进行更改,即使重新组合 MyScreenContent 的正文也是如此。

如要向可组合件添加内部状态,请使用 mutableStateOf 函数,该函数将提供可组合的可变内存。如要让每次重新组合的状态保持相同,请使用 remember 记下可变状态。此外,如果屏幕上的不同位置有可组合件的多个实例,则每个副本都将获得自己的状态版本。您可将内部状态视为类中的私有变量。

可组合函数将自动订阅此变量。如果状态发生更改,系统则会重新组合读取这些字段的可组合件。

创建一个计数器来持续跟踪用户点击 Button 的次数。通过 ButtonCounter 定义为一个可组合函数,显示已点击该按钮的次数:

@Composable
fun Counter() {

    val count = remember { mutableStateOf(0) }

    Button(onClick = { count.value++ }) {
        Text("I've been clicked ${count.value} times")
    }
}

因为 Button 会读取 count.value,所以无论 Button 在什么时候发生更改,系统都会对其进行重新组合,并显示 count 的新值。

您现在可以将 Counter 添加到屏幕:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Transparent, thickness = 32.dp)
        Counter()
    }
}

如果在模拟器中运行应用,或者单击交互式预览按钮 565348e532ffcc43.png,则可以查看 Counter 如何保持状态以及每次点击时的计数增加情况。

2d7f181203b56f12.gif

事实来源

在可组合函数中,应该公开对调用函数有用的状态,因为这是可以使用或控制状态的唯一方法,我们将此过程称为状态提升。

状态提升是通过调用内部状态的函数来对其进行控制的一种方式。为此,您可以通过可控制的可组合函数的参数公开状态,并从可控制的可组合函数外部实例化该状态。进行状态提升可避免出现重复状态和引入错误,并有助于重用可组合件,还可大幅优化可组合件,使其更加易于测试。如果某个状态不能引起可组合件调用方的兴趣,则其应该是内部状态。

在某些情况下,使用者可能并不关心某个状态(例如,在滚动条中,scrollerPosition 状态已公开,而 maxPosition 状态未公开)。事实来源属于创建和控制该状态的人员。

在本例中,因为 Counter 的使用者可能对状态感兴趣,所以您可以通过引入 (count, updateCount) 对作为 Counter 的参数,使其完全遵循调用方的安排。这样,系统便能提升 Counter 的状态:

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val counterState = remember { mutableStateOf(0) }

    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
        Divider(color = Color.Transparent, thickness = 32.dp)
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(onClick = { updateCount(count+1) }) {
        Text("I've been clicked $count times")
    }
}

6. 灵活布局

之前您已简单接触过 Column,其可用于按垂直顺序放置项目。同样,我们可使用 Row 来将水平放置各个项目。

RowColumn 均会逐一放置各个项目。如果要灵活放置某些项目,使其按特定权重占据屏幕的某些部分,则可以使用 weight 辅助键。

比如说您要在屏幕底部放置 Button 按钮,而让其他内容保持在顶部。您可以通过以下步骤执行此操作:

  1. 可通过 weight 辅助键封装另一 Column 内的需要灵活放置的项目。这样一来,此 Column 便可支持灵活放置,而剩余内容则会按既定方式放置,所以它将尽可能占据更多空间。确定外部 Column 的大小后,其便能够利用所有剩余的高度。
  2. Counter 留在外部 Column 中(默认情况下,均会按固定模式放置)。
@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val counterState = remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxHeight()) {
        Column(modifier = Modifier.weight(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}

您可对外部 Column 使用 fillMaxHeight() 辅助键,使其尽可能占据更大的屏幕空间(亦可使用 fillMaxSize()fillMaxWidth() 辅助键)。

  1. 如果您刷新预览内容,则会看到新的更改:

9956c1c7a713c7e7.png

有关如何在 Compose 中利用 Kotlin 还有另外一个例子,您可以通过 if else 语句,根据用户点按 Button 的次数来更改其背景颜色:

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count+1) },
        backgroundColor = if (count > 5) Color.Green else Color.White
    ) {
        Text("I've been clicked $count times")
    }
}

621365c90505a878.gif

此部分的全部代码

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.codelab.basics.ui.BasicsCodelabTheme



class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                MyScreenContent()
            }
        }
    }
}

@Composable
fun MyApp(content: @Composable () -> Unit) {
    BasicsCodelabTheme {
        Surface(color = Color.Yellow) {
            content()
        }
    }
}

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val counterState = remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxHeight()) {
        Column(modifier = Modifier.weight(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}

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

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        backgroundColor = if (count > 5) Color.Green else Color.White
    ) {
        Text("I've been clicked $count times")
    }
}

@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
    MyApp {
        MyScreenContent()
    }
}

7. 确定您应用的主题

在 Codelab 的上述示例中,您未定义任何可组合件的任何样式。如何确定您应用的主题?与其他可组合函数一样,主题是组件层次结构的一部分,BasicsCodelabTheme 便是其中的一个示例。

如果您打开 Theme.kt 文件,则会看到 BasicsCodelabTheme 在其实现过程中使用了 MaterialThemeMaterialTheme 是一个可组合函数,反映了 Material Design 规范中的样式设定原则。系统会将样式信息向下级联到其内部的组件之中,这些组件可读取此类信息来设定自身的样式。在您的简易的初始界面中,可按如下所示使用 BasicsCodelabTheme

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                Greeting(name = "Android")
            }
        }
    }
}

因为 BasicsCodelabTheme 会在内部封装 MaterialTheme,所以系统会使用主题中定义的属性来设定 Greeting 的样式。您可以检索 MaterialTheme 的属性,并按以下方式使用这些属性定义 Text 的样式:

@Composable
fun Greeting(name: String) {
    Text (
        text = "Hello $name!",
        modifier = Modifier.padding(24.dp),
        style = MaterialTheme.typography.h1
    )
}

我们在上述示例中的 Text 可组合件中设置了三个参数,分别是要显示的字符串、辅助键和 TextStyle。您可以创建自己的 TextStyle,或者也可以使用 MaterialTheme.typography 检索按主题定义的样式。您可利用此构造访问 Material 定义的文本样式,如 h1body1subtitle1。在您的示例中,您可使用主题中定义的 h1 样式。

如果使用此代码查看预览内容,则会看到以下屏幕截图:

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

ccdadfe07a0c40ea.png

创建您应用的主题

您可以创建像 Theme.kt 主题一样的应用主题。如要了解更多详情,您可查看文件中的代码,以便了解发生了什么。

因为您可能会在应用的多个位置用到 BasicsCodelabTheme(可能在所有 Activity 中),所以请创建可复用组件。

就像在**"Theming your app(设置应用主题)"**部分中看到的一样,主题是一个可组合函数,其中包含其他子级可组合函数。如要使其可复用,可按**"Declarative UI(声明性界面)"**部分中的操作来创建容器可组合函数:

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {

    // TODO 
}

MaterialTheme 将保留颜色和版式的配置。只需在此处更改几个颜色即可得到所需设计。

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable



private val DarkColors = darkColors(
    primary = purple200,
    primaryVariant = purple700,
    secondary = teal200
)

private val LightColors = lightColors(
    primary = purple500,
    primaryVariant = purple700,
    secondary = teal200
)

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    val colors = if (darkTheme) {
        DarkColors
    } else {
        LightColors
    }

    MaterialTheme(colors = colors) {
        content()
    }
}

您提供的自定义颜色会覆盖 lightColorPalettedarkColorPalette 方法中的颜色,除非另行指定,否则这些颜色将成为浅色和深色 Material 基线主题的默认颜色。系统会将其将传递至 MaterialTheme 的构造函数中,如您之前所见,此构造函数将实现 Material Design 规范中的样式设定原则。

同样地,您可通过将其传递至 MaterialTheme 函数来覆盖应用中的所用版式和形状。

8. 恭喜

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

关于 Codelab 的解决方案

您可以从 GitHub 中获取此 Codelab 的解决方案的代码:

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

或者,您可以以 zip 文件的形式下载此代码库:

未来计划

查看 Compose 路径上的其他 Codelab:

深入阅读