近日有网友在评论区留言,希望我能写一篇关于kotlin+mvvm的框架相关的博客,于是,笔者与百忙之中,抽出时间,对之前框架做出了相关的整理与优化,并完成了相关技术点的疑难解决。本着开源的相关精神,在此,将相关成果共享给大家。
Kotlin是由JetBrains创建的基于JVM的静态编程语言。包含了很多函数式的编程思想的面向对象的的编程语言。kotlin仿佛是为了解决java中存着的一些天生的缺陷而量身定做一般。精简的语法,null-safety,相对应java8更进一步的lambda表达式支持,使用block完美的代替了接口回调,自定义扩展函数的使用等等,使得开发者可以编写尽量少的样板代码。谷歌爸爸早就在2017年就已经宣布kotlin为安卓的官方开发语言了。身为安卓开发从业人员,学习kotlin很有必要。
协程也叫微线程,是一种新的多任务并发的操作手段。 协程是创造出来解决异步问题,线程的调度是操作系统层面完成是抢占式的;协程是非抢占式的,是协作运行的,是由应用层完成调度,协程在挂起的时候不会堵塞线程,只会将当前运行的状态存在内存中,当协程执行完成后,会恢复之前保存的状态继续运行。协程的内部是通过状态机实现的。
协程具有以下特性
MVVM 往往是通过databinding的方式将view层与viewmodel层进行双向绑定,相关的逻辑处理交予viewmodel层中处理,然后通过接口或者livedata的形式传入到view层中进行相关展示工作。viewmodel中并不持有view的实例。
Jetpack 是一个丰富的组件库,它的组件库按类别分为 4 类,分别是架构(Architecture)、界面(UI)、行为(behavior)和基础(foundation)。每个组件都可以单独使用,也可以配合在一起使用。每个组件都给用户提供了一个标准,能够帮助开发者遵循最佳做法,减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者能够集中精力编写重要的业务代码。本框架中主要用到了livedata,lifecycles,以及viewmodel。
主要包括BaseMvvmActivity,BaseViewModel,BaseMvvmView
abstract class BaseMvvmActivity<V : ViewDataBinding, VM : BaseViewModel> : SwipeBackActivity(),
BaseMvpView,
View.OnClickListener,
LifecycleObserver {
lateinit var mBinding: V
lateinit var mViewModel: VM
private var providerVMClass: Class<VM>? = null
private var receiver: MyNetBroadCastReciver? = null
lateinit var mActivity: BaseMvvmActivity<*, *>
lateinit var mRootView: ViewGroup
/**
* 注入绑定
*/
private fun initViewDataBinding() {
//DataBindingUtil类需要在project的build中配置 dataBinding {enabled true }, 同步后会自动关联android.databinding包
mBinding = DataBindingUtil.setContentView(this, layoutId)
mBinding.setVariable(initVariableId(), mViewModel)
mBinding.executePendingBindings()
//liveData绑定activity,fragment生命周期
mBinding.lifecycleOwner = this
mRootView = mBinding.root as ViewGroup
mRootViewParent= layoutInflater.inflate(R.layout.activity_base, null) as LinearLayout?
mRootView.parent?.let {
(mRootView.parent as ViewGroup).removeView(mRootView)
}
mContentView ?.addView(mRootView)
setContentView(mRootViewParent)
if (null != intent) handleIntent(intent)
initView(mRootViewParent!!)
加粗文本
此处有一个要点。由于笔者的开发习惯,总是习惯于,将headview,包括errorview,waitdialog等app基础功能封装到底层,于是在底层进行封装时,必须将业务层的xml文件在底层进行重新注入绑定,此时就会出现问题:就是将业务层xml放入到基层的xml中,基层相关功能是失效的。查看相关源码得知,当我们调用DataBindingUtil.setContentView() 方法时,最中还会调用到activity.setContentView(),此时在加入到基层的xml中,基层功能会完全失效。而如果是先加入到基层的xml,然后实行DataBinding的绑定,则Databinding的双向绑定完全失效,经过笔者的多方调试以及查看源码,解决了此问题。即先将进行绑定,然后加入到底层的xml中,注入相关功能,最后再次调用activity.setContentView()。一定要注意相关顺序,这样才能同时实现双向绑定和底层多状态布局的复用。
open class BaseViewModel : ViewModel(), LifecycleObserver, BaseMvvmView {
val vStatus: MutableLiveData<Map<String, Any>> = MutableLiveData()
override fun showWaitDialog() {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWWAITDIALOG
vStatus.value = viewStatus
}
override fun showWaitDialog(message: String) {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWWAITDIALOG
viewStatus["msg"] = message
vStatus.value = viewStatus
}
override fun hideWaitDialog() {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.HIDEWAITDIALOG
vStatus.value = viewStatus
}
override fun showToast(msg: String?) {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWTOAST
viewStatus["msg"] = msg ?: "error"
vStatus.value = viewStatus
}
override fun showStatusEmptyView(emptyMessage: String) {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWSTATUSEMPTYVIEW
viewStatus["msg"] = emptyMessage
vStatus.value = viewStatus
}
override fun showStatusErrorView(emptyMessage: String?) {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWSTATUSERRORVIEW
viewStatus["msg"] = emptyMessage ?: "未知错误"
vStatus.value = viewStatus
}
override fun showStatusLoadingView(loadingMessage: String) {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.SHOWSTATUSLOADINGVIEW
viewStatus["msg"] = loadingMessage
vStatus.value = viewStatus
}
override fun hideStatusView() {
var viewStatus = HashMap<String, Any>()
viewStatus["status"] = ViewStatusEnum.HIDESTATUSVIEW
vStatus.value = viewStatus
}
fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
block() }
}
suspend fun <T> launchIO(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.IO) {
block
}
}
fun launch(tryBlock: suspend CoroutineScope.() -> Unit) {
launchOnUI {
tryCatch(tryBlock, {
}, {
})
}
}
fun launchWithTryCatch(
tryBlock: suspend CoroutineScope.() -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
launchOnUI {
tryCatch(tryBlock, catchBlock, finallyBlock)
}
}
private suspend fun tryCatch(
tryBlock: suspend CoroutineScope.() -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
try {
tryBlock()
} catch (e: Throwable) {
catchBlock(e.message)
} finally {
finallyBlock()
}
}
}
/**
* 网络请求
*
*/
fun <T> launchRequest(
tryBlock: suspend CoroutineScope.() -> Result<T>?,
successBlock: suspend CoroutineScope.(T?) -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
launchOnUI {
requestTryCatch(tryBlock, successBlock, catchBlock, finallyBlock)
}
}
suspend fun <T> getResopnse(response: Result<T>?): T? {
if (response == null || EmptyUtils.isEmpty(response)) return null
if (response.code == 0) return response.result
else return null
}
private suspend fun <T> requestTryCatch(
tryBlock: suspend CoroutineScope.() -> Result<T>?,
successBlock: suspend CoroutineScope.(T?) -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
try {
var response = tryBlock()
callResponse(
response,
{
successBlock(response?.result)
},
{
catchBlock(response?.message)
}
)
} catch (e: Throwable) {
var errMsg = ""
when (e) {
is UnknownHostException -> {
errMsg = "No network..."
}
is SocketTimeoutException -> {
errMsg = "Request timeout..."
}
is NumberFormatException -> {
errMsg = "Request failed, type conversion exception"
}
else -> {
errMsg = e.message.toString()
Log.e("xxxxxxxxxx", Gson().toJson(e))
}
}
catchBlock(errMsg)
} finally {
finallyBlock()
}
}
}
/**
* 主要用于处理返回的response是否请求成功
*/
suspend fun <T> callResponse(
response: Result<T>?, successBlock: suspend CoroutineScope.() -> Unit,
errorBlock: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
when {
response == null || EmptyUtils.isEmpty(response) -> errorBlock()
response.code == 0 -> successBlock()
else -> errorBlock()
}
}
}
}
这里笔者在BaseViewModel中实现了BaseMvpView中的众多方法,然后将相关的状态通过livedata进行传递。这样可以直接在viewmodel中去调用相关的view中的状态。这一点完全根据个人的开发习惯而来
interface BaseMvvmView {
fun showWaitDialog()
fun showWaitDialog(message: String)
fun showWaitDialog(message: String, cancelable: Boolean)
fun hideWaitDialog()
fun showToast(msg: String?)
fun showStatusEmptyView(emptyMessage: String)
fun showStatusErrorView(emptyMessage: String?)
fun showStatusLoadingView(loadingMessage: String)
fun showStatusLoadingView(loadingMessage: String, isHasMinTime: Boolean)
fun hideStatusView()
}
fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
block() }
}
fun <T> launchRequest(
tryBlock: suspend CoroutineScope.() -> Result<T>?,
successBlock: suspend CoroutineScope.(T?) -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
launchOnUI {
requestTryCatch(tryBlock, successBlock, catchBlock, finallyBlock)
}
}
launchRequest方法中有四个函数作为参数。
tryBlock:返回的是一个由Result包裹的泛型参数。主要用于网络请求的调用。
successBlock:主要用于请求成功的调用。相当要onSuccess回调。
catchBlock:主要用于请求失败的调用。相当于onError回调。
finallyBlock:主要用于请求完成的回调。相当于onComplete回调
requestTryCatch方法中对协程进行了tryCatch操作。并对结果进行了相应的处理
private suspend fun <T> requestTryCatch(
tryBlock: suspend CoroutineScope.() -> Result<T>?,
successBlock: suspend CoroutineScope.(T?) -> Unit,
catchBlock: suspend CoroutineScope.(String?) -> Unit,
finallyBlock: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
try {
var response = tryBlock()
callResponse(
response,
{
successBlock(response?.result)
},
{
catchBlock(response?.message)
}
)
} catch (e: Throwable) {
var errMsg = ""
when (e) {
is UnknownHostException -> {
errMsg = "No network..."
}
is SocketTimeoutException -> {
errMsg = "Request timeout..."
}
is NumberFormatException -> {
errMsg = "Request failed, type conversion exception"
}
else -> {
errMsg = e.message.toString()
}
}
catchBlock(errMsg)
} finally {
finallyBlock()
}
}
}
callResponse,用于对请求的结果进行处理。
suspend fun <T> callResponse(
response: Result<T>?, successBlock: suspend CoroutineScope.() -> Unit,
errorBlock: suspend CoroutineScope.() -> Unit
) {
coroutineScope {
when {
response == null || EmptyUtils.isEmpty(response) -> errorBlock()
response.code == 0 -> successBlock()
else -> errorBlock()
}
}
}
retrofit的使用和封装相信大家都很了解了,这里就不多讲了,直接贴出代码来吧
// An highlighted block
interface ApiServices {
/**
* 用户登录
*/
@GET("login?key=00d91e8e0cca2b76f515926a36db68f5")
fun requestLoginOut( @Query("phone") phone: String,
@Query("passwd") passwd: String): Deferred<Result<LoginBean>>
@GET("createUser?key=00d91e8e0cca2b76f515926a36db68f5")
fun requestRegister(
@Query("phone") phone: String,
@Query("passwd") passwd: String
): Deferred<Result<RegisterBean>>
}
ApiHelper初始化okhttp,和Retrofit。
// An highlighted block
object ApiHelper {
private var api: ApiServices? = null
fun api(): ApiServices? {
if (api == null)
initApi()
return api
}
/**
* 初始化api
*/
fun initApi() {
// Header
val headerInter = Interceptor {
chain ->
val builder = chain.request()
.newBuilder()
chain.proceed(builder.build())
}
val mOkHttpClient = OkHttpClient()
.newBuilder()
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(headerInter)
.addInterceptor(LoggingInterceptor())
.build()
//网络接口配置
api = null
api = Retrofit.Builder()
.baseUrl("https://www.apiopen.top/")
.addConverterFactory(ScalarsConverterFactory.create()) //添加字符串的转换器
.addConverterFactory(GsonConverterFactory.create()) //添加gson的转换器
.addCallAdapterFactory(CoroutineCallAdapterFactory.invoke()) //添加携程的请求适配器 .client(mOkHttpClient)
.client(mOkHttpClient)
.build()
.create(ApiServices::class.java)
}
}
希望大家注意一下这里的Deferred
这里的Deferred用于接收一个Coroutine的返回结果。
协程的请求适配器,需要引入“com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2
”
现在我们可以来使用我们封装的协程来实现网络请求了。
在presenter层中定义了两个请求。案例如下
class MainViewModel : BaseViewModel() {
var edit: MutableLiveData<String> = MutableLiveData()
var test: MutableLiveData<String> = MutableLiveData()
fun requestTestData() {
showWaitDialog()
launchRequest({
ApiHelper.api().requestTestApi("utf-8", "卫衣").await()
}, {
data: List<List<String>>? ->
test.value = data.toString()
}, {
errMsg: String? ->
showToast(errMsg)
}, {
hideWaitDialog()
})
}
}
xml文件中的绑定
<!--单向绑定@{
}-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{mainVM.edit}"
android:layout_marginTop="50dp"
android:textColor="@color/black"
android:textSize="25dp" />
<!--双向绑定@={
}-->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={mainVM.edit}"
android:layout_marginTop="50dp"
android:textSize="25dp" />
传送门
此项目已经托管到github并且开源。如果想看源码点击传送门查看。如果觉得还可以,不妨给在下一个star。谢谢大家。
本片文章主要是为了用协程代替RxJava实现网络请求的异步,使用livedata进行通讯,lifecycles进行生命周期管理,打造Kotlin+协程+mvvm+jetpack的便捷开发架构。