Now in Android现代应用开发实践(二) - 应用架构设计(data+domain)

58 阅读10分钟

截至2026年,Now in Android(以下简称 NiA)依然是由 Google 官方维护、最能代表“现代 Android 应用架构”的标杆项目,GitHub 星标已突破 20K。

它不仅系统性地展示了 Jetpack 全家桶的最佳实践,更关键的是,提供了一整套面向中大型项目的可落地工程方案——涵盖模块化设计分层架构自动化测试性能分析代码提交、风格检查以及团队协作等核心环节,是 Android 应用开发者的必学项目

本系列文章专为 Android 应用开发者打造,将以抓大放小的模式深入解析 Now in Android 的设计精髓,全系列共八章。

  • 《Now in Android 现代应用开发实践(一):模块化设计》
  • 《Now in Android 现代应用开发实践(二):架构设计(data+domain)》
  • 《Now in Android 现代应用开发实践(三):架构设计(UI)》
  • 《Now in Android 现代应用开发实践(四):构建逻辑》
  • 《Now in Android 现代应用开发实践(五):代码规范》
  • 《Now in Android 现代应用开发实践(六):质量保障体系》
  • 《Now in Android 现代应用开发实践(七):Benchmark 性能测试》
  • 《Now in Android 现代应用开发实践(八):Baseline 性能优化》

本文将聚焦于NiA的核心组成部分——数据层(Data Layer)和网域层(Domain Layer) 。这些层是应用数据管理和业务逻辑的基石,确保数据流动的响应式、一致性和离线优先特性。

NiA的架构严格遵循Android官方架构指南,采用分层结构:UI层、网域层和数据层。

  • 数据层作为“可信来源”,负责数据存储、同步和暴露;
  • 网域层则封装业务逻辑,避免UI层逻辑膨胀。

本文将聚焦于该架构中最基础、最关键的两层:数据层(Data Layer) 和 网域层(Domain Layer),深入解析它们如何协同工作,为整个应用提供坚实、可靠且高效的数据支撑。


一、数据层:应用的“可信来源”

暂时无法在飞书文档外展示此内容

数据层是NiA架构的最底层,负责管理应用的所有数据和业务逻辑。它采用离线优先(Offline-First) 的设计理念,确保应用在无网络环境下也能正常运行,并将本地存储作为数据的单一可信来源(Single Source of Truth, SSOT) 。这种设计不仅提升了用户体验(如即时响应),还优化了电池和数据消耗。

什么是单一可信源

单一可信源是指在软件架构中,一个特定数据类型只有一个权威来源(如本地数据库),所有其他组件都由此来源获取数据,确保一致性、避免冲突,并简化数据管理。

在NiA的架构设计中,复杂的数据(例如自来网络的数据)都会被汇总到本地存储中,并作为唯一出口对外暴露,ViewModel 无法直接访问来自网络的数据。

1.数据层

根据Android官方数据层指南,数据层的主要职责包括:

  • 向应用其他部份公开数据。
  • 集中处理数据变化。
  • 解决多数据源之间的冲突。
  • 抽象数据源细节,避免上层依赖具体实现。
  • 包含业务逻辑,确保数据一致性。

Data层的核心是DataSource和Repository。

2.仓库(Repository)

仓库是数据层的公共API(入口),每个仓库处理一种特定数据类型,其他层(如网域层或UI层)只能通过仓库访问数据。

仓库的设计原则:

  • 主线程安全:仓库方法可在主线程调用,内部使用协程(如withContext(Dispatchers.IO))处理耗时操作。
  • 不可变数据:公开的数据模型为不可变对象,防止外部篡改导致不一致。
  • 响应式公开:使用Kotlin Flow公开数据流,支持数据变化的实时通知,而非一次性快照。
  • 命名规范: 遵循 “数据类型 + Repository” 规则。例如TopicsRepository处理话题数据,NewsRepository处理新闻资源。

3.数据源(DataSource)

每个仓库可能依赖多个数据源,这些数据源仅负责单一来源的数据处理。NiA中常见的仓库(如OfflineFirstTopicsRepository)依赖以下数据源:

数据源名称底层实现用途
TopicsDaoRoom/SQLite存储话题相关的持久化关系型数据
NiANetworkDataSourceRetrofit访问远程API通过REST API获取JSON格式的话题数据

