Google官方的安卓应用Nowinandroid使用了目前很主流的技术,其中在架构分层方面使用到了干净架构即CleanArchitecture,该架构配合MVVM模式可以大大提升可读性、拓展性以及可移植性,本文主要学习Google是如何抽出数据层的。
补充:干净架构的分层如下
前言
前情提要:[CleanArchitecture] Google官方的Nowinandroid是如何抽出抽象层(Domain Layer)的 - 掘金 (juejin.cn)
在前一篇文章中我分析了Google是如何抽取出抽象层的,抽象层只负责编写逻辑用例(use cases),用例会从数据层提供的Repository接口拉取数据,组合多个数据源的数据之后返回给UI层使用,回顾一下GetUserNewsResourcesUseCase的构造方法:
class GetUserNewsResourcesUseCase @Inject constructor(
private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository
) {
...
}
//接口
interface NewsRepository : Syncable {
/**
* Returns available news resources as a stream.
*/
fun getNewsResources(): Flow<List<NewsResource>>
/**
* Returns available news resources as a stream filtered by topics.
*/
fun getNewsResources(
filterTopicIds: Set<String> = emptySet(),
): Flow<List<NewsResource>>
}
复制代码
可以发现构建的时候传入了newsRepository和userDataRepository两个接口,接口中定义了功能,使用Hilt依赖注入的方式获取两个Repository的具体实现。这样做的好处是,domain层并不知道Repository的具体实现,也不需要知道,知道了Repository具有什么能力之后即可编写出用例,UI层可以直接拿用例来开发页面,开发数据层的人也可以专注于开发Repository的具体实现;其次是可以方便地mock Repository进行单元测试。
数据层的依赖结构
毫无疑问,数据层也是一个单独的module,来看下它的依赖结构。
dependencies {
implementation(project(":core:common"))
implementation(project(":core:model"))
implementation(project(":core:database"))
implementation(project(":core:datastore"))
implementation(project(":core:network"))
}
复制代码
可见模块分地很细,数据来源于网络请求和本地存储,连数据的model都单独放在一个module,下面以请求NewsResource为例理清楚各层model的关系。
两个数据源都有返回自己定义的model,在数据层统一为一个NewsResource,并定义一个type: NewsResourceType字段来标识数据来源于哪个渠道,但是对于UI层来说并不关心数据来源于哪个渠道,它只需要拿到数据并展示就可以了,因此在抽象层会对NewsResource做一个映射,将之转换成抽象层中的UserNewsResource。
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> {
return map { UserNewsResource(it, userData) }
}
复制代码
数据的缓存与更新
数据的来源可以是网络和本地,一般来说会优先加载本地数据,同时请求网络进行更新然后再更新UI,Google的整体思路是,应用启动的时候异步开始网络同步各种数据,页面展示的时候先加载本地的数据,因此写了一套复杂的同步逻辑,这里简单介绍一下。
在sync module的清单文件中注册了一个InitializationProvider
,这个玩意属于jetpack里面的startup
,简单来说就是也是利用Provider来做一些额外的初始化工作,但是性能和可控性更高一点。
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- TODO: b/2173216 Disable auto sync startup till it works well with instrumented tests -->
<meta-data
android:name="com.google.samples.apps.nowinandroid.sync.initializers.SyncInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
复制代码
重点来看SyncInitializer,在create方法处往WorkManager入队了一个Work。
class SyncInitializer : Initializer<Sync> {
override fun create(context: Context): Sync {
WorkManager.getInstance(context).apply {
// Run sync on app startup and ensure only one sync worker runs at any time
enqueueUniqueWork(
SyncWorkName,
ExistingWorkPolicy.KEEP,
SyncWorker.startUpSyncWork() //1
)
}
return Sync
}
}
复制代码
主要工作在SyncWorker,看下它的doWork方法做了什么。
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted private val appContext: Context,
@Assisted workerParams: WorkerParameters,
private val niaPreferences: NiaPreferencesDataSource,
private val topicRepository: TopicsRepository,
private val newsRepository: NewsRepository,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(appContext, workerParams), Synchronizer {
override suspend fun getForegroundInfo(): ForegroundInfo =
appContext.syncForegroundInfo()
override suspend fun doWork(): Result = withContext(ioDispatcher) {
traceAsync("Sync", 0) {
// 1 开始执行各repository的同步逻辑
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
async { newsRepository.sync() },
).all { it }
if (syncedSuccessfully) Result.success()
// 2 失败则返回重试信号
else Result.retry()
}
}
}
复制代码
协程调度同步逻辑执行于IO线程,newsRepository.sync()调用之后会调用到Syncable的syncWith方法,由于NewsRepository实现了Syncable接口,所以逻辑最终走到了NewsRepository的实现类的syncWith方法。
class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao,
private val topicDao: TopicDao,
private val network: NiaNetworkDataSource,
) : NewsRepository {
....
override suspend fun syncWith(synchronizer: Synchronizer) =
synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
// Order of invocation matters to satisfy id and foreign key constraints!
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.flatten()
)
}
)
}
复制代码
看似很复杂,实则逻辑很清晰,Synchronizer主要是帮助实现以下逻辑:
- 检查当前缓存的版本号(versionReader)
- 从网络拉取比当前版本号新的数据(changeListFetcher)
- 更新覆盖到本地本地数据库(modelUpdater)、更新版本号(versionUpdater)
整个数据的缓存与更新的逻辑就理完了,这套逻辑还是很值得去借鉴的。思维发散一下,目前数据层的能力足够了吗?在我看来其实还可以做一点改进,数据的来源可以有多个渠道,目前给UI层的数据默认都是通过数据库这个渠道,网络同步后才更新,那如果我在某些情况下只允许从网络获取数据怎么办,这个时候就显得不够灵活,可以增加一个控制策略,UI层通过参数传递来控制使用的数据源。第二个是数据的缓存只做了本地缓存,某些页面的数据可能变化的频率不大,这时可以引入LRU cache,请求时也有参数控制缓存策略。
数据源module的设计
- network数据源的设计
能力主要由Retrofit提供,由NiaNetworkDataSource接口定义module对外提供的接口,由RetrofitNiaNetwork实现,最后实现交由retrofit完成。interface NiaNetworkDataSource { suspend fun getTopics(ids: List<String>? = null): List<NetworkTopic> } private interface RetrofitNiaNetworkApi { @GET(value = "topics") suspend fun getTopics( @Query("id") ids: List<String>?, ): NetworkResponse<List<NetworkTopic>> } @Singleton class RetrofitNiaNetwork @Inject constructor( networkJson: Json ) : NiaNetworkDataSource { private val networkApi = Retrofit.Builder() .baseUrl(NiaBaseUrl) .client( OkHttpClient.Builder() .addInterceptor( // TODO: Decide logging logic HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) } ) .build() ) .addConverterFactory( @OptIn(ExperimentalSerializationApi::class) networkJson.asConverterFactory("application/json".toMediaType()) ) .build() .create(RetrofitNiaNetworkApi::class.java) override suspend fun getTopics(ids: List<String>?): List<NetworkTopic> = networkApi.getTopics(ids = ids).data } 复制代码
- database数据源设计
主要使用到jetpack room,这里的设计稍有不一样,不再定义datasource,而是由对应的Dao承担datasource的角色,使用依赖注入提供对应的Dao,单元测试则是创建一个inMemoryDatabase,只存在于内存中的数据库,测试Dao。@Dao interface TopicDao { @Query( value = """ SELECT * FROM topics WHERE id = :topicId """ ) fun getTopicEntity(topicId: String): Flow<TopicEntity> } @Module @InstallIn(SingletonComponent::class) object DaosModule { @Provides fun providesTopicsDao( database: NiaDatabase, ): TopicDao = database.topicDao() } 复制代码
- datastore数据源设计
主要使用Proto DataStore,因为可以存储自定义类。在src/main/proto目录创建user_preferences.proto结构,会在build/generated/source/proto/处生成对应的实体类。接着创建一个UserPreferencesSerializer
实现datastore的Serializer接口。由依赖注入提供DataStore实例。
对外提供的api统一由NiaPreferencesDataSource提供,操作的是@Module @InstallIn(SingletonComponent::class) object DataStoreModule { @Provides @Singleton fun providesUserPreferencesDataStore( @ApplicationContext context: Context, @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, userPreferencesSerializer: UserPreferencesSerializer ): DataStore<UserPreferences> = DataStoreFactory.create( serializer = userPreferencesSerializer, scope = CoroutineScope(ioDispatcher + SupervisorJob()), migrations = listOf( IntToStringIdsMigration, ) ) { context.dataStoreFile("user_preferences.pb") } } 复制代码
DataStore<UserPreferences>
对象。
单元测试时则是在临时文件夹创建一个临时的class NiaPreferencesDataSource @Inject constructor( private val userPreferences: DataStore<UserPreferences> ) { suspend fun setFollowedTopicIds(topicIds: Set<String>) { try { userPreferences.updateData { it.copy { followedTopicIds.clear() followedTopicIds.putAll(topicIds.associateWith { true }) updateShouldHideOnboardingIfNecessary() } } } catch (ioException: IOException) { Log.e("NiaPreferences", "Failed to update user preferences", ioException) } } } 复制代码
DataStore<UserPreferences>
。fun TemporaryFolder.testUserPreferencesDataStore( userPreferencesSerializer: UserPreferencesSerializer = UserPreferencesSerializer() ) = DataStoreFactory.create( serializer = userPreferencesSerializer, ) { newFile("user_preferences_test.pb") } 复制代码
总结
- 数据层主要是由各个Repository组成,Repository中从数据源拉取数据并做缓存与更新操作
- 数据源分为database、datastore、network三个渠道,分处于三个module
- 数据源有自己的model,在数据层统一成一个model给domain层用