有关 Android 架构的建议

本页介绍了一些与架构有关的最佳实践和建议。采用这些最佳实践和建议不仅可以提高应用的质量、稳健性和可伸缩性,还可以让您的应用更便于维护和测试。

以下最佳实践按主题分组。每项最佳实践都具有对应的优先级,反映了建议的程度。优先级列表如下:

  • 强烈建议:除非与您的做法发生根本冲突,否则您应该按照相应实践的要求操作。
  • 建议:按照相应实践的要求操作很有可能让您的应用变得更优秀。
  • 可选:按照相应实践的要求操作在某些情况下能让您的应用变得更优秀。

分层架构

我们建议采用的分层架构有助于实现关注点分离。这种架构可以通过数据模型来驱动界面,符合单一可信来源原则,也符合单向数据流原则。以下是一些与分层架构有关的最佳实践:

建议 说明
使用明确定义的数据层 数据层用于向应用的其余部分公开应用数据,并且包含应用的绝大部分业务逻辑。
  • 创建代码库,即使它们只包含一个数据源也不例外。
  • 在小型应用中,您可以选择将数据层类型放置在 data 软件包或模块中。
使用明确定义的界面层 界面层用于在屏幕上显示应用数据,并充当主要的用户互动点。Jetpack Compose 是推荐用于构建应用界面的新工具包。
  • 在小型应用中,您可以选择将数据层类型放置在 ui 软件包或模块中。
如需详细了解界面层最佳实践,请参阅界面层
使用代码库从数据层公开应用数据。

确保界面层中的组件(如可组合项或 ViewModel)不会直接与数据源交互。数据源示例包括:

  • 数据库、DataStore、SharedPreferences、Firebase API。
  • GPS 位置信息提供程序。
  • 蓝牙数据提供程序。
  • 网络连接状态提供程序。
使用协程和数据流 使用协程和数据流在层之间进行通信。

如需详细了解协程最佳实践,请参阅在 Android 中使用协程的最佳实践

使用网域层 如果您需要在多个 ViewModel 中重复使用与数据层交互的业务逻辑,或者想要简化特定 ViewModel 业务逻辑的复杂程度,请使用包含用例的网域层

界面层

界面层的作用是在屏幕上显示应用数据,并充当主要的用户互动点。以下是一些有关界面层的最佳实践:

建议 说明
遵循单向数据流 (UDF) 原则。 遵循单向数据流 (UDF) 原则,即 ViewModel 使用观察者模式来公开界面状态,并通过方法调用接收来自界面的操作。
如果 AAC ViewModel 的优势适用于您的应用,请加以使用。 使用 AAC ViewModel 处理业务逻辑,并提取应用数据以向界面公开界面状态。

如需详细了解 ViewModel 最佳实践,请参阅架构建议

如需详细了解 ViewModel 的优势,请参阅将 ViewModel 用作业务逻辑状态容器

使用生命周期感知型界面状态收集方式。 使用适当的生命周期感知型协程构建器 collectAsStateWithLifecycle 从界面收集界面状态。

详细了解 collectAsStateWithLifecycle

请勿将来自 ViewModel 的事件发送到界面。 在 ViewModel 中立即处理事件,并通过事件的处理结果引发状态更新。如需详细了解界面事件,请参阅处理 ViewModel 事件
使用单 activity 应用。 如果您的应用包含多个屏幕,请使用 Navigation 3 在屏幕以及指向您应用的深层链接之间导航。
使用 Jetpack Compose 使用 Jetpack Compose 为手机、平板电脑、可折叠设备和 Wear OS 构建新应用。

以下代码段简要说明了如何以生命周期感知型方式收集界面状态:

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

ViewModel 负责提供界面状态和访问数据层。以下是一些有关 ViewModel 的最佳实践:

建议 说明
使 ViewModel 独立于 Android 生命周期。 在 ViewModel 中,请勿存储对任何与生命周期相关的类型的引用。请勿将 ActivityContextResources 作为依赖项传递。如果某元素需要在 ViewModel 中使用 Context,请仔细评估其是否位于正确的层中。
使用协程和数据流

ViewModel 通过以下方式与数据层或网域层交互:

  • 通过 Kotlin 数据流接收应用数据
  • 用于通过 viewModelScope 执行操作的 suspend 函数
在屏幕级别使用 ViewModel。

请勿在可重复使用的界面部分中使用 ViewModel。您应该在以下位置使用 ViewModel:

  • 屏幕级别的可组合项;
  • View 中的 activity/fragment;
  • 使用 Jetpack Navigation 时的目的地或图表。
