2022Android公式最新アプリアーキテクチャガイド-まとめ

アーチ-まとめ

2022Android公式アーキテクチャガイドラインシリーズ

レビュー

Arch-An Overviewでは、高品質のアプリを開発する際の課題と、この課題に対処するために関心の分離、データ駆動型、その他の設計原則などの設計原則がどのように必要かについて簡単に説明します。

アプリのさまざまな動作に応じて、アプリを分離/レイヤー化してその責任を決定し、各レイヤー間の通信の相互作用はリアクティブな方法を採用します。

アプリは、、、の3層構造でUI LayerありDomain LayerData Layerその依存関係は一方向です。上層は下層に依存できますが、下層は上層に依存することはできません。大まかに次のようになります。ここDomain Layerで、はオプションのレイヤーです。

Untitled.png

各レイヤーの主な責任は次のとおりです。

  • UI Layer:UI要素を使用してアプリにデータを表示する
    • 基になるデータを簡単にUI elements使用できるUiStateデータに処理します。
    • 対応するUiState描画UI elements;
    • ユーザー操作イベントに応答し、必要に応じて配布します。
  • Domain Layer:一般的なビジネスロジックをカプセル化します
    • 複雑なビジネスロジックをカプセル化して、大きなクラスを回避します。
    • コードの重複を避けるために、よりViewModel一般的なします。
  • Data Layer:信頼できる唯一の情報源を提供するために、統合されたデータソースをカプセル化します
    • フレームワークとサードパーティのSDKをカプセル化するためにさまざまなAPIDataSourceを。
    • 同じビジネスの異なるデータ型を統合するように定義RepositoryDataSourceます。

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

Modern_Android_App_Arch.gif

关于 UI LayerDomain LayerData Layer 中更多详细内容可查看之前文章介绍,也可以查看官方文档应用架构指南

下面就聊下之前没有提到的并且和本次主题相关的一些内容。

MVI 的关系?

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

無題1.png

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

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

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

business.gif

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

如何处理线程?

无论是在那一层都要确保其在主线程安全的,即在主线程调用不会阻塞主线程或者是抛出异常。那应该是在那一层进行处理呐? 其可选项有 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

如何处理实体类(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,大型团队建议采用这种方式。

如何组织代码?

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

# 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. 这方式在大型项目中的优势会更加明显;

速记手册

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

image.png

官方材料

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

Untitled 2.png

最后

Untitled 3.png

最近では、 Google I / O 2022の開催に伴い、最新の公式サンプルNow in Androidもリリースされています。この例の完全性は、以前のJetNewsやSunflowerよりも高く、さらに説明と分析が行われます。後でこの倉庫で。完全なプロジェクトの観点からのAndroidの新しいアーキテクチャガイドライン。

おすすめ

転載: juejin.im/post/7097859304237531166