​Android 官方现代 App 架构指南 | 开发者说·DTalk

302736d56e540c53be7c7548fcf994f5.jpeg

本文原作者: madroid,原文发布于: Rethink Android 

2463584895ba22237218300edd486e14.png

回顾

根据 App 行为的不同,我们对其进行分离/分层并确定其职责,每层之间的通讯交互采用响应式方式。

App 有三层结构,分别为 UI Layer、Domain Layer、Data Layer,其依赖关系是单向的,上层可以依赖下层,下层却不能反过来依赖上层。大致如下,其中 Domain Layer 是可选层: 

87b14af3804a04b44fe990a442252aed.png

△ App Arch Layer Design

每层的主要职责分别为: 

  • UI Layer: 使用 UI 元素展示 App 中数据

    • 将底层数据处理成容易被 UI elements 使用的 UiState 数据;

    • 根据 UiState 绘制对应的 UI elements;

    • 响应用户的操作事件,根据需求对其进行分发;

  • Domain Layer: 封装通用的业务逻辑

    • 封装复杂的业务逻辑,避免出现大型类;

    • 封装多 ViewModel 通用的业务逻辑,避免代码重复;

  • Data Layer: 封装统一的数据来源,提供单一可信来源

    • 定义不同的 DataSource 来封装 Framework 及三方 SDK 的 API;

    • 定义 Repository 来整合相同业务的不同数据类型的 DataSource;

每层依赖关系是单向的,UI Layer 可以依赖 Domain Layer,但是 Domain Layer 却不能依赖 UI Layer。这种依赖方式可以使用简单的函数传递依赖事件,但是却不能处理结果的回调,即 UiState 的更新。想要处理结果的回调每层之间就可以采用数据驱动/响应式的方式来交互了。这种方式也被称为是单向数据流的方式,及 UI 事件从 UI 层流向数据层,UiState 从数据层流向 UI 层。

f4cb695e188e0437b89fee45d5f78d48.gif

关于 UI Layer、Domain Layer、Data Layer 中更多详细内容可以查看官方文档应用架构指南:

https://developer.android.google.cn/jetpack/guide

f290f90484789e60f88983e3931c6284.png

与 MVI 的关系?

MVI 的全称是 Model-View-Intent,这里的 Intent 并不是指 Android 中的 Intent 类,而是表示一种意图,可以简单理解为对用户 Event 的一种抽象。其交互图大致如下: 

3e25fbaa3a551e99b01fedd3aa1ea934.png

MVI 并不像 MVC、MVP、MVVM 一样,不论是 Controller、Presenter 还是 ViewModel 都是 View 与 Model 的之间的桥接类,负责这两者之间的通信与交互 (虽然 MVC 可以跨过 Controller 直接进行交互)。而 Intent 并没有类似的职责,仅仅是约束了 View 的事件通过类似枚举的方式定义,这种方式更像是前端框架中的 Flux 或者是 Redux,更多内容可以查看 Reclaim the reactivity of your state management, say no to imperative MVI,实现 MVI 的主流框架有: OrbitMavericksUniflow-ktMobius

  • Reclaim the reactivity of your state management, say no to imperative MVI
    https://zhuinden.medium.com/reclaim-the-reactivity-of-your-state-management-say-no-to-imperative-mvi-3b23ca6b8537

  • Orbit
    https://github.com/orbit-mvi/orbit-mvi/blob/06e9f759a87e7192767baeebc682fc92369a7eff/orbit-core/src/commonMain/kotlin/org/orbitmvi/orbit/internal/RealContainer.kt#L74-L75

  • Mavericks

    https://github.com/airbnb/mavericks/blob/e8a631a19fc1b044da3ddff358712e129dc487a6/mvrx/src/main/kotlin/com/airbnb/mvrx/CoroutinesStateStore.kt#L57-L59

  • Uniflow-kt

    https://github.com/uniflow-kt/uniflow-kt/blob/a1fdbeb733a0b550a162227be3b1e03d03197023/uniflow-core/src/main/kotlin/io/uniflow/core/flow/ActionReducer.kt#L28-L32

  • Mobius
    https://github.com/spotify/mobius

