转眼就 2017 年了,原本定的博客计划因为太忙而搁置了两个月,最近终于可以恢复更新了。今天要阐述的是我理解的完美 Repository 层的实现,Repository 层也就是我们的数据层,在这里封装了获取网络数据、缓存数据、数据库数据以及数据转换逻辑
DO data class 即 Domain Object ,业务实体类。通常会用一个 Java Bean 来描述它,这里我们用 Kotlin 的 data class 实现:
1 2 3 4 5 6 7 8 9 data class Article (@SerializedName("_id" ) val id: String, val desc: String, val source: String, val type: String, val url: String, val used: Boolean , val who: String?, val createdAt: Date, val publishedAt: Date)
使用了data class
关键词之后,Kotlin 会自动帮我们生成getter
(如果属性是使用var
声明的则还会生成setter
)、equals()
、hashCode()
、toString()
、copy()
等常用方法。
而在 Java 中写一个 JavaBean,我们需要一个个声明属性并写出它们对应的getter
、setter
以及其它代码,相比之下 Kotlin 的代码量小了一大半。
Parcelable 很多情况下我们需要 DO 实现Parcelable
以便我们在 Android 中传输数据,这个时候如果手写大量的代码是非常麻烦的一件事情,我们可以使用Parceler 节约开发时间和代码量。
但出于兼容性原因,所有 DO 的属性都要是 Optional
1 2 3 4 5 6 7 8 9 10 11 @Parcel(Parcel.Serialization.BEAN) data class Content (@SerializedName("_id" ) val id: String? = null , val createdAt: Date? = null , val desc: String? = null , val publishedAt: Date? = null , val source: String? = null , val type: String? = null , val url: String? = null , val used: Boolean = false , val who: String? = null )
然后我们就可以这样传输一个 DO 了:
1 2 3 4 val parcelable: Parcelable = Parcels.wrap(Content())val content: Content = Parcel.unwrap(parcenlable)
RealmObject 讲真,我不建议使用 Realm ,不仅侵入性较强,而且使用 RxJava 的时候有线程切换的问题产生。
在严格分层的应用中查询数据要跨线程传输,而此时 Realm 因为查询出来的结果不能跨线程使用必须 copy 对象,所以效率并不比其它数据库高。
DataStore and Cache 在数据库的选择中因为 Realm 的线程切换以及侵入性问题,第一时间就排除掉了。
然后是 SQL 和 NoSQL 的选择,Android 中的 SQL 只有 Sqlite,而基于开发的便利性使用的框架都会对项目造成一定的侵入性。
最后经过思考选择了 paperdb 存储数据。使用 NoSQL 还有一个好处就是缓存数据非常方便,而且还可以把以前存在 SharedPreferences 里的一些数据挪到数据库中。
项目中引入 paperdb :
1 compile 'io.paperdb:paperdb:2.0'
存储和取出数据:
1 2 3 4 5 6 7 Paper.book().write("city" , "Lund" ) Paper.book().write("task-queue" , queue) Paper.book().write("countries" , countryCodeMap) val city: String = Paper.book().read("city" )val queue: LinkedList = Paper.book().read("task-queue" )val countryCodeMap: HashMap = Paper.book().read("countries" )
Data Cache 可以使用 paperdb
存储,Memory Cache 则使用 LruCache 。
这样在 Repository 中我们就可以做到三层缓存了。
Memory Cache(LruCache) -> DataStore Cache(paperdb) -> Cloud(Web)
网络请求 网络请求使用 okhttp + Retrofit + gson + RxJava 实现
如果不使用 Dagger 的话是这样的:
DataManager.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 object DataManager { const val RESPONSE_CACHE_FILE = "response_cache" const val RESPONSE_CACHE_SIZE = 10 * 1024 * 1024L const val HTTP_CONNECT_TIMEOUT = 10L const val HTTP_READ_TIMEOUT = 30L const val HTTP_WRITE_TIMEOUT = 10L const val MAX_MEMORY_CACHE_SIZE = 16 const val DATE_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'" val mGson = GsonBuilder() .setDateFormat(DATE_FORMAT) .create() val mCache = LruCache<Any, Any>(MAX_MEMORY_CACHE_SIZE) lateinit var gankModel: GankModel private set fun init (context: Context , mApiHostUrl: String , isDebug: Boolean ) { val cacheDir = File(context.cacheDir, RESPONSE_CACHE_FILE) val cache = Cache(cacheDir, RESPONSE_CACHE_SIZE) val client = OkHttpClient.Builder() .cache(cache) .connectTimeout(HTTP_CONNECT_TIMEOUT, TimeUnit.SECONDS) .build() val retrofit = Retrofit.Builder() .baseUrl(mApiHostUrl) .addConverterFactory(GsonConverterFactory.create(mGson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(client) .build() createModels(retrofit) } private fun createModels (retrofit: Retrofit ) { gankModel = GankRepository(retrofit.create<GankApi>(GankApi::class .java)) } }
GankApi.kt
1 2 3 4 5 6 interface GankApi { @GET("data/Android/{size}/{index}" ) fun getAndroidContent (@Path("index" ) index: Int , @Path("size" ) size: Int ) : Flowable<ApiResult<List<Content>>> }
GankRepository.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class GankRepository (private val mGankApi: GankApi) : GankModel { val key = "androidCache" @Suppress("UNCHECKED_CAST" ) override fun getAndroidContent () : Flowable<List<Content>> { return mGankApi.getAndroidContent(1 , 10 ) .handleResult() .map { DataManager.mCache.put(key, it) Paper.book().write(key, it) it } .startWith(Flowable.just<List<Content>>( DataManager.mCache.get (key) as List<Content>? ?: Paper.book().read<List<Content>?>(key) ?: ArrayList<Content>())) .subscribeOnIO() } }
通用信息比如 App 版本号,客户端类型,用户ID 等都可以放在请求头中,这个时候应该使用 okhttp 的拦截器实现,这里就不展开了。
错误处理 我们的接口中会包含错误类型错误码,于是对应的也会定义对应的异常信息抛给上层处理,这里使用了 kotlin 的扩展方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 fun <T> Flowable<ApiResult<T> >.handleResult () : Flowable<T> { return flatMap { if (it.error) { Flowable.error<T>(Exception("error" )) } else { Flowable.just(it.results) } } }
SharedPreferences 虽然说我们使用了 paperdb,可以替代大多数情况下 SharedPreferences 的使用,但在使用设置界面时还是推荐使用 SharedPreferences,因为系统的 PreferenceFragment 可以为我们节省大量的代码,节约开发时间。
那么这个时候还是没办法避免使用 SharedPreferences 了,这里提供一个使用 Kotlin 委托属性 实现的 SharedPreferences 委托类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Preference <T >(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> { val prefs by lazy { context.getSharedPreferences("default" , Context.MODE_PRIVATE) } override fun getValue (thisRef: Any ?, property: KProperty <*>) : T { return findPreference(name, default) } override fun setValue (thisRef: Any ?, property: KProperty <*>, value: T ) { putPreference(name, value) } private fun <U> findPreference (name: String , default: U ) : U = with(prefs) { val res: Any = when (default) { is Long -> getLong(name, default) is String -> getString(name, default) is Int -> getInt(name, default) is Boolean -> getBoolean(name, default) is Float -> getFloat(name, default) else -> throw IllegalArgumentException("This type can be saved into Preferences" ) } res as U } private fun <U> putPreference (name: String , value: U ) = with(prefs.edit()) { when (value) { is Long -> putLong(name, value) is String -> putString(name, value) is Int -> putInt(name, value) is Boolean -> putBoolean(name, value) is Float -> putFloat(name, value) else -> throw IllegalArgumentException("This type can be saved into Preferences" ) }.apply() } }
在代码中我们可以如下使用
1 2 3 4 5 6 7 class WhateverActivity : Activity () { var aInt: Int by Preference(this , "aInt" , 0 ) fun whatever () { println(aInt) aInt = 9 } }
总结 这就是我心目中完美的 Repository 层(或 Data 层)实现了,也许并不是最优的,但在我看来做到了易用和效率的平衡。
以上。