使用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,

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,包括errorview,waitdialog等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’
如圖

- 首先我們需要在baseUI中引入相應的庫
- 我們需要在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的便捷開發架構,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/255926.html
標籤:其他
