Google官方的安卓运用Nowinandroid运用了现在很主流的技能,其中在架构分层方面运用到了干净架构即CleanArchitecture,该架构配合MVVM模式能够大大提升可读性、拓展性以及可移植性,本文首要学习Google是怎么抽出数据层的。

弥补:干净架构的分层如下

[CleanArchitecture] Google官方的Nowinandroid是如何抽出数据层(Data Layer)的

前言

前情提要:[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的联系。

[CleanArchitecture] Google官方的Nowinandroid是如何抽出数据层(Data Layer)的

两个数据源都有回来自己界说的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首要是帮助完成以下逻辑:

  1. 查看当前缓存的版本号(versionReader)
  2. 从网络拉取比当前版本号新的数据(changeListFetcher)
  3. 更新掩盖到本地本地数据库(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实例。
      @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")
          }
      }
    
    对外供给的api一致由NiaPreferencesDataSource供给,操作的是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层用