Kotlin Coroutine在 Android 中的使用和封裝

kotlin出來不少年,並被google被賦予Android首選開發語言的地位,仍是有必要研究一下的,基礎語法再也不復述了,只簡單聊聊kotlin中的核心內容之一:協程 Coroutine。java

相關概念和API能夠直接進入官網查詢, 不過不推薦kotlin中文網,文章翻譯的略顯生硬,看了更容易迷惑。android

本文不講基礎(基礎我也不怎麼清楚。。),適合喜歡拿來主義的同窗api

協程做爲輕量級的線程,是創建在線程之上的調度,比線程節省資源但不意味着不消耗資源,在Android這種系統資源相對珍貴的環境中,當異步繁瑣的調用或者切換線程,如何及時的回收也是必要的工做。bash

1、添加依賴配置

coroutine 是在kotlin 1.3中發佈的1.0正式版,因此建議優先使用這個版本。網絡

項目根目錄中gradle配置:app

buildscript {
    ext.kotlin_version = '1.3.0'
    repositories {
        google()
        mavenCentral()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

複製代碼

主module中gradle配置異步

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
}

// 實驗性開啓協程的配置,目前版本中已經不須要了
kotlin{             
    experimental{
        coroutines 'enable'
    }
}
複製代碼

假如你的其餘module也使用kt代碼,也須要添加上述依賴,否則有可能會出現找不到class或符號相關編譯錯誤async


2、替換RxJava和升級Retrofit

通常項目中會引用retrofit + RxJava的組合進行網絡請求,或者你單獨封裝了一層RxJava來進行異步操做、切換線程來處理你的業務maven

RxJava雖然操做符衆多(得有100個以上了吧),上手不易,但着實好用,一條鏈走下來搞定你的所有業務,若是有網絡請求配合上retrofit這種支持Rx的庫,不要太給力。ide

不過函數式開發有個通病,這條鏈子上會建立大量對象,回收內存抖動你不得不考慮,雖然使用了線程池,可開銷不容小覷。

RxJava 雖然能夠鏈式調用,但終究仍是基於回調形式,而協程徹底作到了同步方式寫異步代碼。

看下之前Retrofit的寫法(簡易)

object RetrofitHelper: Interceptor {

    init {
        initOkHttpClient();
    }

    private var mOkHttpClient: OkHttpClient? = null

    private val BASE_GANK_URL = "http://gank.io/api/"

    fun getRetroFitBuilder(url :String) : Retrofit {
        return Retrofit.Builder()
                .baseUrl(url)
                .client(mOkHttpClient)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

    fun getGankMeiziApi(): GankMeiziAPI {
        return getRetroFitBuilder(BASE_GANK_URL).create(GankMeiziAPI::class.java)
    }

interface GankMeiziAPI {
    @GET("data/福利/{number}/{page}")
    abstract fun getGankMeizi(@Path("number") number: Int, @Path("page") page: Int): Observable<GankMeiziResult>
}

class GankMeiziPresenter : BaseMvpPresenter<IGankMeiziView>() {

    fun getGankList(context: RxAppCompatActivity, pageNum: Int , page :Int){
        RetrofitHelper.getGankMeiziApi()
                .getGankMeizi(pageNum, page)
                .subscribeOn(Schedulers.io())
                .compose(context.bindUntilEvent(ActivityEvent.DESTROY))
                .filter({ gankMeiziResult -> !gankMeiziResult.error })
                .map({ gankMeiziResult -> gankMeiziResult.results })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ gankMeiziInfos ->
                    mvpView?.showSuccess(gankMeiziInfos)
                }, { throwable ->
                    mvpView?.showError()
                })
    }
}
複製代碼

RxJava 的調用鏈上使用RxLife來綁定生命週期(或者使用Uber的autodispose),很常見的代碼。



對於RxJava你還有可能會封裝一個小工具(主要是爲了生命週期和回收問題)

public class RxImpl {

    public static <T> void exeOnLife(final AppCompatActivity mActivity , Observable<RespBody<XueError, T>> observable, final Accept acceptFun){
        if (mActivity == null || ActivityHelper.activityIsDead(mActivity) || observable == null) return;
        observable.as(AutoDispose.<RespBody<XueError,T>>autoDisposable(AndroidLifecycleScopeProvider.from(mActivity, Lifecycle.Event.ON_DESTROY)))
                .subscribe(new Consumer<RespBody<XueError, T>>() {
                    @Override
                    public void accept(RespBody<XueError, T> respBody) throws Exception {
                        if (acceptFun == null) return;
                        acceptFun.accept(respBody);
                    }
                });
    }

    public interface Accept <E extends XueError,T extends Object> {
        void accept(RespBody<E, T> respBody);
    }
}
複製代碼

如今呢,想用協程怎麼辦,假設你如今已經知道了如何啓動一個協程

GlobalScope.launch { }
        
GlobalScope.async {}        

複製代碼

也知道了launch 和 async的區別(返回值不一樣),也知道了須要一個返回值和多個返回值的選擇(async 和 reduce),那在Android中如何使用呢



3、封裝

協程Job具備層級關係,父協程控制子協程的運行,取消父協程的運行,也就至關於關閉了全部你開在父協程裏的所有任務,因此在基類中你須要聲明一個全局的協程上下文,保證你開啓的協程都處於這個context環境中,在onDestroy下取消全局context,就達到了你的目的

abstract class AbstractFragment : RxFragment() , CoroutineScope by MainScope() {

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}
複製代碼

你能夠像封裝RxJava同樣,封裝一個工具來使用協程,同時遵循一些原則:

1 調度器

你開啓的協程須要有調度器,就像RxJava自由切換線程同樣,但主線程調度器最好指定Main來避免沒必要要的麻煩。

舉個例子,你使用RxJava+autodipose來綁定 生命週期,但你在子線程用RxJava又開啓了一個新線程,調度回UI線程刷新view的時候會發生崩潰

2 父協程和子協程有強烈的層級關係

cancel一個Job 會拋出 CancellationException ,但這個異常不須要開發人員管理,但若是協程內部發生了非 CancellationException ,則會使用這個異常來取消父Coroutine。爲了保證穩定的Coroutine層級關係,這種行爲不能被修改。 而後當全部的子Coroutine都終止後,父Coroutine會收到原來拋出的異常信息。


下邊是個協程的簡易封裝案例

fun <T> executeRequest(context : CoroutineContext, request: suspend () -> T?, onSuccess: (T) -> Unit = {}, onFail: (Throwable) -> Unit = {}): Job {
    return CoroutineScope(Dispatchers.Main).launch(context) {
        try {
            val res: T? = withContext(Dispatchers.IO) { request() }
            res?.let {
                onSuccess(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
            onFail(e)
        }
    }
}
複製代碼

context 視業務狀況而定是否傳入

而後在你的act或Fragment中直接調用

class MyFragment : BaseFragment {
    
    onAcitvityCreate(){
        executeRequest<Body>(
            context,
                request = {
                    // 異步任務
                },

                onSuccess = {
                   
                },

                onFail = {
                    
                }
        )
    }
}
複製代碼




對於Retrofit修改更爲簡單,2.6版本已經支持了協程,網上有不少介紹

interface GankMeiziAPI {
    @GET("data/福利/{number}/{page}")
    suspend fun getGankMeizi(@Path("number") number: Int, @Path("page") page: Int): GankMeiziResult
}
複製代碼

將Observable返回值修改成你須要的bean, 加上suspend標記便可, 下邊是示例代碼:

return executeRequest<GankMeiziResult>(
               context,
                request = {
                    RetrofitHelper.getGankMeiziApi().getGankMeizi(pageNum, page)
                },

                onSuccess = {
                    mvpView?.showSuccess(it.results)
                },

                onFail = {
                    mvpView?.showError()
                }
        )
複製代碼

4、混淆

網上的帖子大多以demo的思路來寫,商用確定要混淆了,不添加proguard你混淆後就死翹翹了,因此直接貼上來

coroutine的配置

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}
複製代碼

假如release打包後你的Retrofit 請求結果發生了npe,檢查下你的bean是否添加了不混淆配置



其餘

研究協程沒多久,有錯誤或建議歡迎指出,共同交流

另外推薦雲在千峯的博客 ,是我目前看過的最友好的coroutine系列文章

相關文章
相關標籤/搜索