使用Kotlin+协程+MVVM+Jetpack搭建快速开发框架

使用Kotlin+协程+MVVM+Jetpack搭建快速开发框架

  • 文章目录
    • 简介
      • 相关概念
    • MVVM的具体实现
    • 协程的使用与封装
    • Retrofit的使用
    • 网络请求的实现
    • 项目地址
    • 总结

文章目录

简介

近日有网友在评论区留言,希望我能写一篇关于kotlin+mvvm的框架相关的博客,于是,笔者与百忙之中,抽出时间,对之前框架做出了相关的整理与优化,并完成了相关技术点的疑难解决。本着开源的相关精神,在此,将相关成果共享给大家。

相关概念

  • Kotlin是由JetBrains创建的基于JVM的静态编程语言。包含了很多函数式的编程思想的面向对象的的编程语言。kotlin仿佛是为了解决java中存着的一些天生的缺陷而量身定做一般。精简的语法,null-safety,相对应java8更进一步的lambda表达式支持,使用block完美的代替了接口回调,自定义扩展函数的使用等等,使得开发者可以编写尽量少的样板代码。谷歌爸爸早就在2017年就已经宣布kotlin为安卓的官方开发语言了。身为安卓开发从业人员,学习kotlin很有必要。

  • 协程也叫微线程,是一种新的多任务并发的操作手段。 协程是创造出来解决异步问题,线程的调度是操作系统层面完成是抢占式的;协程是非抢占式的,是协作运行的,是由应用层完成调度,协程在挂起的时候不会堵塞线程,只会将当前运行的状态存在内存中,当协程执行完成后,会恢复之前保存的状态继续运行。协程的内部是通过状态机实现的。
    协程具有以下特性

    • 可控制:协程能做到可被控制的发起子任务(协程的启动和停止都由代码控制,不像 java)
    • 轻量级:协程非常小,占用资源比线程还少
    • 语法糖:使多任务或多线程切换不再使用回调语法
      本框架中使用协程封装来实现异步的网络请求。
  • MVVM 往往是通过databinding的方式将view层与viewmodel层进行双向绑定,相关的逻辑处理交予viewmodel层中处理,然后通过接口或者livedata的形式传入到view层中进行相关展示工作。viewmodel中并不持有view的实例。

  • Jetpack 是一个丰富的组件库,它的组件库按类别分为 4 类,分别是架构(Architecture)、界面(UI)、行为(behavior)和基础(foundation)。每个组件都可以单独使用,也可以配合在一起使用。每个组件都给用户提供了一个标准,能够帮助开发者遵循最佳做法,减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者能够集中精力编写重要的业务代码。本框架中主要用到了livedata,lifecycles,以及viewmodel。
    使用Kotlin+协程+MVVM+Jetpack搭建快速开发框架_第1张图片

MVVM的具体实现

主要包括BaseMvvmActivity,BaseViewModel,BaseMvvmView

  • BaseMvvmActivity
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,包括errorviewwaitdialog等app基础功能封装到底层,于是在底层进行封装时,必须将业务层的xml文件在底层进行重新注入绑定,此时就会出现问题:就是将业务层xml放入到基层的xml中,基层相关功能是失效的。查看相关源码得知,当我们调用DataBindingUtil.setContentView() 方法时,最中还会调用到activity.setContentView(),此时在加入到基层的xml中,基层功能会完全失效。而如果是先加入到基层的xml,然后实行DataBinding的绑定,则Databinding的双向绑定完全失效,经过笔者的多方调试以及查看源码,解决了此问题。即先将进行绑定,然后加入到底层的xml中,注入相关功能,最后再次调用activity.setContentView()。一定要注意相关顺序,这样才能同时实现双向绑定和底层多状态布局的复用。

  • BaseViewModel
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中的状态。这一点完全根据个人的开发习惯而来

  • BaseMvpView BaseMvpView中主要定义了一些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()
}

协程的使用与封装

  • 协程的引入
    • 首先我们需要在baseUI中引入相应的库
      implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1’
      implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1’
      如图
      在这里插入图片描述
  • 我们需要在gradle脚本中添加coroutines支持
    kotlin {
    experimental {
    coroutines ‘enable’
    }
    }
    在这里插入图片描述
  • 协程的引入
    我们的BaseViewModel 直接继承与ViewModel,并实现了LifecycleObserver 进行生命周期的管理,协程可以直接使用ViewModel的viewModelScope即可
 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的使用

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的便捷开发架构。

你可能感兴趣的:(开源,mvvm,jetpack,kotlin,android,移动开发)