跳至主要內容

译文 | Jetpack Compose 入门教程

guodongAndroid大约 20 分钟

提示

本文是《Jetpack Compose Tutorial for Android: Getting Started》的译文,仅供个人学习使用,不得且不可用于商业用途,若是因用于商业用途而产生的一切法律责任与本人无关。

译者已获得原组织/原作者翻译授权,版权归原组织/原作者所有。@kodeco


原文信息:

发布日期:2020年11月16日

原文作者:Alex Sullivanopen in new window

原文地址open in new window

原文资源open in new window

前言

移动应用程序开发的革新通常以波浪或趋势的形式出现。无论当前的趋势是关于语言的安全性和稳定性,还是平台的性能改进,跟上趋势总是很重要的,因为其中一些趋势从根本上改变了我们开发应用程序的方式。在过去的一两年里,当涉及到应用程序的业务逻辑时,我们一直在构建声明式和响应式编程。

这意味着"程序猿们"已经从经典的命令式 MVC/P(Model-View-Controller/Presenter) 编程向更具响应式的编程方向迁移,例如 MVVM(Model-View-ViewModel),可以在 ViewModel 中更新数据流,并且 UI 通过重新绘制自身或简单地更新其 UI 的一部分来对更改做出响应。发生这种变化是因为声明式编程不易出错、更易于组合且更易于测试,因为你更关注于数据而不是 UI 交互。

MVVM in Clean Architecture

Jetpack 和 MVVM。

顺着这样的趋势,构建用户界面自然而然地转向了这个方向,以及声明式 UI 框架和工具包的趋势。前端工具包和框架(例如 React.jsopen in new windowFlutteropen in new window)日益流行,这促使其他平台也提出了自己的实现方案。

在 Google I/O 2019 上,Google 首次发布了 Jetpack Compose。Jetpack Compose 是 Google 对声明式 UI 框架趋势的响应,Android 团队正在开发它以从根本上改变开发人员创建 UI 的方式,使其编写起来更容易、更快,运行起来更高效。它是 Jetpack 库套件的一部分,因此应该在整个平台版本中提供兼容性,无需回避某些功能,因为它的目标是低端设备或旧版本的 Android。

尽管它仍处于 alpha 阶段,但 Compose 已经在 Android 社区掀起波澜。

在本教程中,你将学习 Jetpack Compose 的基本概念,比如:可组合函数,如何在屏幕上显示内容以及更新内容。在掌握了基本概念之后,你会继续使用 Jetpack Compose 构建一个菜谱 App,它展示了由 Material-design 风格的食谱列表,其中包含卡片、图像和文本。

提示

注意:Compose 仍然处于 alpha 阶段。不应该把它用于正式 Apps,因为它的 API 还不够稳定,从而可能会破坏你的代码。

为什么使用 Jetpack Compose 进行构建?

在使用 Compose 编写代码之前,请先思考为什么 Android 团队创建了这个库?

creature brain

与 Android 中当前的 UI 构建过程相比,Compose 具有三大优势。

它是声明式的

Jetpack Compose 是一个 声明式 UI 框架,这意味着你可在不以命令式编程的概念改变前端视图的情况下描述应用界面。

以隐藏视图为例。当使用当前命令式的 UI 工具包,你通常使用 findViewById() 来获取你想要隐藏的 View 的一个引用,然后通过这个引用调用 setVisibility()

在声明式 UI 中,你将只描述 UI 是可见或不可见。当你想要更改其可见性时,只需重新运行描述 UI 的代码。

因此,在创建视图后,你不用再获取对视图的引用。相反,你只需重新运行使用不同参数创建视图的代码。

提示

注意:当你更新某些内容时,Compose 实际上不会重建整个视图。它会智能更新那些需要更新的部分。

它是独立的

Compose 是一个不依赖操作系统的库。对开发者来说这是一大优势。

