Android-AAC架构之Kotlin协程

architecture-components-samples官方示例
在协程和 Flow 使用 LiveData

Android-AAC架构之Kotlin协程_第1张图片
本文涉及Kotlin协程,Retrofit,LiveData,ViewModel,Dagger2.Android。
在官方示例GithubBrowserSample里面没有使用协程,在实际项目里需要修改部分代码。有什么不对的地方欢迎提出。

1、Retrofit - suspend

说说这里遇到问题,加上suspend时返回类型不能是CallCallAdapterFactory返回的自定义类,必须是接口返回的数据结构。下面这段代码是官方示例里面的LiveDataCallAdapterFactoryLiveDataCallAdapter返回一个LiveData,在CallAdapter.Factory里面会自动加上一个Calll,返回LiveData会报类型转换异常,因为responseTypeCall并不是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>()

2、Retrofit - Flow

Android-Retrofit返回Flow

Flow部分API还处于实验性状态,有可能在未来的版本中弃用,应避免在生产环境使用可能会弃用的API。
Flow使用上与Rxjava一样,Rxjava的部分操作符Flow也有,由于还没稳定就随便说说。先自定义FlowCallAdapterFactoryFlowCallAdaptercallbackFlow(试验中)。

/**
 * @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.MainDispatchers.IODispatchers.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)
    }

}

3、Repository

在Repository里返回Flow,Flow可以发射多次数据,比如我们要观察请求开始前和请求完成后。

Android-AAC架构之Kotlin协程_第2张图片
参考官方示例里面的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

4、ViewModel - LiveData

collect里面处理结果,Status.LOADINGNetworkBoundResource里面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 {
     // 登录成功
     ...
})

你可能感兴趣的:(Android)