9.2 Room + Hilt + Retrofit 完成 DataLayer

451 阅读3分钟

这章内容 基本上都是代码,Retrofit 就不再介绍了,Room 和 Hilt 前面两张都介绍完了。 内容较长,可以直接看 Git 代码。

DataLayer 由 wan-data 和 wan-repository 两个模块组成。

  • wan-data 模块负责网络数据获取和本地数据的存储/查询
  • wan-repository 模块负责对外提供数据

wan-data 模块

新建 wan-data 模块 , 模块由 entity 、local 、remote 、di 四个包组成

entity 包

网络接口返回的 Banner 和 Article json 对应的实体类,用插件生成一下,添加 @Entity 注解, 需要注意 Article 中使用了 local包中的 TypeConverter 。

local 包

BaseTypeConverter.kt

abstract class BaseTypeConverter<T> {
    companion object{
        val gson = Gson()
    }

    abstract fun fromJson(json: String): T // 将 json 字符串转换成对象

    @TypeConverter
    fun toJson(obj:T): String = gson.toJson(obj) //将对象转换成 json 字符串
}

TagListConverter.kt

object TagListConverter: BaseTypeConverter<List<Tag>>() {
    @OptIn(ExperimentalStdlibApi::class)
    @TypeConverter
    override fun fromJson(json: String): List<Tag> = gson.fromJson(json, typeOf<List<Tag>>().javaType)
}

BaseDao.kt ,Dao 层通用的增删改方法。

abstract class BaseDao<E> {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract suspend fun insert(entity: E): Long

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract suspend fun insertAll(vararg entity: E)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract suspend fun insertAll(entities: List<E>)

    @Update
    abstract suspend fun update(entity: E)

    @Delete
    abstract suspend fun deleteEntity(entity: E): Int
}

BannerDao.kt

@Dao
abstract class BannerDao : BaseDao<Banner>() {

    @Query("SELECT * FROM Banner")
    protected abstract fun getBanner(): Flow<List<Banner>>

    fun getDistinctBanner():Flow<List<Banner>> = getBanner()

    //删除表中 ids 之外的记录 (因为 Banner 可能会部分更新)
    @Query("DELETE FROM Banner WHERE id NOT IN (:ids)")
    abstract fun removeCacheNotIn(ids:List<Int>)

    @Transaction
    suspend fun saveBanners(banners:List<Banner>){
        //先删除已经无效的 Banner
        removeCacheNotIn(banners.map { it.id })
        insertAll(banners)
    }
}

ArticleDao.kt

@Dao
abstract class ArticleDao : BaseDao<Article>() {
    //置顶文章 type = 1
    @Query("SELECT * FROM Article WHERE type = 1 AND id in(:ids)")
    abstract fun getTopics(ids: List<Int>): List<Article>
}

WanAndroidDB.kt

@Database(
    entities = [
        Article::class,
        Banner::class
    ],
    version = 1
)
abstract class WanAndroidDB : RoomDatabase(){

    abstract fun bannerDao(): BannerDao
    abstract fun articleDao(): ArticleDao

    companion object{
        private val instance:WanAndroidDB?= null

        @Synchronized
        fun getDB(context:Context):WanAndroidDB{
            return instance ?: buildDB(context)
        }

        private fun buildDB(context:Context):WanAndroidDB{
            val builder = Room.databaseBuilder(
                context,
                WanAndroidDB::class.java,
                "wanAndroidDB"
            ).fallbackToDestructiveMigration()
            return builder.build()
        }
    }
}

PreferenceStore.kt Context 拓展,DataStore 使用时需要引入依赖。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "WanAndroid")

TopicPDS.kt。 不知道这个置顶文章到底是个啥逻辑 所以每次请求之后把置顶文章的 id 缓存到 DS 里。

@Singleton
class TopicPDS @Inject constructor(
    private val dataStore: DataStore<Preferences>
){
    private val Key_TopicIds  = stringPreferencesKey("TopicIds")

    suspend fun saveTopicIds(ids:List<Int>){
        val idsStr = ids.joinToString(separator = ",") {it.toString()}
        dataStore.edit {
            it[Key_TopicIds] = idsStr
        }
    }

    fun getTopicIds():Flow<List<Int>>{
        return  dataStore.data.map {
            it[Key_TopicIds] ?: ""
        }.map { ids ->
            if (ids.isNotEmpty()){
                ids.split(",").map { it.toInt() }
            }else{
                emptyList()
            }
        }
    }

}

remote 包

retrofit , api 接口声明略。

NetResponse.kt ,网络接口返回对象数据的基类,响应码定义,返回数据统一处理。