如果现在 Google 想更新 LinearLayout 组件,它需要发布一个新的 Android 系统版本。遗憾的是,由于碎片化问题导致很多人无法使用最新的 Android 版本。

这意味着你不能依赖新的操作系统发布多年并且大多数用户升级后再使用新的 LinearLayout 组件。

借助 Jetpack Compose,开发者可以在不依赖更新操作系统版本的情况下添加新功能。无论用户设备上的操作系统是什么版本,它都可以正常工作。

此外,与 LinearLayout 组件不同,发布新版本的 Jetpack Compose 不会破坏现有应用程序,因为除非你愿意,否则你不必升级到最新版本的 Jetpack Compose。Jetpack Compose 也没有使用 Android 现有的 UI 工具包,因此它是一个全新的开始,它提供了一个机会来解决 View 层次结构存在的一些由来已久的问题。而一旦你升级到新版本的 Android,作为操作系统一部分的 LinearLayout 组件也会随之升级,并且它可能会给依赖它的应用程序带来重大变化。

它是可组合的

Compose 库的构建方式使你可以在细分的、可重用的构建块中构建 UI,而不是在 FragmentActivity 中。这种可组合性使代码更加清晰明了并利于重用。

Android 已经问世十多年了,它的 UI 创建代码开始显示出它的年代感。光是 View 类就有上万行代码并且还需要兼容大量遗留代码。

启航

你可以在本文顶部原文信息中访问 原文地址 来获取本教程所需要的资源,也可以通过访问 原文资源 来获取。

为了获得使用 Jetpack Compose 开发的最佳体验,你应该下载最新的金丝雀版本的 Android Studio 预览版。因为当你使用 Android Studio 通过 Jetpack Compose 开发你的 App 时,你可以利用智能编辑功能,比如新项目模板和 Compose UI 实时预览功能。现在你有两个选择:升级你现有的 Android Studio 到金丝雀版本,或者下载一个金丝雀版本open in new window。你可以同时安装金丝雀和稳定版本的 Android Studio。

开始使用 Jetpack Compose

使用金丝雀版本的 Android Studio 来打开你下载资源中的 starter 项目。

编译并运行,你将看到一个白色屏幕,其中包含一个 Hello World 文本。

Hello World

现在打开 app/build.gradle 文件并查看 dependencies 代码块。你会看到四个有趣的依赖:

def composeVersion = "1.0.0-alpha06"
...
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.material:material:$composeVersion"

Compose 不仅仅是你导入的一个库,它是包含不同库的一个套件,用于构建你可能想要的不同 UI。在这个项目中,你将使用构建基本布局所需的基本构建代码块。

除了上述依赖,你还可以看到在 android -> buildFeatures 代码块中 compose 被设置为 true

buildFeatures {
  compose true
}

你已经了解了基本的 Jetpack Compose 项目所需的依赖,现在,可以真正开始学习它了。

浅尝 Jetpack Compose

由于 Jetpack Compose 是使用代码的方式来构建 UI,所以你不需要任何 XML 文件。这意味着你不需要在 ActivityFragment 中使用 setContentView(),而是使用 setContent() 来设置 UI。

现在,打开 MainActivity.kt 并使用下面的代码替换现有的 setContentView() 调用:

setContent {
  Text("Hello, World!")
}

当执行完上面的操作后,一定要确保从 androidx.compose 包中导入了相关依赖。setContent()Activity 的一个扩展函数,并且它有一个被 @Composable 标记的 lambda 参数。后续你会学习更多关于 @Composable 相关的内容。

除了 setContent() 之外,上面的代码片段中还有另一个新玩家:Text()

在 Jetpack Compose 中,你使用被 @Composable 注解标记的函数来构建 UI。如果你在 Mac 上使用 Command-Click 或者在 Windows 上使用 Control-click 来点击 Text(),你将看到类似下面的内容:

@Composable
fun Text(
    ...
)