在可重复使用的界面组件中使用普通状态容器类 使用普通状态容器类处理可重复使用的界面组件中的复杂工作。这样即可从外部对状态进行提升和控制。
请勿使用 AndroidViewModel 使用 ViewModel 类,而非 AndroidViewModel。请勿在 ViewModel 中使用 Application 类。正确做法是将依赖项移至界面层或数据层。
公开界面状态。 让您的 ViewModel 通过名为 uiState 的单个属性向界面公开数据。如果界面显示多块不相关的数据,虚拟机可能会公开多个界面状态属性
  • uiState 设为 StateFlow
  • 如果数据作为来自层次结构中的其他层的数据流传入,您应该使用 stateIn 运算符和 WhileSubscribed(5000) 政策来创建 uiState。(请参阅此代码示例。)
  • 如果没有来自数据层的数据流,则属于较简单的情况,可以使用作为不可变的 StateFlow 公开的 MutableStateFlow
  • 您可以选择将 ${Screen}UiState 作为能够包含数据、错误和加载信号的数据类。如果不同状态是互斥的,该类也可以是密封的类。

以下代码段简要说明了如何从 ViewModel 公开界面状态:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

生命周期

遵循有关如何使用 Activity 生命周期的最佳实践:

建议 说明
在可组合函数中使用生命周期感知型效应,而不是替换 Activity 生命周期回调。

请勿替换 Activity 生命周期方法(例如 onResume)来运行与界面相关的任务。请改用 Compose 的 LifecycleEffects 或生命周期感知型协程范围:

以下代码段简要说明了如何在特定生命周期状态下执行操作:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

处理依赖关系

在管理组件之间的依赖关系时,请遵循以下最佳实践:

建议 说明
使用依赖项注入 尽可能使用依赖项注入最佳实践,主要是构造函数注入
在必要时将作用域限定为某个组件。 如果类型包含多项需要共享的可变数据,或者类型初始化开销高昂且在应用中广泛使用,则将作用域限定为某个依赖项容器
使用 Hilt 在简单应用中使用 Hilt手动依赖项注入。如果您的项目足够复杂,则使用 Hilt。例如,如果项目包含以下任一项:
  • 有多个包含 ViewModel 的屏幕
  • 使用 WorkManager
  • 具有作用域限定为导航返回堆栈的 ViewModel

测试

以下是一些有关测试的最佳实践:

建议 说明
了解要测试的内容

除非项目像“Hello World”应用一样简单,否则请对其进行测试。至少应包含以下内容:

  • 针对 ViewModel(包括 Flow)的单元测试
  • 针对数据层实体(即存储库和数据源)的单元测试
  • 进行界面导航测试(可在持续集成环境中用作回归测试)
尽量采用虚假实现,而非模拟实现。 如需详细了解如何使用伪对象,请参阅 Android 文档中的“使用测试替身”
测试 StateFlow。 测试 StateFlow 时,请执行以下操作:

如需了解详情,请参阅 Android 中要测试的内容测试 Compose 布局

模型

在应用中开发模型时,请遵循以下最佳实践:

建议 说明
对于复杂应用,要为每个层创建一个模型。

在复杂应用中,必要时可以在不同的层或组件中创建新模型。请参考以下示例:

  • 远程数据源可以将通过网络接收的模型映射到仅包含应用所需的数据的简单类。
  • 代码库可以将 DAO 模型映射到仅包含界面层所需的数据的简单数据类型。
  • ViewModel 可以在 UiState 类中包含数据层模型。

命名惯例

为代码库命名时,您应了解以下最佳实践:

建议 说明
命名方法。
可选
使用动词短语来命名方法,例如 makePayment()
为属性命名。
可选
使用名词短语来命名属性,例如 inProgressTopicSelection
为数据流命名。
可选
如果某个类公开了 Flow 流或任何其他流,则命名惯例为 get{model}Stream。例如 getAuthorStream(): Flow<Author>。如果函数返回模型列表,请使用复数模型名称:getAuthorsStream(): Flow<List<Author>>
为接口实现命名。
可选
为接口实现使用有意义的名称。如果找不到更好的名称,请使用 Default 作为前缀。例如,对于 NewsRepository 接口,您可以使用 OfflineFirstNewsRepositoryInMemoryNewsRepository。如果找不到合适的名称,请使用 DefaultNewsRepository。 为虚假实现添加前缀 Fake,例如 FakeAuthorsRepository

其他资源

如需详细了解 Android 架构,请参阅下面列出的其他资源:

文档

查看内容