data class ResponseException(val code:Int,val msg:String):Exception()

object ResponseCode {
    val OK = 0
}

data class NetResponse<T>(val data:T,val errorCode:Int,val errorMsg:String)

suspend fun <T,R> NetResponse<T>.handleResponse(handler:suspend (T)->R):R{
    if (errorCode == ResponseCode.OK){
        return handler(this.data)
    }else{
        //todo 统一处理 errorCode
        throw ResponseException(errorCode,errorMsg)
    }
}

di 包

NetServiceModule.kt


@Qualifier
annotation class DebugURL
@Qualifier
annotation class PreProductURL
@Qualifier
annotation class ProductURL

@Module
@InstallIn(SingletonComponent::class)
object NetServiceModule {

    @Provides
    @DebugURL
    fun providerDebugBaseUrl():String = WanAndroidService.baseUrlDebug

    @Provides
    @PreProductURL
    fun providerPreProductBaseUrl():String = WanAndroidService.baseUrlPreProduct

    @Provides
    @ProductURL
    fun providerProductBaseUrl():String = WanAndroidService.baseUrlProduct


    @Provides
    @Singleton
    fun providerWanAndroidService(@DebugURL baseUrl:String):WanAndroidService = RetrofitClient.apply {
        init(baseUrl)
    }.createService(WanAndroidService::class.java)
}

DaoModule.kt

@Module
@InstallIn(SingletonComponent::class)
object DaoModule {

    @Provides
    @Singleton
    fun bannerDao(db:WanAndroidDB) = db.bannerDao()

    @Provides
    @Singleton
    fun articleDao(db:WanAndroidDB) = db.articleDao()
}

DatabaseModule.kt

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Singleton
    @Provides
    fun provideWanAndroidDB(@ApplicationContext context: Context):WanAndroidDB = WanAndroidDB.getDB(context)
}

DataStoreModule.kt

@InstallIn(SingletonComponent::class)
@Module
class DataStoreModule {

    @Provides
    @Singleton
    fun provideDataStore(@ApplicationContext context:Context) = context.dataStore
}

wan-repository

新建 wan-repository 模块 , datasource 包里放网络数据相关操作,datastore 包里放本地数据相关操作 ,repository 类依赖 datasource 和 datastore 向外提供数据。

datastore 包

BannerDataStore.kt

@Singleton
class BannerDataStore @Inject constructor(
    private val bannerDao: BannerDao
) {

    fun getBanner() = bannerDao.getDistinctBanner()

    suspend fun saveBanner(banners:List<Banner>) = bannerDao.saveBanners(banners)

}

ArticleDataStore.kt

@Singleton
class ArticleDataStore @Inject constructor(
    private val articleDao: ArticleDao,
    private val topicPDS: TopicPDS
) {
    fun getTopicByIds(ids:List<Int>): List<Article> = articleDao.getTopics(ids)
    suspend fun saveTopics(articles:List<Article>) = articleDao.insertAll(articles)

    fun getTopicIds(): Flow<List<Int>> = topicPDS.getTopicIds()
    suspend fun saveTopicIds(ids:List<Int>) = topicPDS.saveTopicIds(ids)
}

datasource 包

BannerDataSource.kt

@Singleton
class BannerDataSource @Inject constructor(
    private val service: WanAndroidService
) {
   suspend fun banners() = service.getBanners()
}

ArticleDataSource.kt

@Singleton
class ArticleDataSource @Inject constructor(
    private val service: WanAndroidService
) {
    suspend fun topicArticles() = service.getTopArticles()
}

repositories

BannerRepository.kt

@Singleton
class BannerRepository @Inject constructor(
    private val bannerDataStore: BannerDataStore,
    private val bannerDataSource: BannerDataSource
) {
    suspend fun updateBanner() {
        bannerDataSource.banners().handleResponse {
            bannerDataStore.saveBanner(it)
        }
    }
    fun observeBanner(): Flow<List<Banner>> = bannerDataStore.getBanner()
}

ArticleRepository.kt

@Singleton
class ArticleRepository @Inject constructor(
    private val articleDataStore: ArticleDataStore,
    private val articleDataSource: ArticleDataSource
) {
    fun observeTopic(): Flow<List<Article>> = articleDataStore.getTopicIds().map {
        articleDataStore.getTopicByIds(it)
    }.flowOn(Dispatchers.IO)

    suspend fun updateTopic() {
        withContext(Dispatchers.IO){
            articleDataSource.topicArticles().handleResponse {
                articleDataStore.saveTopics(it)
                articleDataStore.saveTopicIds(it.map { article -> article.id })
            }
        }
    }
}

Git 地址

代码 git