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

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

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

pS1W9df.png

前言

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

截屏2023-01-19 16.27.52.png

两个数据源都有返回自己定义的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层用
分类:
Android
标签: