Google官方的安卓运用Nowinandroid运用了现在很主流的技能,其中在架构分层方面运用到了干净架构即CleanArchitecture,该架构配合MVVM模式能够大大提升可读性、拓展性以及可移植性,本文首要学习Google是怎么抽出数据层的。
弥补:干净架构的分层如下
前言
前情提要:[CleanArchitecture] Google官方的Nowinandroid是怎么抽出笼统层(Domain Layer)的 – ()
在前一篇文章中我剖析了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层用