architecture-components-samples官方示例
在协程和 Flow 使用 LiveData
本文涉及Kotlin协程,Retrofit,LiveData,ViewModel,Dagger2.Android。
在官方示例GithubBrowserSample里面没有使用协程,在实际项目里需要修改部分代码。有什么不对的地方欢迎提出。
说说这里遇到问题,加上suspend
时返回类型不能是Call
或CallAdapterFactory
返回的自定义类,必须是接口返回的数据结构。下面这段代码是官方示例里面的LiveDataCallAdapterFactory
,LiveDataCallAdapter
返回一个LiveData
,在CallAdapter.Factory
里面会自动加上一个Calll
,返回LiveData
会报类型转换异常,因为responseType
是Call
并不是LiveData
。
JakeWharton回复:https://github.com/square/retrofit/issues/3246
@POST(API.LOGIN)
suspend fun login(@Body any: Any): LiveData<Result<User>>
class LiveDataCallAdapterFactory : Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != LiveData::class.java) {
return null
}
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
val rawObservableType = getRawType(observableType)
if (rawObservableType != ApiResponse::class.java) {
throw IllegalArgumentException("type must be a resource")
}
if (observableType !is ParameterizedType) {
throw IllegalArgumentException("resource must be parameterized")
}
val bodyType = getParameterUpperBound(0, observableType)
return LiveDataCallAdapter<Any>(bodyType)
}
}
class LiveDataCallAdapter<R>(private val responseType: Type) :
CallAdapter<R, LiveData<ApiResponse<R>>> {
override fun responseType() = responseType
override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> {
return object : LiveData<ApiResponse<R>>() {
private var started = AtomicBoolean(false)
override fun onActive() {
super.onActive()
if (started.compareAndSet(false, true)) {
call.enqueue(object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
postValue(ApiResponse.create(response))
}
override fun onFailure(call: Call<R>, throwable: Throwable) {
postValue(ApiResponse.create(throwable))
}
})
}
}
}
}
}
换成Call
会导致GsonConverterFactory
报错。
@POST(API.LOGIN)
suspend fun login(@Body any: Any): Call<Result<User>>
Unable to invoke no-args constructor for retrofit2.Call<com.xxx.xxx.xxx>. Registering an InstanceCreator with Gson for this type may fix this problem.
接收的对象数据结构跟接口返回的一致,然后自己处理返回结果。
@POST(API.LOGIN)
suspend fun login(@Body any: Any): Result<User>
data class Result<T>(val code: Int, val message: String, val data: T)
/**
* @Author ChenXingYu
* @Date 2020/2/24-10:33
*/
class LoginRemoteImpl @Inject constructor(
retrofit: Retrofit
) : BaseRemoteImpl(), LoginRemote {
private val loginService: LoginService = retrofit.create(LoginService::class.java)
override suspend fun login(mobile: String, prefix: String, password: String): ApiResponse<Result<User>> {
val paramMap = mutableMapOf<String, String>()
paramMap["mobile"] = mobile
paramMap["prefix"] = prefix
paramMap["password"] = password
return call { loginRegisterService.login(paramMap) }
}
}
/**
* Common class used by API responses.
* @param the type of the response object
* @Author ChenXingYu
* @Date 2020/2/20-23:18
*/
sealed class ApiResponse<T> {
companion object {
fun <T> create(error: Throwable): ApiErrorResponse<T> {
Timber.e(error.message ?: "unknown error")
return ApiErrorResponse(error.message ?: "unknown error")
}
fun <T> create(resultData: T): ApiResponse<T> {
return if (resultData == null) {
ApiEmptyResponse()
} else {
ApiSuccessResponse(body = resultData)
}
}
}
}
class ApiEmptyResponse<T> : ApiResponse<T>()
data class ApiSuccessResponse<T>(val body: T) : ApiResponse<T>()
data class ApiErrorResponse<T>(val errorMessage: String) : ApiResponse<T>()
Android-Retrofit返回Flow
Flow
部分API还处于实验性状态,有可能在未来的版本中弃用,应避免在生产环境使用可能会弃用的API。
Flow
使用上与Rxjava一样,Rxjava的部分操作符Flow
也有,由于还没稳定就随便说说。先自定义FlowCallAdapterFactory
和FlowCallAdapter
,callbackFlow
(试验中)。
/**
* @Author ChenXingYu
* @Date 2020/4/9-15:19
*/
class FlowCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Flow::class.java) {
return null
}
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
return FlowCallAdapter<Any>(observableType)
}
}
/**
* @Author ChenXingYu
* @Date 2020/4/9-15:21
*/
class FlowCallAdapter<R>(private val responseType: Type) :
CallAdapter<R, Flow<R>> {
override fun responseType() = responseType
@ExperimentalCoroutinesApi
override fun adapt(call: Call<R>): Flow<R> {
return callbackFlow {
val started = AtomicBoolean(false)
if (started.compareAndSet(false, true)) {
call.enqueue(object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
offer(response.body())
close()
}
override fun onFailure(call: Call<R>, throwable: Throwable) {
close(throwable)
}
})
}
awaitClose { call.cancel() }
}
}
}
然后在Retrofit添加进去addCallAdapterFactory(FlowCallAdapterFactory())
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.API_BASE)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(FlowCallAdapterFactory())
.build()
}
上面的做完就可以用Flow作为接受类型。Flow
可以使用catch
(试验中)操作符捕获异常,flowOn
(试验中)操作符切换协程调度器(Dispatchers.Main
,Dispatchers.IO
,Dispatchers.Default
),collect
操作符获取数据,当然还有很多其他的操作符这里就不细说了。
@POST(API.LOGIN)
fun login(@Body any: Any): Flow<Result<User>>
data class Result<T>(val code: Int, val message: String, val data: T)
/**
* @Author ChenXingYu
* @Date 2020/2/24-10:33
*/
class LoginRemoteImpl @Inject constructor(
retrofit: Retrofit
) : BaseRemoteImpl(), LoginRemote {
private val loginService: LoginService = retrofit.create(LoginService::class.java)
override fun login(mobile: String, prefix: String, password: String): Flow<Result<User>> {
val paramMap = mutableMapOf<String, String>()
paramMap["mobile"] = mobile
paramMap["prefix"] = prefix
paramMap["password"] = password
return loginRegisterService.login(paramMap)
}
}
在Repository里返回Flow,Flow可以发射多次数据,比如我们要观察请求开始前和请求完成后。
参考官方示例里面的NetworkBoundResource
做部分修改。数据库推荐Room,版本2.2.0开始支持协程流。
Room使用方法
@Query
DAO 方法的返回类型可以为 Flow
。如果查询中的观察表已失效,则返回的流将重新发出一组新值。声明具有 Channel
返回类型的 DAO 函数是错误的做法,Room 建议您使用 Flow
,然后使用相邻函数将 Flow
转换为 Channel
。b/130428884本次不介绍从本地数据库获取数据,NetworkBoundResource
预留了获取本地数据的支持,通过构造参数dbEnable
传入true
开启或false
关闭。
/**
* @Author ChenXingYu
* @Date 2020/2/24-9:42
*/
class LoginRepositoryImpl @Inject constructor(
private val loginRemote: LoginRemote
) : BaseRepositoryImpl(), LoginRepository {
/**
* 登录
*/
override suspend fun login(mobile: String, prefix: String, password: String)
: Flow<Resource<User>> {
return object : NetworkBoundResource<User>(false) {
/**
* 写入数据库(数据库启用时)
*/
override suspend fun saveCallResult(item: User) {
TODO("Not yet implemented")
}
/**
* 判断数据库数据是否过期或空(数据库启用时)
*/
override fun shouldFetch(data: User?): Boolean {
TODO("Not yet implemented")
}
/**
* 数据库读取(数据库启用时)
*/
override suspend fun loadFromDb(): User? {
TODO("Not yet implemented")
}
/**
* 从网络获取数据
*/
override suspend fun createCall(): ApiResponse<Result<User>> {
return loginRemote.login(mobile, prefix, password)
}
}.asFlow()
}
}
/**
* A generic class that can provide a resource backed by both the sqlite database and the network.
* @Author ChenXingYu
* @Date 2020/2/20-23:44
*
* @param dbEnable 是否启用数据库
*/
abstract class NetworkBoundResource<ResultType>(private val dbEnable: Boolean) {
private suspend fun fetchFromNetwork(flowCollector: FlowCollector<Resource<ResultType>>,
dbSource: ResultType?) {
when (val apiResponse = withContext(Dispatchers.IO) { createCall() }) {
is ApiSuccessResponse -> {
val result = processResponse(apiResponse)
if (dbEnable) {
withContext(Dispatchers.IO) { saveCallResult(result.data) }
val newDbSource = withContext(Dispatchers.IO) { loadFromDb() }
flowCollector.emit(Resource.success(result.message,
newDbSource ?: result.data))
} else {
if (result.code == 1) {
flowCollector.emit(Resource.success(result.message, result.data))
} else {
flowCollector.emit(Resource.error(result.message, result.data))
}
}
}
is ApiEmptyResponse -> {
flowCollector.emit(Resource.error("ApiEmptyResponse", dbSource))
}
is ApiErrorResponse -> {
onFetchFailed()
flowCollector.emit(Resource.error(apiResponse.errorMessage, dbSource))
}
}
}
protected open fun onFetchFailed() {}
suspend fun asFlow(): Flow<Resource<ResultType>> {
return flow {
emit(Resource.loading(null))
if (dbEnable) {
val dbSource = withContext(Dispatchers.IO) { loadFromDb() }
if (shouldFetch(dbSource)) {
fetchFromNetwork(this, dbSource)
} else {
emit(Resource.success(ResUtil.getString(R.string.successfully), dbSource))
}
} else {
fetchFromNetwork(this, null)
}
}
}
protected open fun processResponse(response: ApiSuccessResponse<Result<ResultType>>) = response.body
/**
* 写入数据库(数据库启用时)
*/
protected abstract suspend fun saveCallResult(item: ResultType)
/**
* 判断数据库数据是否过期或空(数据库启用时)
*/
protected abstract fun shouldFetch(data: ResultType?): Boolean
/**
* 数据库读取(数据库启用时)
*/
protected abstract suspend fun loadFromDb(): ResultType?
/**
* 从网络获取数据
*/
protected abstract suspend fun createCall(): ApiResponse<Result<ResultType>>
}
Flow
稳定后可以对NetworkBoundResource
修改支持Remote和Database返回Flow
。
在collect
里面处理结果,Status.LOADING
在NetworkBoundResource
里面emit(Resource.loading(null))
发射出来的。这里还能用Flow
的操作符onStart
(试验中)请求开始前和onCompletion
(试验中)请求完成后,目前这2个操作符在版本1.3.0里是稳定的,可能会在下一个版本转正。
/**
* @Author ChenXingYu
* @Date 2020/2/24-9:44
*/
class LoginViewModel @Inject constructor(
private val loginRepository: LoginRepository
) : BaseViewModel() {
/**
* 用户信息
*/
val userLiveData = MutableLiveData<User>()
/**
* 登录
*/
fun login(mobile: TextInputLayout, prefix: TextInputLayout, password: TextInputLayout) {
viewModelScope.launch {
loginRepository.login(
mobile.editText?.text.toString(),
prefix.editText?.text.toString(),
password.editText?.text.toString()
).collect {
when (it.status) {
Status.LOADING -> {
// 加载中...
}
Status.SUCCESS -> {
it.data?.let { entity ->
userLiveData.value = entity
ToastUtil.makeTextShort(ResUtil.getString(R.string.login_successfully))
}
}
Status.ERROR -> {
it.message?.let { message -> ToastUtil.makeTextShort(message) }
}
}
}
}
}
}
// 在Activity或Fragment里面观察LiveData
mLoginViewModel.userLiveData.observe(this, Observer {
// 登录成功
...
})