有的 MVI 在实现还需要借助 ViewModel,仅仅是把 View 的事件定义成的对应的密封类。目的仅仅是为了强制实现单向数据流的方式,根据之前介绍实现单向数据流的方式还是比较简单的,上层只能依赖下层实现,下层的处理结果通过 LiveData、Flow 方式更新。

那再来聊一下 MVC、MVP、MVVM 与 Android 官方的推荐的 MAD Arch 之间的关系。其实经常提到的 MVVM 与 Android 官方的架构还是有本质区别的。MVX  (对 MVC、MVP、MVVM 的统称) 的架构方式对 Model 这一层提到的非常少,留下的印象可能就是除了 VX 之外剩下的就是 Model 的部分。但是这部分在整个 App 的架构中也是非常重要的。我们还是有大量的业务逻辑是在 Model 层处理的。

2a130e9ac1c007f0ed2b4fad127daf0e.gif

而 Android 官方的架构中却包含了这部分的描述,新增了 Data Layer 与 Domain Layer。所以总结下来就是 MVX 处理的仅仅是 UI Layer 中的问题,描述的是状态管理的部分;官方文档中描述的确是整个 App 的架构,是一种包含的关系。

33e7beee19285281aabed3c3f98d7960.png

如何处理线程?

无论是在哪一层都要确保其在主线程安全的,即在主线程调用不会阻塞主线程或者是抛出异常。那应该是在哪一层进行处理呐?其可选项有 ViewModel、UseCase、Repository、DataSource,只要在任何一层处理耗时操作都可以确保其是主线程安全的。这里建议采用 "就近原则",即谁产生数据谁就保持数据的安全性。

Data Layer 中 DataSource 是 "产生" 数据的地方,在这里直接切换到对应的子线程是可以的,代码大致如下: 

class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * 在 IO 线程中,获取网络数据,在主线程调用是安全的
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> = withContext(ioDispatcher) {
        // 将耗时操作移动到 IO 线程中
        newsApi.fetchLatestNews()
    }
}

如果 Repository 中需要整合很多的 DataSource 中的数据,在 Repository 中切换到对应的子线程也是可以的,这样可以减少频繁的线程调度。

同时也需要考虑响应业务的生命周期情况,如果当前业务跟随这页面进行的,那么使用 viewModelScope 或者是 lifecycleScope 即可;如果其业务是跟随 App 的什么周期的,那么则需要使用整个 App 生命周期的 CoroutineScope;如果在 App 被终止后,仍然希望可以执行任务,那么可以考虑使用 WorkManager: 

https://developer.android.google.cn/topic/libraries/architecture/workmanager

5512982ead4d43ffbb68f573c0ed94fb.png

如何处理实体类 (Entity)?

各层之间的 Entity 根据其职责定义会有所不同,可以根据具体的使用场景自定义 Entity。如云端返回的 Entity 与数据库需要存储的 Entity 可能并不相同,使用相同的 Entity 会导致代码的可维护性下降,而且没有必要暴露过多的细节。如下:

@Entity(tableName = "user")
data class RemoteUser(
    @PrimaryKey
  @SerializedName("user_id")
    val userId: String,
    val username: String,
    @Ignore
    val token: String,
    @Ignore
    val inventory: RemoteInventory,
    @Ignore
    val profile: RemoteProfile,
)

这种场景下,我们就可以针对云端返回数据与数据库存储数据分别定义不同的 Entity,如下: 

//  云端数据 Entity
data class RemoteUser(
  @SerializedName("user_id")
    val userId: String,
    val username: String,
    val token: String,
    val inventory: RemoteInventory,
    val profile: RemoteProfile,
)


// 数据库 Entity
@Entity(tableName = "user")
data class UserEntity(
    @PrimaryKey
    val userId: String,
    val username: String,
)

对于不同页面直接传递数据的场景 (Intent),建议定义单独的 Entity,因为传递数据的大小是有限的。定义大致如下:

@Parcelize
data class Inventory(
    val id: UUID,
    val type: String
): Parcelable

对于 UI Layer 中的实体定义,要根据其业务类型进行细分,切记不要将一页面中的所有的 UiState 都定义在同一个 Entity 中。因为汇总型的定义在相关字段的更新频率不一致的时候会导致频繁的 UI element 重复绘制,同时不可变的 Entity 的字段增加也会导致不必要的内存开销。如果一个 UiState 中有超过 5 个状态,那就需要回过来看下 UiState 是否可以进行拆分了。