Text() 实际上是被 @Composable 注解标记的一个函数。Text() 可组合函数负责在屏幕上绘制文本。你可以把它认为是 Compose 版本的 TextView

提示

注意:通常你应该使用驼峰方式来命名函数。当你创建可组合函数时,你应该使用大写的函数名称以明确的表示你正在构建一个可组合函数。类似于 Flutter 小部件的工作方式或 Kotlin Coroutine 函数(如 Job() 的命名方式)。

编译并运行,你将看到屏幕中的 Text()

Screenshot_1578158894

你可以通过 TextStyle 来自定义你的文本。通过将现有的 Text() 替换为以下内容来尝试一下:

Text("Hello, World!", style = TextStyle(color = Color.Red))

再次确保导入了合适的 androidx.ui 包。编译并运行,现在你将看到红色的文本。

Screenshot_1578159132.png

使用 Jetpack Compose 时,你将使用普通的 Kotlin 代码和函数参数而不是 XML 样式和属性来自定义你的 UI。你将在下一节中亲自尝试。

创建可组合函数

Jetpack Compose 的最大好处之一是你可以使用许多细分函数以模块化方式构建 UI,而不是为每个 Activity 使用一个巨大的 XML 文件。

现在你已经熟悉了 Text,你可以创建你第一个 @Composable 函数了。

MainActivity 下面添加以下函数:

@Composable
fun Greeting() {
  Text("Hello, World!", style = TextStyle(color = Color.Red))
}

恭喜,你刚刚创建了第一个自定义可组合函数!

为了使用它,使用 Greeting() 替换在 setContent() 中对 Text() 的调用:

setContent {
  Greeting()
}

编译并运行,你将看到如之前耀眼般的红色文本。

Screenshot_1578159132.png

使用大量细分函数是创建可在不同屏幕上重复使用的 UI 块的好方式。

不过,有件事需要特别牢记:你只能在另外一个 @Composable 函数中调用 @Composable 函数,否则,App 将会崩溃。

这一点与 Kotlin Coroutines 非常相似,你只能在另外一个挂起函数或协程中调用挂起函数。

预览可组合函数

通常,当你编写一个 Activity XML 的 UI 时,你可以使用布局预览来查看你的视图,而无需构建和运行你的应用程序。

Jetpack Compose 附带了一个类似的工具。

在之前创建的 Greeting() 函数之上且 @Composable 之下添加 @Preview

@Composable
@Preview
fun Greeting() {
  Text("Hello, World!", style = TextStyle(color = Color.Red))
}

导入注解后,你会在屏幕右侧的预览面板中看到可组合函数的预览。

Screen-Shot-2020-09-27

每次更新正在预览的可组合函数时,都必须重新编译才能看到更新后的视图。只能预览不带任何参数的可组合函数。

你可以预览你的组件了,现在是时候学习如何使用布局了。

布局可组合函数

屏幕上只有一个 Text 并不能构成一个特别有趣的应用程序。然而,当屏幕上显示三个 Text 时应该会带来绝对引人入胜的体验!

更新 Greeting() 以使用 Text() 三次:

Text("Hello, World!", style = TextStyle(color = Color.Red))
Text("Hello, Second World!", style = TextStyle(color = Color.Red))
Text("Hello, Third World!", style = TextStyle(color = Color.Red))

猜想一下上述可组合函数会如何显示?编译并运行,然后在预览窗口中查看结果是否符合你的预期。

BadPreviewThreeText

非常完美。

开个玩笑,它看起来非常糟糕!

没有任何东西控制这些 Text 的位置,因此它们都在彼此之上绘制,就好像它们位于 FrameLayout 中一样。幸运的是,Jetpack Compose 提供了大量布局可组合函数。

在上述情况中,你将使用 Column 可组合函数为上述的混淆添加秩序。

使用 Column 可组合函数

Column 想象成垂直的 LinearLayout。它只是将其所有子可组合函数布局在一个垂直的列中。

