【NowInAndroid架构拆解】(9)重新审视NowInAndroid架构设计

534 阅读4分钟

【NowInAndroid架构拆解】系列文章


在完成了前面8篇架构拆解文章后,是时候进行阶段性的总结了,在本文中,我将重新审视NowInAndroid项目,尝试从宏观角度分析,该项目是如何在可伸缩性、模块化、可测试等角度提供最佳实践的思路的。

概览:ForYou 页面的实现

image.png

这一次我们同样以 ForYou 页面为例进行分析,一方面是因为这个页面足够简单,另一方面,它也很好地体现了,在 Data 层和 UI 层之间,数据流自底向上、以及控制流自顶向下的传递过程。

image.png

  • UI:响应式 UI,监听 Data 层发送来的数据流
  • Data:单一可信数据源

image.png

  • 数据流:从 Data 流向 UI,使用 Kotlin Flow 实现
  • 控制流:从 UI 流向 Data,例如“添加/移除书签”操作

Data 层

Data 的分层设计

image.png

对上层仅暴露 Repository,内部则由不同的 Data source 作为数据源。

LocalDataSource 实现

在 NowInAndroid APP 中,数据以新闻流的方式呈现,因此它设计了一个本地数据源 LocalDataSource,用来提供本地缓存的数据。

一方面可以获取新闻列表,另一方面也会将用户进行的“收藏”操作同步到数据源。

image.png

LocalDataSource 代码实现如下所示:

// ===> 封装了数据源DataStore的具体实现,向上层提供数据流
class LocalDataSource(private val dataStore: DataStore<userPreferences>) {
    val bookmarksStream : Flow<List<String>> = dataStore.data.map {
        it.bookmarksMap.keys.toList()
    }
    // ===> 更新数据,耗时操作
    suspend fun toggleBookmark(newsResourceId: String, isBookmarked: Boolean) {
        ...
        if (isBookmarked) {
            bookmarks.put(newsResourceId, true)
        } else {
            bookmarks.remove(newsResourceId)
        }
        ...
    }
}

Repository 实现

image.png

在更上层,则是提供对外接口的 Repository,它对 LocalDataSource 和 RemoteDataSource(图上未标出)进行统一管理,处理两者之间数据融合的逻辑。

其代码如下所示:

// ===> 
class BookmarksRepository(private val localDataSource: LocalDataSource) {
    val bookmarksStream: Flow<List<String>> = localDataSource.bookmarksStream
    
    suspend fun toggleBookmark(newsResourceId: String, isBookmarked: Boolean) =
        localDataSource.toggleBookmark(newsResourceId, isBookmarked)
    }
}

封装不同的数据源

image.png

UI 层实现

image.png

UI 层的主要职责有两个:

  • 展示 UI 界面
  • 处理交互事件

ForYouViewModel 监听并组装 Repository 的数据流

image.png

位于 UI 层的 ViewModel,持有多个 Repository 的对象,将其发送的数据流拼接成 UiState 不可变对象,从而更新界面显示。

image.png

以新闻流为例,界面中不仅显示新闻的标题、头图、概要,同时也会在右上角展示该用户是否收藏了这条新闻。这个过程就是组装两个 Repository 提供的数据流的过程。

代码如下所示:

image.png

Loading、Success 页面状态处理

使用密封类来控制页面加载状态枚举,同时对于 Success 状态,可以携带列表数据,从而进行显示。

image.png

对于标记“收藏”的操作,同样由 ViewModel 提供函数,供 UI 层的 Screen 调用。

image.png

实现 Screen

image.png

在 UI 层,Screen 是最终展示在界面上的对象,它接收 ViewModel 发射的不可变 UiState 对象,并将其映射为不同状态的界面元素,随后展示给用户。

Screen 的主流实现有 View 和 Jetpack Compose 两种,以下为 Compose 实现。

待实现的效果图

image.png

对应的 Compose 代码

image.png

image.png

接下来是页面 ForYouScreen

image.png

Activity.onCreate() 中创建页面布局。

image.png

模块化 Modularization

image.png

通过良好的模块化设计,可以增加APP的伸缩性,更加有利于并行开发。而良好的模块化设计,应当做到以下两点:

  • 关注点分离:模块自身的功能聚焦且独立
  • 依赖关系明确:避免出现同级依赖、循环依赖

通常来说,应用的模块化可以分为 appfeaturecore 三层,它们的关系如下所示:

image.png

NowInAndroid 项目的模块拆分如下所示:

image.png

最上层的 build-logic 可以用来管理公共依赖的版本,在不同子 module 里会使用到相同依赖包,将其版本号提取到顶层,以便统一处理。

Before:

image.png

After:

image.png

NowInAndroid 模块拆分

image.png

可测试性设计

在进行了分层设计的基础上,通过替换 Repository 为测试数据源,可以 mock 数据进行测试。

image.png

单元测试代码如下所示。

image.png

参考资料