UiState 中经常遇到的一个场景就是添加 Loading 状态,这种情况添加封装统一的 Wrapper 类进行处理,如下: 

sealed interface UiStateWrapper {
    object Loading : UiStateWrapper
    class Success<T>(val uiState: T) : UiStateWrapper
    class Failure(val exception: Throwable) : UiStateWrapper
}

这种处理方式,并不需要在 UiState Entity 新增一个 isLoading 字段,保持 UiState 的 "纯洁性",同时也可以在 UI elements 中对 UiStateWrapper 做统一的处理,不必每个 UiState 中都出 Loading 的状态,当然,这是在 Loading 处理逻辑相同的前提下的。

整体而言,根据不同职责定义不同的 Entity 会让我们的代码逻辑相对合理,但是会增加一定的工作量以及会对要使用何种 Entity 产生混淆。所以还是需要根据自己的项目及团队情况决定是否需要精细化管理 Entity,大型团队建议采用这种方式。

76444651e14580fda5eed6f41c3f8c0f.png

如何组织代码?

代码建议按照业务模块方式进行组织,而非功能进行组织。大致如下: 

# DO
- Project
 - feature1
  - ui
  - domain
  - data
 - feature2
  - ui
  - domain
  - data
  - feature3

不要使用如下的方式:

# DO NOT
- Project
 - ui
  - feature1
  - feature2
  - feature3
 - domain
  - feature1
  - feature2
  - feature3
  - data

采用 Feature 方式组织代码的优势大致有以下几点: 

  1. 我们大概率都是在已有的项目中开发,而历史的项目中或多或少存在着一些历史技术债务,我们可以在开发特性的时候引入新的技术,这样不会对旧的目录结构产生过多影响;

  2. 后续可以很方便的对该特性进行改造,比如可以把这个文件夹移到一个单独的 module 中进行模块化相关的改造;

  3. 这方式在大型项目中的优势会更加明显;

278e8c4b84fba85d40f27a6f1fa81687.png

速记手册

整理了一些关键知识点,可以保存图片定期回顾。

1308b7daddb7d4752ddc2ff4f58050df.png

14672375583ab899b08e753f3bac7eff.png

官方材料

文章中的内容基本上都是参考官方文档以及 Youtube 上的 mad - arch 系列。都看到这里了建议您到官方文档中的 pathawy 地址中获取下现代 Android 应用架构徽章,只要阅读完下面的文档以及完成对应测试即可。

73c78c632ce31f21d4486c954fcb7df0.png

  • Youtube
    https://www.youtube.com/watch?v=TPWmfJq16rA&list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX&ab_channel=AndroidDevelopers

  • pathawy
    https://developer.android.google.cn/courses/pathways/android-architecture

  • ui-layer
    https://developer.android.google.cn/jetpack/guide/ui-layer

  • ui-layer/events
    https://developer.android.google.cn/jetpack/guide/ui-layer/events

  • domain-layer
    https://developer.android.google.cn/jetpack/guide/domain-layer

  • data-layer
    https://developer.android.google.cn/jetpack/guide/data-layer

  • youtube playlist
    https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX

  • Modern Android App Architecture quiz
    https://developer.android.google.cn/courses/quizzes/android-architecture/architecture-layers?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-architecture%23quiz-%2Fcourses%2Fquizzes%2Fandroid-architecture%2Farchitecture-layers

33aa57c7b7aa98d5d7aa7af0ef5e137d.png

最后

b61eefe2f7ebf38f3f242c73b429ebde.jpeg

今年的 Google I/O 发布了一个最新的官方示例 Now in Android,这个示例的完整度比之前的 JetNews、Sunflower 要高,后面也将基于这个仓库做进一步的说明解析,从一个完整项目的角度来看 Android 新推出的架构指南

  • Now in Android
    https://github.com/android/nowinandroid


长按右侧二维码

查看更多开发者精彩分享

0bae3d74fc48d2670e2b9be8398cc3b4.png

"开发者说·DTalk" 面向b4ff612af6875b8e012db57fe1f64a67.png中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

693cbf7088f944253884815d4aa22418.gif 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 


b7c3dc1d58841d6e046fe42a7c43474f.png

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值