本地数据源:如Room数据库和DataStore,是可信来源。Room提供关系型存储,支持SQL查询和Flow变化通知;DataStore则用于非结构化数据存储,分为Preferences DataStore(简单键值对)和Proto DataStore(类型化对象)。NiA使用Proto DataStore存储用户数据(如UserData),确保类型安全和事务性写入。

示例: 读取用户数据流(从NiAPreferencesDataSource):

val userData: Flow<UserData> = dataStore.data
    .map { preferences ->
        // 映射偏好数据到UserData对象
        UserData(
            followedTopics = preferences.followedTopics,
            // 其他字段...
        )
    }

远程数据源:如基于Retrofit的网络API,仅用于同步数据。NiA使用RetrofitNiANetwork执行API请求,返回JSON数据后转换为内部模型。

命名规范: 遵循 “数据类型 + 来源类型 + DataSource” 规则,通用场景用Remote/Local(如NewsRemoteDataSource),需明确来源细节时用具体类型(如NewsNetworkDataSource表示网络来源、NewsDiskDataSource表示磁盘来源)

4.数据读取与写入

读取: 以Flow形式暴露,支持响应式编程。NiA中,仓库从本地数据源读取数据,确保即时响应。即使数据为空,也不会抛出错误,而是返回空流或默认状态。

示例: 读取话题列表(从TopicsRepository):

fun getTopics(): Flow<List<Topic>> = topicsDao.getTopicsStream()
    .map { entities -> entities.map { it.asExternalModel() } }  // 转换为公共模型

写入:通过挂起函数实现,确保在协程作用域内调用。写入后立即更新本地存储,并通过Flow通知变化。

示例: 关注话题(从UserDataRepository):

suspend fun toggleFollowedTopicId(topicId: String, followed: Boolean) {
    // 更新DataStore中的用户偏好
    dataStore.updateData { preferences ->
        preferences.copy { followedTopics = if (followed) add(topicId) else remove(topicId) }
    }
}

5.数据同步:离线优先的实现

NiA的数据同步采用“离线优先”策略:本地存储是可信来源,远程数据仅用于更新本地。同步过程通过WorkManager调度,确保持久化和重试。

同步机制:应用启动时,使用WorkManager加入同步任务队列(如Sync.initialize)。OfflineFirstNewsRepository调用远程API,获取数据后插入/更新Room数据库。数据库变化通过Flow推送给上层。

示例: 新闻资源同步(从OfflineFirstNewsRepository.syncWith):

override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
    synchronizer.changeListSync(
        versionReader = ChangeListVersions::topicVersion,
        changeListFetcher = { currentVersion ->
network.getTopicChangeList(after = currentVersion)
        } ,
        versionUpdater = { latestVersion ->
copy(topicVersion = latestVersion)
        } ,
        modelDeleter = topicDao::deleteTopics,
        modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
            topicDao.upsertTopics(
                entities = networkTopics.map(NetworkTopic::asEntity),
            )
        } ,
    )

WorkManager的作用:用于持久化后台任务,支持约束(如网络连接、充电状态)和指数退避重试。NiA的SyncWorker实现同步逻辑,确保设备重启后任务恢复。

示例: 调度同步任务:

override fun requestSync() {
    val workManager = WorkManager.getInstance(context)
    // 在应用程序启动时运行同步,并确保任何时候只有一个同步工作程序运行
    workManager.enqueueUniqueWork(
        SYNC_WORK_NAME,
        ExistingWorkPolicy.KEEP,
        SyncWorker.startUpSyncWork(),
    )
}

二、网域层(Domian):封装业务逻辑

暂时无法在飞书文档外展示此内容

网域层位于数据层之上,是可选层,用于封装复杂或可复用的业务逻辑。它避免了ViewModel的逻辑膨胀,并消除代码重复。在NiA中,网域层主要包含用例(Use Case) ,每个用例是一个单一职责的类,仅有一个可调用方法(invoke)。

⚠️注意:网域层是可选的。但是现代项目中ViewModel的代码量极易暴涨,所以推荐额外封装一个网域层。

即使

1.网域层的核心职责

根据Android官方网域层指南,网域层的主要功能:

  • 封装复杂业务逻辑(如多仓库数据合并)。
  • 提供可复用逻辑,避免重复代码。
  • 提升可测试性和可读性。
  • 无独立生命周期,受调用组件(如ViewModel)限制。
  • 主线程安全,耗时操作移至后台线程。

