文章目錄
簡介
近日有網友在評論區留言,希望我能寫一篇關於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的便捷開發架構。