尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Now in Android `:core:data` 数据层模块全解析:离线优先仓库架构与模块依赖图详解

Now in Android `:core:data` 数据层模块全解析:离线优先仓库架构与模块依赖图详解 Now in Android:core:data数据层模块全解析离线优先仓库架构与模块依赖图详解【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroidcore/data/README.md是 Now in Android 项目中:core:data模块的架构说明书它以一张模块依赖图定义了该模块在整个分层架构中的位置数据层被夹在:core:database/:core:datastore/:core:network等基础设施之上向feature层屏蔽底层数据来源细节。本文以该依赖图为骨架结合模块内 Repository 接口与实现、changeListSync同步机制、Hilt 依赖注入以及单元测试源码完整还原这个离线优先数据层的设计思路与落地代码读完你既能看懂这张图也能直接在仓库中定位每一个关键类。模块定位依赖图揭示的分层职责原文档开篇即用一张 Mermaid 依赖图说明:core:data在:core分组中的位置。图中实线箭头表示编译期硬依赖虚线箭头表示运行时/弱依赖如仅在特定构建变体或执行时机才触达图例约定android-application绿、android-feature橙、android-library蓝、android-test蓝紫、jvm-library紫、unknown红其中library -- jvm表示 Android 库可以依赖纯 JVM 库如:core:model、:core:common。把:core:data相关的边翻译成依赖语义表依赖边类型含义:core:data -- :core:common实线编译期依赖公共工具与协程扩展:core:data -- :core:database实线依赖 Room 数据库DAO 与 Entity:core:data -- :core:datastore实线依赖 Preferences DataStore 用户偏好:core:data -- :core:network实线依赖网络数据源获取远端数据:core:data -.- :core:analytics虚线运行时调用埋点上报:core:data -.- :core:notifications虚线运行时触达本地通知从这张图可以读出:core:data的核心设计原则它把数据库 DataStore 网络 埋点 通知组装成一组面向业务语义的 Repository向上层只暴露干净的接口。:core:model不被:core:data直接依赖而是经由:core:database/:core:datastore/:core:network间接提供纯 Kotlin 领域模型避免了数据层与 UI 层在模型上的耦合。构建配置api 与 implementation 的依赖纪律依赖图在 Gradle 中的落地见 core/data/build.gradle.ktsplugins { alias(libs.plugins.nowinandroid.android.library) alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.nowinandroid.hilt) id(kotlinx-serialization) } android { namespace com.google.samples.apps.nowinandroid.core.data testOptions.unitTests.isIncludeAndroidResources true } dependencies { api(projects.core.common) api(projects.core.database) api(projects.core.datastore) api(projects.core.network) implementation(projects.core.analytics) implementation(projects.core.notifications) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.serialization.json) testImplementation(projects.core.datastoreTest) testImplementation(projects.core.testing) }几个值得注意的工程决策api传递依赖common、database、datastore、network用api(...)暴露给下游。因为:core:data的公开 Repository 接口签名里会出现这些模块的类型如NiaNetworkDataSource、DAO 查询结果下游 feature 模块必须能解析这些类型所以采用传递依赖。implementation私有依赖analytics、notifications仅被实现类内部使用如OfflineFirstUserDataRepository上报埋点、OfflineFirstNewsRepository发通知不进入公开 API 面因此用implementation隔离避免下游模块反向耦合。测试基建isIncludeAndroidResources true允许 Robolectric 环境加载资源测试依赖core.testing提供TestDispatcherRule等与core.datastoreTest提供测试用 DataStore 实现对应test/目录下的TestSynchronizer.kt、testdoubles/中的TestNewsResourceDao、TestTopicDao、TestNiaNetworkDataSource等测试替身。Repository 架构接口与实现的离线优先配对模块内源码结构core/data/src/main/kotlin分为di/、model/、repository/、util/四块。repository/目录里接口与实现成对出现接口默认实现数据来源TopicsRepositoryOfflineFirstTopicsRepositoryRoom 网络NewsRepositoryOfflineFirstNewsRepositoryRoom 网络UserDataRepositoryOfflineFirstUserDataRepositoryPreferences DataStoreRecentSearchRepositoryDefaultRecentSearchRepositoryRoomSearchContentsRepositoryDefaultSearchContentsRepository网络 DataStoreUserNewsResourceRepositoryCompositeUserNewsResourceRepository上述仓库组合命名中的OfflineFirst是核心设计信号的直接体现。以 OfflineFirstTopicsRepository.kt 为例类注释明确写着Disk storage backed implementation of the TopicsRepository. Reads are exclusively from local storage to support offline access.即读操作只走本地磁盘internal class OfflineFirstTopicsRepository Inject constructor( private val topicDao: TopicDao, private val network: NiaNetworkDataSource, ) : TopicsRepository { override fun getTopics(): FlowListTopic topicDao.getTopicEntities() .map { it.map(TopicEntity::asExternalModel) } override fun getTopic(id: String): FlowTopic topicDao.getTopicEntity(id).map { it.asExternalModel() } }查询结果从 Room 的Flow中流出再由asExternalModel()把数据库 Entity 映射为:core:model中的纯领域模型Topic。这意味着 UI 层永远不需要直接碰 DAO 或网络层——只要数据库里有数据即使断网也能完整渲染。OfflineFirstNewsRepository.kt 的读路径同理但它额外支持NewsResourceQuery条件过滤override fun getNewsResources( query: NewsResourceQuery, ): FlowListNewsResource newsResourceDao.getNewsResources( useFilterTopicIds query.filterTopicIds ! null, filterTopicIds query.filterTopicIds ?: emptySet(), useFilterNewsIds query.filterNewsIds ! null, filterNewsIds query.filterNewsIds ?: emptySet(), ) .map { it.map(PopulatedNewsResource::asExternalModel) }useFilterXxx布尔开关用于让 Room 在是否启用该过滤条件之间切换查询分支从而用同一个方法服务全部新闻流按关注话题过滤按 ID 集合过滤等多种场景。变更列表同步changeListSync 的类 git 机制写路径是离线优先架构的关键数据必须通过网络增量同步进本地库。core/data用一套名为change list sync的机制完成这件事定义在 SyncUtilities.ktSynchronizer管理ChangeListVersions各模型的版本号的读写并提供Syncable.sync()语法糖Syncable声明suspend fun syncWith(synchronizer: Synchronizer): Boolean是所有可同步仓库的标记接口changeListSync(...)通用的同步编排函数注释里用 git 做了非常形象的类比。suspend fun Synchronizer.changeListSync( versionReader: (ChangeListVersions) - Int, changeListFetcher: suspend (Int) - ListNetworkChangeList, versionUpdater: ChangeListVersions.(Int) - ChangeListVersions, modelDeleter: suspend (ListString) - Unit, modelUpdater: suspend (ListString) - Unit, ) suspendRunCatching { // Fetch the change list since last sync (akin to a git fetch) val currentVersion versionReader(getChangeListVersions()) val changeList changeListFetcher(currentVersion) if (changeList.isEmpty()) returnsuspendRunCatching true val (deleted, updated) changeList.partition(NetworkChangeList::isDelete) // Delete models that have been deleted server-side modelDeleter(deleted.map(NetworkChangeList::id)) // Using the change list, pull down and save the changes (akin to a git pull) modelUpdater(updated.map(NetworkChangeList::id)) // Update the last synced version (akin to updating local git HEAD) val latestVersion changeList.last().changeListVersion updateChangeListVersions { versionUpdater(latestVersion) } }.isSuccess完整流程对应 git 三步走fetch读本地已同步版本号向服务端请求该版本之后的变更列表NetworkChangeList含id、isDelete、changeListVersion字段apply将变更列表partition为已删除与已更新两组先删后改commit用变更列表末条的changeListVersion更新本地版本号作为下次增量同步的水位线。同步失败时suspendRunCatching会捕获非协程取消类异常并返回Result.failure同时通过Log.i记录由changeListSync收敛为false。这里特意重抛CancellationException避免破坏结构化并发。OfflineFirstTopicsRepository.syncWith是它的最简用法版本号存于ChangeListVersions::topicVersion变更通过topicDao.upsertTopics落库。而OfflineFirstNewsRepository.syncWith则复杂得多包含了三个工程化细节分批拉取SYNC_BATCH_SIZE 40注释说明这是为了平衡服务端与客户端的序列化/反序列化成本外键顺序代码注释强调 Order of invocation matters to satisfy id and foreign key constraints!——必须先topicDao.insertOrIgnoreTopics插入话题实体再upsertNewsResources插入新闻最后insertOrIgnoreTopicCrossRefEntities建立多对多关联否则会违反 Room 外键约束首次同步去噪isFirstSync currentVersion 0时将首批历史新闻全部标记为已读setNewsResourcesViewed(changedIds, true)避免新用户被海量历史通知淹没已完引导shouldHideOnboarding的用户若其关注话题下出现新增新闻则通过notifier.postNewsNotifications触发本地通知。这一整套同步调用链由 util/SyncManager.kt 调度由sync/work模块的 WorkManager 任务触发。用户数据仓库DataStore 偏好与埋点上报OfflineFirstUserDataRepository.kt 是所有用户个性化状态的汇聚点它不做持久化而是把写操作委托给NiaPreferencesDataSourcePreferences DataStore并同步调用AnalyticsHelper埋点override suspend fun setTopicIdFollowed(followedTopicId: String, followed: Boolean) { niaPreferencesDataSource.setTopicIdFollowed(followedTopicId, followed) analyticsHelper.logTopicFollowToggled(followedTopicId, followed) } override suspend fun setNewsResourceBookmarked(newsResourceId: String, bookmarked: Boolean) { niaPreferencesDataSource.setNewsResourceBookmarked(newsResourceId, bookmarked) analyticsHelper.logNewsResourceBookmarkToggled( newsResourceId newsResourceId, isBookmarked bookmarked, ) } override suspend fun setThemeBrand(themeBrand: ThemeBrand) { niaPreferencesDataSource.setThemeBrand(themeBrand) analyticsHelper.logThemeChanged(themeBrand.name) }其公开接口 UserDataRepository.kt 覆盖了 Now in Android 的全部用户可配置状态关注话题集合、新闻收藏/已读、主题品牌ThemeBrand、深色模式DarkThemeConfig、动态取色开关以及引导完成状态。读端则以val userData: FlowUserData单一数据流对外暴露UI 层通过combine订阅后即可响应所有偏好变化。值得注意的实现细节是setFollowedTopicIds标了VisibleForTesting只用于测试或批量恢复场景业务路径上使用的是单个话题的setTopicIdFollowed。搜索与组合仓库针对搜索与For You页core/data额外提供了两组仓库DefaultRecentSearchRepository.kt基于 Room 的RecentSearchQueryDaoinsertOrReplaceRecentSearch用Clock.System.now()记录查询时间getRecentSearchQueries(limit)返回按时间倒序的最近搜索UI 层限制展示条数clearRecentSearches一键清空。DefaultSearchContentsRepository负责搜索内容的组装。CompositeUserNewsResourceRepository聚合用户数据 新闻 话题三个仓库为 For You 页输出合并后的UserNewsResource流对应 CompositeUserNewsResourceRepositoryTest.kt 与UserNewsResourceTest.kt中的大量测试用例。依赖注入DataModule 把接口与实现绑在一起core/data采用 Hilt 注入所有绑定集中在 di/DataModule.ktModule InstallIn(SingletonComponent::class) abstract class DataModule { Binds internal abstract fun bindsTopicRepository( topicsRepository: OfflineFirstTopicsRepository, ): TopicsRepository Binds internal abstract fun bindsNewsResourceRepository( newsRepository: OfflineFirstNewsRepository, ): NewsRepository Binds internal abstract fun bindsUserDataRepository( userDataRepository: OfflineFirstUserDataRepository, ): UserDataRepository Binds internal abstract fun bindsRecentSearchRepository( recentSearchRepository: DefaultRecentSearchRepository, ): RecentSearchRepository Binds internal abstract fun bindsSearchContentsRepository( searchContentsRepository: DefaultSearchContentsRepository, ): SearchContentsRepository Binds internal abstract fun bindsNetworkMonitor( networkMonitor: ConnectivityManagerNetworkMonitor, ): NetworkMonitor Binds internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor }Binds的绑定全部是internal且依赖抽象接口业务代码只感知接口、不知道实现存在。此外di/UserNewsResourceRepositoryModule.kt 单独成模块把CompositeUserNewsResourceRepository绑定到UserNewsResourceRepository体现了一个领域一个绑定模块的 Hilt 组织方式。工具类网络监听与时区感知util/目录下的三个组件为仓库和同步服务提供系统能力抽象NetworkMonitor.kt 与ConnectivityManagerNetworkMonitor把ConnectivityManager的网络状态封装为FlowBoolean供同步逻辑判断是否具备联网条件TimeZoneMonitor.kt 与TimeZoneBroadcastMonitor监听ACTION_TIMEZONE_CHANGED广播供需要按本地时间展示的新闻内容刷新数据SyncManager同步调度入口被sync/work模块的 WorkManager 任务调用。这些工具类同样遵循接口 默认实现 Hilt 绑定的模式见上文DataModule中的bindsNetworkMonitor与binds使得测试可以轻松替换为假实现。测试体系离线优先行为的可验证性core/data的test/目录完整覆盖了上述仓库的离线行为测试文件验证目标OfflineFirstNewsRepositoryTest.kt新闻变更列表同步、分批 upsert、首次同步标记已读、通知触发OfflineFirstTopicsRepositoryTest.kt话题同步与版本号更新OfflineFirstUserDataRepositoryTest.kt用户偏好写路径与埋点调用CompositeUserNewsResourceRepositoryTest.kt、UserNewsResourceTest.ktFor You 组合数据流TestSynchronizer.kt测试用Synchronizer替身testdoubles/TestNewsResourceDao.kt、TestTopicDao.kt、TestNiaNetworkDataSource.ktDAO 与网络数据源的内存假实现其中TestNiaNetworkDataSource让测试无需真实网络即可模拟服务端变更列表TestSynchronizer则让测试可以精确控制ChangeListVersions的读写——这正是changeListSync把版本读写与数据拉取解耦后带来的可测性红利。总结回到那张依赖图:core:data是整个 Now in Android 数据访问的唯一门面。它在编译期依赖common/database/datastore/network在运行时协作analytics/notifications对外只暴露语义化的 Repository 接口与Flow数据流。其核心价值可归纳为三点离线优先所有读操作只走本地Room / DataStore网络仅作为写路径的增量同步来源保证弱网甚至断网场景下的可用性增量同步changeListSync以版本号为水位线的类 git 机制配合分批拉取、外键顺序与首次同步去噪兼顾正确性与性能可替换性接口 实现 Hilt 绑定 测试替身的组合使每个仓库都能在测试中独立验证也让后续替换存储介质如换用 Paging、引入 Remote Mediator不影响上层调用方。如果要在仓库中继续深挖推荐从三个入口入手先读 core/data/README.md 建立依赖全貌再对照 DataModule.kt 梳理接口实现映射最后以OfflineFirstNewsRepositoryTest为模板理解如何用测试替身驱动一次完整的 change list 同步。【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表