命名规范:动词 + 名词 + UseCase,如GetUserNewsResourcesUseCase。用例不持可变状态,所有状态管理在上层。

2.用例的实现与依赖

用例依赖数据层的仓库,通过Flow或挂起函数通信。用例之间可相互依赖,形成多层结构。

示例:合并新闻和用户数据(从GetUserNewsResourcesUseCase):

class GetUserNewsResourcesUseCase(
    private val newsRepository: NewsRepository,
    private val userDataRepository: UserDataRepository
) {
    operator fun invoke(): Flow<List<UserNewsResource>> = combine(
        newsRepository.getNewsResources(),
        userDataRepository.userData
    ) { news, userData ->
        news.map { resource ->
            UserNewsResource(
                resource,
                isBookmarked = userData.bookmarkedResources.contains(resource.id)
            )
        }
    }
}
  • 调用方式:像函数一样调用useCase(),便于ViewModel集成。
  • 线程处理:使用withContext(Dispatchers.Default)处理耗时逻辑。

在NiA中,网域层暂未包含事件处理用例,事件直接由UI层调用仓库。但对于复杂场景,如日期格式化或多源合并,用例显著简化了代码。

3.示例:NiA中的用例应用

在“For You”页面,ForYouViewModel使用GetUserNewsResourcesUseCase获取包含书签的新闻流:

val feedState: StateFlow<NewsFeedUiState> = getUserNewsResourcesUseCase()
    .map { NewsFeedUiState.Success(it) }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), NewsFeedUiState.Loading)

这将冷Flow转换为热StateFlow,确保UI实时响应数据变化。

4.访问权限

设计网域层时,需决定是否允许UI层越过网域层直接访问数据层,两种方案各有优劣:

  • 方案 1:允许UI层越过网域层直接访问数据层

优势:灵活高效,对于简单的数据获取(如仅获取用户姓名),无需创建无用例,减少代码冗余。

适用场景:应用中网域层用例较少,大部分数据访问为简单 CRUD 操作。

  • 方案 2:强制UI层必须通过网域层访问数据层

优势:所有数据访问都经过网域层,可统一添加业务逻辑(如数据访问日志、权限校验),防止界面层绕过必要逻辑。

适用场景:应用需严格管控数据访问(如金融类应用需记录所有数据查询操作),或网域层用例覆盖大部分数据访问场景。

决策建议:无绝对最优方案,需根据代码库实际情况选择:

  • 若界面层多数数据访问需经过用例,可逐步过渡到 “强制通过网域层”;
  • 若简单访问较多,保持 “允许直接访问” 更高效。

三、Data + Domain 层的协同工作流程

以 “为你推荐(For You)” 页面的新闻加载流程为例,梳理数据层与网域层的协同逻辑,还原 “离线优先 + 响应式数据流” 的完整落地。

最新版本的NiA已经与下面的流程图不再一致,逻辑虽然更简单,但是在处理复杂业务时,下面的流程更值得参考。

初始化阶段:应用启动时通过 WorkManager 触发后台同步,保证本地数据是最新的;

数据流等待GetUserNewsResourcesUseCase会等待 “用户偏好 Flow” 和 “新闻 Flow” 均输出数据,期间 UI 展示Loading

同步触发更新:远程数据同步写入 Room 后,Room 的 Flow 自动推送更新,触发用例重新计算,最终更新 UI 状态;

离线场景适配:若无网络,用例直接读取 Room 中的历史数据,保证 UI 正常渲染。


四、总结

Now in Android 的数据层和网域层共同构建了一个灵活且易于维护的数据处理体系。

  • 数据层 通过 离线优先 策略、响应式数据流(Flow) 以及对 Room、DataStore、WorkManager 等 Jetpack 组件的巧妙运用,为应用提供了可靠、高效的数据服务。

  • 网域层 通过 用例(Use Cases) 将核心业务逻辑模块化、单一化,极大地提升了代码的可读性、可复用性和可测试性。

下一篇文章,我们将聚焦 UI 层(ViewModel + Jetpack Compose),解析 “状态驱动 UI” 的实现,以及如何处理用户交互与状态流转。

参考资料

本文写作时使用的辅助AI模型:Grok 4.1 + Qwen3 Max