更新 Greeting() 以将三个 Text() 包装在一个 Column() 中:

@Composable
@Preview
fun Greeting() {
  Column {
    Text("Hello, World!", style = TextStyle(color = Color.Red))
    Text("Hello, Second World!", style = TextStyle(color = Color.Red))
    Text("Hello, Third World!", style = TextStyle(color = Color.Red))
  }
}

Column 有一个被 @Composable 标记的 lambda 块,在其中你可以定义垂直排列的子可组合函数。

Greeting() 中你添加了三个 Text() 来作为 Column 的子可组合函数。这种让可组合函数接受 lambda 以创建其他可组合函数的模式在 Jetpack Compose 中很常见。甚至可以说这就是整个 Compose 的设计思想。

编译并运行,你将看到三个在垂直列中排列的 Text。这看起来好多了。

Screenshot-2020-01-04

除了 Column() 之外,你还可以使用 Row() 在水平行布局子可组合函数中。这看起来像一个水平方向的 LinearLayout

ComposableCookBook 简介

你将构建一个名为 ComposableCookBook 的菜谱 App,而不是构建简单的红色文本,它会显示美味的食谱列表。该项目带有一个预定义的 Recipe

data class Recipe(
  @DrawableRes val imageResource: Int,
  val title: String,
  val ingredients: List<String>
)

一个 Recipe 包含一个图片,名称和一个原料列表。App 还在 Recipes.kt 中附带了默认的食谱列表。你将使用此食谱列表来构建你的菜谱 App。

创建一个食谱可组合函数

你的目标是使用 Jetpack Compose 使 ComposeCookbook 应用看起来更好,通过创建一个 UI,该 UI 在卡片中显示每个食谱,卡片顶部是食谱的图片,卡片下方是原料列表。

第一步,你需要创建一个显示单个食谱的可组合函数。

右键单击 ComposableCookBook 包并选择 New ▸ New Kotlin File/Class。然后从列表中选择 File 并输入文件名 RecipeCard。最后,在文件中添加如下所示的 RecipeCard() 可组合函数:

@Composable
fun RecipeCard(recipe: Recipe) {
  Text(recipe.title)
}

现在,你只是使用 Text() 显示菜谱的名称。

由于 RecipeCard() 带有一个参数,所以你不能使用 @Preview。然而,你可以创建另一个提供默认 RecipeCard() 的可组合函数。在 RecipeCard() 下面添加以下代码:

@Composable
@Preview
fun DefaultRecipeCard() {
  RecipeCard(defaultRecipes[0])
}

现在你可以预览 RecipeCard() 了。

Screenshot-2020-01-04

接下来,你将要在 RecipeCard() 名称的上方添加图片。

在 RecipeCard 中添加图片

RecipeCard() 中的内容替换为以下代码:

// 1
val image = imageResource(recipe.imageResource)
// 2
Column(modifier = Modifier.fillMaxWidth()) {
  // 3
  Image(asset = image, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxWidth().height(144.dp))
  Text(recipe.title)
}

确保导入所有可能标记为未解析引用的红色函数。

如此之小的代码块中有很多神奇之处!然后根据需要刷新预览。

以下是对上述代码的解释:

  1. 每个 Recipe 都有一个指向 Drawable 的图片资源。通常,你要么在 ImageView 上设置 Drawable,要么使用 Context 来获取 Drawable 的实例。

    在 Jetpack Compose 中,它使用不同的机制来获取 Image 的实例,此行代码告诉 Jetpack Compose 从给定的可绘制资源标识符中获取 Image

  2. 然后,你使用你之前学过的 Column 在垂直布局中排列你的食谱卡片内容。理想情况下,与食谱相关的图片应该是撑满卡片的宽度,所以你使用 Modifier 告诉这个 Column 占满卡片的宽度。

    Modifier 允许你调整可组合函数的 UI。在这种情况下,你使用修饰符来控制 Column 的大小。

    你可以使用修饰符进行比调整大小更多的操作,包括扩展可组合函数或应用纵横比。

  3. 最后,将第一步获取的 image 传递给 Image() 来渲染图片。就像你熟悉和喜爱的 ImageView 类一样,你可以为 Image 设置内容比例、宽度和高度。在 Jetpack Compose 中要实现这个功能,你可以使用 Modifier 设置图片的宽度和高度,然后使用 contentScale 参数设置内容比例。

