截至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)依赖以下数据源:
| 数据源名称 | 底层实现 | 用途 |
|---|---|---|
| TopicsDao | Room/SQLite | 存储话题相关的持久化关系型数据 |
| NiANetworkDataSource | Retrofit访问远程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” 的实现,以及如何处理用户交互与状态流转。
参考资料:
- Android 官方应用架构指南:developer.android.com/topic/archi…
- Now in Android GitHub 主仓库:github.com/android/now…
本文写作时使用的辅助AI模型:Grok 4.1 + Qwen3 Max