这章内容 基本上都是代码,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 })
}
}
}
}