刷新预览,你会看到食谱卡片的雏形!

Screenshot-2020-01-04

拉面看起来很好吃——但是你需要什么原料来烹饪它呢?下一个任务,你将创建一个原料列表。

列出原料

要列出原料,你将使用 Text()。由于在上一步你已经定义了一个 Column,添加原料将会非常容易。

将以下代码添加到食谱名称的 Text() 下面:

for (ingredient in recipe.ingredients) {
  Text(ingredient)
}

Jetpack Compose 的一大优点是你可以使用普通的 Kotlin 代码来描述稍微复杂的 UI 细节。

在上述代码中,你使用 for 循环列出了所有包含原料的 Text。如果你重建 UI,你会在名称下方看到这道美味拉面餐的所有原料。太酷了,对不对?而且你不需要定义一个 RecyclerView.Adapter 和任意的 ViewHolder

Screenshot-2020-01-04

拉面看起来确实很好吃,但食谱卡片本身看起来比较方正。接下来,你将为食谱卡片添加圆角,让它看起来更好看。

为 RecipeCard 添加圆角

使用 Surface() 作为父级可以让你有选择的为子项添加圆角。使用下面的代码替换现有 RecipeCard() 的内容:

Surface(shape = RoundedCornerShape(8.dp), elevation = 8.dp) {
  val image = imageResource(recipe.imageResource)
  Column(modifier = Modifier.fillMaxWidth()) {
    Image(asset = image, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxWidth().height(144.dp))
    Text(recipe.title)
    for (ingredient in recipe.ingredients) {
      Text("• $ingredient")
    }
  }
}

Surface() 处理绘制形状并为组件提供一个"海拔"高度。对于食谱卡片,你将使用带有 RoundedCornerShape() 和 8 dp "海拔"高度的 Surface()

刷新构建。预览现在应该显示一张圆形的卡片!

Screenshot-2020-01-04

卡片已初具规模,但它缺少两件事:名称组件上的一些基本样式以及组件之间的一些间距。你将在下一步中处理上述问题。

优化 RecipeCard 的 UI

首先在食谱名称 Text() 中添加文本样式:

Text(recipe.title, style = MaterialTheme.typography.h4)

在这里,你使用 style 参数通过默认的 MaterialTheme 排版标题样式来设置文本样式。

Screenshot-2020-01-04

为 RecipeCard 添加间距

要将间距添加到卡片,请将名称和原料 Text() 包装在另一个 Column() 中并添加以下修饰符:

Column(modifier = Modifier.padding(16.dp)) {
  Text(recipe.title, style = MaterialTheme.typography.h4)
  for (ingredient in recipe.ingredients) {
    Text("• $ingredient")
  }
}

除了使用修饰符来控制宽度和高度之外,你还可以使用它们为不同的 @Composable 添加间距。

刷新预览。你应该看到一张漂亮的食谱卡片:

Screenshot-2020-01-04

你的食谱卡片现在看起来很棒。是时候让你的用户能够列出他们最喜欢的食谱了。

创建一个食谱列表

通常,你使用 RecyclerView 来创建一个列表。在 Jetpack Compose 中,你可以使用 LazyColumnFor @Composable 来实现延迟实例化子项的滚动列表,就像使用 RecyclerView 一样,但只需少量的代码!

要实现食谱列表,再次 右键单击 根代码包,然后选择 New ▸ New Kotlin File/Class。然后从列表中选择 File 并输入文件名称 RecipeList。最后,将 RecipeList() 添加到文件中:

