# 将架构蓝图项目迁移至 Jetpack Compose

**URL:** <https://meta.101.dev/t/jetpack-compose/657>\
**Category:** Android 开发\
**Tags:** android\
**Created:** [2022 年6 月 13 日 01:52 UTC](https://meta.101.dev/t/jetpack-compose/657 "2022-06-13T01:52:00Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![101](https://meta.101.dev/user_avatar/meta.101.dev/101/32/575_2.png) [@101](https://meta.101.dev/u/101)\
发布日期： [2022 年6 月 13 日 01:52 UTC](https://meta.101.dev/t/jetpack-compose/657/1 "2022-06-13T01:52:00Z")

</div>

![](https://devrel.andfun.cn/devrel/posts/2022/06/Fu1ehf.jpg)

_作者 / Manuel Vivo, Android DevRel @ Google_

在我们努力实现 [应用架构指南](https://developer.android.google.cn/topic/architecture) 现代化的过程中，我们希望尝试各种用户界面模式，了解哪个模式最有效，找出替代方案之间的相似性和差异，并最终将这些内容整合为最佳实践。

为了让我们的结果尽可能易于理解，我们需要一个不太复杂的样本，并基于大家熟悉的商业案例。于是，我们选择了热门的 _TODO_ 类应用。并在 [架构蓝图](https://github.com/android/architecture-samples) (Architecture Blueprints) 项目中来制作示例！架构蓝图以前本就是用于挑选架构的实验性项目，这正好完美契合了我们的需求！

![△ 架构蓝图应用演示](https://devrel.andfun.cn/devrel/posts/2022/06/y7otPy.gif)

△ 架构蓝图应用演示

我们想要尝试的模式显然受到了现今可用的多种 API 的影响。而我们这次要使用的是新推出的 [Jetpack Compose State API](https://developer.android.google.cn/jetpack/compose/state)！由于 Compose 可与任何 [单向数据流模式](https://developer.android.google.cn/jetpack/guide/ui-layer#udf) 无缝衔接使用，因此我们将用 Compose 来渲染界面，让比较更加公平。

**这篇文章介绍了我们的团队如何将架构蓝图迁移到 Jetpack Compose** 。由于 [LiveData](https://developer.android.google.cn/topic/libraries/architecture/livedata) 也被视为我们实验中的备选方案，因此在迁移时，我们将样本保留原样。 **在这次重构中，ViewModel 类和数据层都未经改动** 。

**⚠** 请注意: 在基于 LiveData 的代码库中使用的架构，并未完全遵循 [最新的架构最佳实践](https://developer.android.google.cn/jetpack/guide)。特别是，LiveData 不应该用于 [数据层](https://developer.android.google.cn/jetpack/guide/data-layer) 或 [网域层](https://developer.android.google.cn/jetpack/guide/domain-layer)，而应该采用 Flow 和协程。

现在项目背景已经明确，让我们来深入探究如何使用 Jetpack Compose 重构蓝图项目。您可以在 [dev-compose](https://github.com/android/architecture-samples/tree/dev-compose) 上查看完整代码。

## **✍ 规划逐步迁移**

在进行任何实际编码工作前，团队首先制定了一个迁移计划，以确保每个人都接受提出的更改意见。最终目标是让蓝图成为单一 Activity 应用，其各个屏幕为可组合函数，并使用推荐的 [Compose Navigation](https://developer.android.google.cn/jetpack/compose/navigation) 库在屏幕之间移动

幸运的是，蓝图已经是单一 Activity 应用，且使用 [Jetpack Navigation](https://developer.android.google.cn/guide/navigation) 在通过 Fragment 实现的不同屏幕之间移动。为了迁移到 Compose，我们遵循 Navigation [互操作性指南](https://developer.android.google.cn/jetpack/compose/navigation#interoperability)，该指南建议混合型应用使用基于 Fragment 的 Navigation 组件，并使用 Fragment 来容纳基于视图的屏幕、Compose 屏幕，以及同时使用二者的屏幕。遗憾的是，您无法在同一 Navigation 图中混用 Fragment 和 Compose 目的地。

逐步迁移的目的是减少代码审查工作量，并在整个迁移过程中保持产品可交付。迁移计划涉及三个步骤:

- 将每个屏幕的 _内容_ 迁移至 Compose。每个屏幕均可单独迁移至 Compose，包括其界面测试。然后 Fragment 将成为每个已迁移屏幕的容器。
- 将应用迁移至 Navigation Compose (此操作会移除项目中的所有 Fragment) 并将 Activity 界面逻辑迁移至基于 Composable。端到端测试也会在此时迁移。
- 移除 View 系统依赖项。

我们也是这样操作的！时间快进到两周后，我们迁移了 [统计信息 (Statistics) 屏幕](https://github.com/android/architecture-samples/pull/821)、[添加/编辑任务 (Add/Edit task) 屏幕](https://github.com/android/architecture-samples/pull/823)、[任务详细信息 (Task detail) 屏幕](https://github.com/android/architecture-samples/pull/822)，以及 [任务 (Tasks) 屏幕](https://github.com/android/architecture-samples/pull/826)；同时我们合并了 [最终 PR](https://github.com/android/architecture-samples/pull/827)，此操作将 Navigation 和 Activity 逻辑迁移至 Compose，包括 [移除未使用的 View 系统依赖项](https://github.com/android/architecture-samples/pull/827/commits/2810a37c479ef4b23b4cabf095c55df7b342235e)。

![△ 我们如何将蓝图逐步迁移至 Compose](https://devrel.andfun.cn/devrel/posts/2022/06/NgqFGy.png)

△ 我们如何将蓝图逐步迁移至 Compose

## **💡 迁移重点**

迁移过程中，我们遇到了一些针对 Compose 的问题，值得重点讲述:

### **🧪 界面测试**

将 Compose 添加到应用后，断言 Compose 界面的测试需要使用 [Compose 测试 API](https://developer.android.google.cn/jetpack/compose/testing)。

**对于屏幕级别的界面测试** ，我们不使用 [launchFragmentInContainer API](https://developer.android.google.cn/guide/fragments/test#create)，而是使用 [createAndroidComposeRule API](https://developer.android.google.cn/jetpack/compose/testing)，这样我们可以在测试中捕获字符串资源。 **这些测试可在 Espresso 和 Robolectric 中运行** 。因为 Compose 已经可为所有这一切提供支持，所以无需任何额外改动。例如，您可以比较 [AddEditTaskFragmentTest](https://github.com/android/architecture-samples/blob/653a563e9fe0874b4ae3fba539ce4b6518a2f796/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragmentTest.kt) 中已迁移至 [AddEditTaskScreenTest](https://github.com/manuelvicnt/architecture-samples/blob/8a203594541b25e5eec2daac63415c05884242ad/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt) 的代码。请注意，如果您使用 ComponentActivity，那么需要依赖 [androidx.compose.ui:ui-test-manifest](https://developer.android.google.cn/jetpack/compose/testing#setup) 组件。

**对于端到端到集成测试** ，我们也未发现任何问题！得益于 **Espresso 和 Compose 的互操作性** ，我们可以使用 Espresso 断言来查看 View，使用 Compose API 来查看 Compose 界面。您可以实际查看迁移至 Compose 期间某一时刻的 [AppNavigationTest](https://github.com/manuelvicnt/architecture-samples/blob/249a636ea9a3f16aab5c284e3245069ef56a557f/app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt)。

### **🤙 ViewModel 事件**

对于在蓝图中 [处理 ViewModel 事件](https://developer.android.google.cn/jetpack/guide/ui-layer/events#handle-viewmodel-events) 的方式，我们确实遇到过问题。蓝图采用了 [事件封装容器](https://github.com/android/architecture-samples/blob/8e1e0527a0d043b41da58925a39fb8e03d62829a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/Event.kt) 解决方案，将_命令_从 ViewModel 发送到界面。但是，这在 Compose 中并不好用。最新的指南建议将这些 “事件” 建模为状态，我们在迁移中也是这么做的。

让我们看看_在屏幕上显示消息_的事件用例，我们将 LiveData 的 Event 类型替换为 Int?。这同样对没有要向用户显示任何消息的场景进行了建模。在这一特定用例中，当消息被显示时，ViewModel 还需要获得来自界面的确认。在下面的代码中可以看出两种实现之间的代码差异 (diff)。

```Kotlin
/* Copyright 2022 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

class AddEditTaskViewModel(
  private val tasksRepository: TasksRepository
) : ViewModel() {

- private val _snackbarText = MutableLiveData<Event<Int>>()
- val snackbarText: LiveData<Event<Int>> = _snackbarText

+ private val _snackbarText = MutableLiveData<Int?>()
+ val snackbarText: LiveData<Int?> = _snackbarText

+ fun snackbarMessageShown() {
+ _snackbarText.value = null
+ }
}

```

尽管乍一看似乎工作量变大了，但它能 **保证** 消息会在屏幕上显示！

在界面代码中，确保事件只处理一次的方法是调用 `event.getContentIfNotHandled()`。这种方法在 Fragment 中还算行得通，但 **在 Compose 中就完全失效了** (如果您编写的是完全原生的 Compose 代码的话)！因为在 Compose 中随时可能发生重新组合，事件封装容器并非有效的解决方案。如果在事件处理后，函数被重新组合 (在测试中经常发生这种现象)，那么信息提示控件 (snackbar) 将被取消，用户可能会错过消息。这是一个无法接受的用户体验问题。 **事件封装容器解决方案不应在 Compose 应用中使用** 。

请注意，您可以写出在某些情况下避免重新组合部分函数的 Compose 代码，然而，事件包装器解决方案限制了用户界面的实现方式。 **我们不鼓励大家在 Compose 中使用事件封装器解决方案** 。

请查看以下带有 “之前” (事件封装容器) 和 “之后” (事件作为状态) 对照的代码片段。因为在屏幕上显示消息是 [界面逻辑](https://developer.android.google.cn/jetpack/guide/ui-layer#logic-types)，而我们的屏幕可组合项变得越来越复杂，因此使用 [纯状态容器类](https://developer.android.google.cn/jetpack/compose/state#types-of-state-and-logic) 来管理此复杂性 (比如 [AddEditTaskState](https://github.com/manuelvicnt/architecture-samples/blob/88cf650fd1759486cce198878b5cf08e823012dc/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskState.kt))。

```Kotlin
/* Copyright 2022 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

// FRAGMENTS CODE CONSUMING THE EVENT WRAPPER SOLUTION

- class AddEditTaskFragment : Fragment() {
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ...
- viewModel.snackbarText.observe(
- lifecycleOwner,
- Observer { event ->
- event.getContentIfNotHandled()?.let {
- showSnackbar(context.getString(it), Snackbar.LENGTH_SHORT)
- }
- }
- )
- }
- }

// COMPOSE CODE CONSUMING USER MESSAGES AS STATE

// State holder for the AddEditTask composable.
// This class handles AddEditTask's UI elements' state and UI logic.
+ class AddEditTaskState(...) {
+ init {
+ // Listen for snackbar messages
+ viewModel.snackbarText.observe(viewLifecycleOwner) { snackbarMessage ->
+ if (snackbarMessage != null) {
+ // If there's a previous message showing on the screen
+ // stop showing it in favor of the new one to be displayed
+ currentSnackbarJob?.cancel()
+ val snackbarText = context.getString(snackbarMessage)
+ currentSnackbarJob = coroutineScope.launch {
+ scaffoldState.snackbarHostState.showSnackbar(snackbarText)
+ viewModel.snackbarMessageShown()
+ }
+ }
+ }
+ }

```

### **👌 请优先确保应用正确性**

重构期间，您可能很想把手上的 _所有内容_ 迁移到 Compose。虽然这么做完全没问题，但您不应牺牲应用的用户体验或正确性。逐步迁移的全部意义在于，让应用始终处于可交付状态。

在将一些屏幕迁移到 Compose 时，我们也遇到了这种情况。我们不想同时进行过多迁移，所以在从事件封装容器迁移 “之前”，先将一些屏幕迁移到了 Compose。与其在 Compose 中处理事件封装容器，获得不够理想的体验，不如继续在 Fragment 中处理这些消息，而屏幕的其他代码则使用 Compose 实现。例如，您可以参考 [迁移过程中 TasksFragment 的状态](https://github.com/manuelvicnt/architecture-samples/blob/249a636ea9a3f16aab5c284e3245069ef56a557f/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt)。

## **🧐 挑战**

不是所有步骤都像看上去那么顺利。尽管将 Fragment 内容转换为 Compose 很简单，但从 Navigation Fragment 迁移到 Navigation Compose 需要花费更多的时间和心思。

我们有必要从各方面扩展和改进指南，让迁移到 Compose 的过程更加轻松。这项工作引起了广泛讨论，我们希望很快制定出这方面的全新指南！🎊

我在初次使用 Navigation ✋ 并处理向 Navigation Compose 迁移的问题时，面临了以下挑战:

- 文档中没有任何代码显示如何 **使用可选参数进行导航** ！多亏有 [Tivi 的导航图](https://github.com/chrisbanes/tivi/blob/main/app/src/main/java/app/tivi/AppNavigation.kt)，我才找到办法解决这个问题。您可以 [关注此问题并改进文档](https://issuetracker.google.com/226103829)。

- **从基于 XML 的导航图和 SafeArgs 迁移到 Kotlin DSL** 应该是一项简单的机械式任务。但对我来说这项任务并不轻松，因为我并没有参与初始实现。一些有关如何正确操作的指南本应对我有所帮助。您可以 [关注此问题并改进文档](https://issuetracker.google.com/226315955)。

- 第三点与其说是挑战，不如说这就是一个问题。说到导航，[NavigationUI](https://developer.android.google.cn/guide/navigation/navigation-ui) **已经为您做了一些工作** 。由于 Compose 中不存在该界面，您需要注意这一点，并手动实现。例如，在 Drawer 屏幕之间导航时，保持后退堆栈的清洁需要特殊的 NavigationOptions (请参考 [示例](https://github.com/android/architecture-samples/blob/dev-compose/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt#L79))。[文档](https://developer.android.google.cn/jetpack/compose/navigation#bottom-nav) 中已经讲到了这一点，但您需要意识到自己需要这么做！

## **🧑‍🏫 小结**

总的来说，从 Navigation Fragment 迁移到 Navigation Compose 是一项有趣的工作！有意思的是，我们花在等待同行审查上的时间，比迁移项目本身的时间还要多！制定迁移计划并让每个人都切实理解它，无疑有助于尽早确定期望结果，并提醒同事注意即将到来的漫长审查。

希望这篇文章对您有所帮助，让您了解了我们迁移到 Compose 的方法，同时我们期待分享更多我们在架构蓝图中进行的实验和改进。

如果您有兴趣了解 Compose 版的蓝图代码，请查看 [dev-compose](https://github.com/android/architecture-samples/tree/dev-compose)。

如果您想浏览逐步迁移的 PR，请查看以下列表:

- [统计信息](https://github.com/android/architecture-samples/pull/821) (Statistics) 屏幕
- [添加/编辑任务](https://github.com/android/architecture-samples/pull/823) (Add/Edit task) 屏幕
- [任务详细信息](https://github.com/android/architecture-samples/pull/822) (Task detail) 屏幕
- [任务](https://github.com/android/architecture-samples/pull/826) (Tasks) 屏幕
- 以及 [最终 PR](https://github.com/android/architecture-samples/pull/827)，此操作将 Navigation 和 Activity 逻辑迁移至 Compose，包括 [移除未使用的 View 系统依赖项](https://github.com/android/architecture-samples/pull/827/commits/2810a37c479ef4b23b4cabf095c55df7b342235e)。

欢迎您 [点击这里](https://go2.gdsub.com/androiddevfb) 向我们提交反馈，或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要，感谢您的支持！
