使用Kotlin+協程+MVVM+Jetpack搭建快速開發框架

文章目錄

簡介

近日有網友在評論區留言,希望我能寫一篇關於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。
    在這裏插入圖片描述

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的便捷開發架構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章