@Composable
fun RecipeList(recipes: List<Recipe>) {
  LazyColumnFor(items = recipes) { item ->
    RecipeCard(item)
  }
}

确保导入缺少的引用。LazyColumnFor @Composable 接受一个子项列表和一个带有一个参数的 @Composable lambda —— 参数是列表中的单个子项。然后你可以使用这个子项来构建你的滚动列表。可以看到这里没有 ViewHolderAdapter

现在你有一个可组合函数来显示你的食谱列表,是时候将所有内容连接到你的 MainActivity 中并在设备上查看你的工作成果了!

组合起来

打开到 MainActivity 并将 setContent() 的内容替换为以下内容:

RecipeList(defaultRecipes)

编译并运行。你会看到令人垂涎欲滴的美味食谱列表。

cluttered

App 看起来还不错,但是你无法真正辨别每个食谱卡片。要使 UI 看起来清晰明了,你需要在子项之间添加一些间距。

再次打开 RecipeCard 文件,为 RecipeCard 函数添加一个 Modifier 参数,将此参数传递给 Surface

fun RecipeCard(recipe: Recipe, modifier: Modifier) {
  Surface(shape = RoundedCornerShape(8.dp), elevation = 8.dp, modifier = modifier) {
  ...
  }
}

然后修改 DefaultRecipeCard 预览函数,增加一个修饰符:

@Composable
@Preview
fun DefaultRecipeCard() {
  RecipeCard(defaultRecipes[0], Modifier.padding(16.dp))
}

最后,打开 RecipeList 文件,并为 RecipeCard 增加一个 16dp 的间距修饰符。RecipeList 函数现在应该如下所示:

@Composable
fun RecipeList(recipes: List<Recipe>) {
  LazyColumnFor(items = recipes) { item ->
    RecipeCard(item, Modifier.padding(16.dp))
  }
}

编译并运行。你应该会看到一个间距合理的美味食谱列表。

paddingrific

现在 App 唯一缺少的是工具栏!你将在最后一步添加一个工具栏。

添加工具栏

拥有工具栏是 Android 应用程序的默认行为,所以我们就必须要添加它!使用下面的代码替换 MainActivity 文件中 setContent() 的内容:

// 1
Column(modifier = Modifier.fillMaxSize()) {
  // 2
  TopAppBar(title = {
    Text("ComposableCookBook")
  })
  // 3
  RecipeList(defaultRecipes)
}

以下是对上述代码的解释:

  1. 就像你之前使用 Column 一样,但是这次你需要通过使用 fillMaxSize() 指定一个 Modifier 来告诉它填充所有可用的空间。
  2. 接下来你将使用 TopAppBar @Composable 可组合函数来添加一个工具栏。请注意,不仅仅是将 String 传递给工具栏,而是需要传递另一个 @Composable 可组合函数。这可以使你随心所欲的控制工具栏的显示内容!对于这个 App,一个简单的 Text 就足以满足你对工具栏的需求。
  3. 最后,你将 RecipeList 添加到 TopAppBar 的下方。

编译并运行,你会在顶部看到一个漂亮的带有 ComposeableCookBook 字样的新工具栏。

toolbar-1

恭喜!你已经使用 Jetpack Compose 构建了你的第一个 App。

总结

你现在已经体验了 Android 用户界面世界的一些最新和最伟大的变化。但是你只是了解了 Compose 提供的一些基本功能,并且在 Compose 稳定之前,其中许多 API 必然会发生重大变化。

在本教程中,你了解了:

  1. Jetpack Compose 背后的理念和初衷,
  2. 如何使用 @Composable 注解构建可组合函数,
  3. 如何使用 @Preview 注解预览可组合函数,
  4. 通过 Column 可组合函数了解基本的布局,
  5. 如何使用 MaterialThemes 为你的组件添加主题样式。

Jetpack Compose Roadmap