Edited at

Android + Kotlin Coroutine + Android Architecture Componentsで非同期

More than 1 year has passed since last update.

Android Architecture ComponentsLifecycleKotlin-coroutinesでライフサイクルとバインドした非同期処理を簡単に書けるようにしてみた話

Androidでの非同期といえばAsyncTaskとかRxJava+RxAndroidとかあります。

AsyncTaskだと非同期処理中にActivity等のライフサイクルが終わったときの処理を書くのが非常に面倒ですから、どちらを使うかと言えば後者なのですが、後者でもFlowableとか使うときはとてもいいのですがSingleを使うとなると少々冗長になりがちだと思います。

さらにどちらにしてもonDestory()Disposable#dispose()を呼ぶ等しなければいけませんし書き忘れがちです。

それに、async/awaitのように簡潔に書けるわけでは無いです。

なのでkotlin-coroutineのasync/awaitとAndroid Architecture ComponentsのLifecycleを用いて簡潔にかつ安全に非同期処理を書けるようにしていきたいと思います。

~追記~

「非同期処理周り」の_Coroutine.ktで定義されている関数bindLaunchにてミスがあったため修正しました。

稀にJob内の非同期処理が終わるタイミングによって、onDestoroyが呼ばれてもJobがキャンセルされないといったミスです。

またkotlinx.coroutineの23.0からCoroutineScope.coroutineContextが非推奨になっているのでそれに合わせた変更もしました。


非同期処理のレスポンス

非同期処理をするならば、そのレスポンスで成功か失敗かを判断できるようにしたいところです。

なのでまずは非同期処理のレスポンスを格納するクラスを作成します。


Response.kt

//非同期処理のResponse

sealed class Response<R>

//成功時のResponse
data class Success<R>(val result: R) : Response<R>()

//失敗時のResponse
data class Failure<R>(val error: Throwable) : Response<R>()


Responsesealed classにすることで継承を制限し、when式などで型チェックを行うことで成功時と失敗時のそれぞれの値を取り出すことができるようにしてみました。

与えた型変数はSuccessでしか使いませんが、ここはResponseをダウンキャストしたときの利便性重視です。

値を取り出す際は例えば


sample.kt

when (response) {

is Success -> {
//成功時の処理
}
is Failure -> {
//失敗時の処理
}
}

というふうにすればそれぞれの枝でResponse型のオブジェクトがスマートキャストされて値が取り出せるようになります。

いちいちwhenif等を用いてキャストするのが面倒なのであれば、何かしらの関数を定義してやればいいと思います


_Resonse.kt

//Successにセーフキャスト

fun <R> Response<R>.success(): Success<R>? = this as? Success

//Failureにセーフキャスト
fun <R> Response<R>.failure(): Failure<R>? = this as? Failure

//Successな時に関数オブジェクト実行
inline fun <R> Response<R>.successIf(handle: (R) -> Unit) {
val result = success()?.result ?: return
handle(result)
}

//Failureな時に関数オブジェクト実行
inline fun <R> Response<R>.failureIf(handle: (Throwable) -> Unit) {
val throwable = failure()?.error ?: return
handle(throwable)
}

//Success、もしくはresultがnullじゃないときはその値を返して、そうでなければdefaultの値を生成して返す
fun <R> Response<R>.getOrElse(default: () -> R): R = success()?.result ?: default()

//resultを他の型や値に変換して返す
inline fun <R, T> Success<R>.map(transform: (R) -> T): T = transform(result)


こんな感じで


非同期処理周り

非同期処理の結果を格納するクラスが出来たところで、肝心のasync/awaitをLifecycleとバインドさせる仕組みを作っていきたいと思います。

kotlinのasync/awaitはlaunch()などの関数でコルーチンを起動して使う感じです。

なのでその起動したコルーチンのキャンセルをLifecycleのonDestroy()のタイミングで行うことで、非同期処理中にActivityが死んで、コールバックの際にNPEが発生して死んでしまう。ということを防げるはず。


_Coroutine.kt

import android.arch.lifecycle.*

import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.NonCancellable.invokeOnCompletion
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.intrinsics.startCoroutineCancellable
import java.lang.ref.WeakReference
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.coroutines.experimental.coroutineContext
import kotlin.reflect.KClass

//いちいちDeferred<Response<R>>って書くのめんどくさい
typealias DeferredResponse<R> = Deferred<Response<R>>

//与えられた関数オブジェクトを実行してResponseとして返す
internal inline fun <R> generateResponse(vararg expected: KClass<out Throwable> = arrayOf(Throwable::class),
block: () -> R): Response<R> = try {
Success(block())
} catch (t: Throwable) {
if (expected.any { it.isInstance(t) })
Failure(t)
else throw t
}

//coroutineのasyncをResponseを返せるように拡張する感じ
fun <R> asyncResponse(context: CoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
vararg expected: KClass<out Throwable> = arrayOf(Throwable::class),
block: suspend () -> R): DeferredResponse<R> =
async(context, start, parent) { generateResponse(*expected) { block() } }

//以下、JobのLifecycleとのバインド周り

//Jobを保持してonDestroyでそれをキャンセルするLifecycleObserverを生成する
private fun createLifecycleObserver(job: Job) = object : LifecycleObserver {
val mJobRef = WeakReference<Job>(job)

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
val j = mJobRef.get() ?: return
val completed = j.isCompleted
if (!completed) {
j.cancel()
}
}
}

//これだと稀にLifecycleObserverをLifecycleOwnerに登録する前に、
//Job内の非同期処理が終了してしまって駄目なことがります。
//LifecycleにバインドされたJobを起動する
//fun bindLaunch(owner: LifecycleOwner, context: CoroutineContext = UI,
// start: CoroutineStart = CoroutineStart.DEFAULT,
// parent: Job? = null,
// block: suspend CoroutineScope.() -> Unit
//) = launch(context, start, parent, block).apply {
// val observer = createLifecycleObserver(this)
// val lifecycle = owner.lifecycle
// lifecycle.addObserver(observer)
// //コルーチンが終わればObserverをremove()
// invokeOnCompletion { lifecycle.removeObserver(observer) }
//}

//修正後のbindLaunch↓

//Jobを生成する関数
private fun createLazyCoroutine(context: CoroutineContext,
block: suspend CoroutineScope.() -> Unit
) = object : AbstractCoroutine<Unit>(context, false) {
override fun onStart() {
block.startCoroutineCancellable(this, this)
}
}

private fun createCoroutine(context: CoroutineContext) =
object : AbstractCoroutine<Unit>(context, true) {}

//修正したbindLaunch
//Jobを生成後にJobを元にして生成したLifecycleObserverを、
//LifecycleOwner登録してからJobを実行するように修正
fun bindLaunch(owner: LifecycleOwner, context: CoroutineContext = UI,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> Unit): Job {

val newContext = newCoroutineContext(context, parent)

val coroutine = if (start.isLazy)
createLazyCoroutine(newContext, block) else
createCoroutine(newContext)

val observer = createLifecycleObserver(coroutine)
val lifecycle = owner.lifecycle
lifecycle.addObserver(observer)
invokeOnCompletion { lifecycle.removeObserver(observer) }
coroutine.start(start, coroutine, block)
return coroutine
}

//修正後のbindLaunch↑

//CoroutineScope.coroutineContextは非推奨
//val CoroutineScope.defaultContext: CoroutineContext
// get() = coroutineContext + CommonPool

//async()によく渡すであろうCoroutineContext
suspend inline fun defaultContext() = coroutineContext + CommonPool


_Coroutine.ktとは別のパッケージにLifecycleOwnerから呼び出せるbindLaunch()を定義


_LifecycleOwner.kt

import android.arch.lifecycle.LifecycleOwner

import kotlinx.coroutines.experimental.CoroutineScope
import kotlinx.coroutines.experimental.CoroutineStart
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import tech.ketc.anktfw.androidarch.croutine.bindLaunch
import kotlin.coroutines.experimental.CoroutineContext

//bindLaunchをLifecycleOwnerの拡張関数として定義したやつ
fun LifecycleOwner.bindLaunch(context: CoroutineContext = UI,
start: CoroutineStart = CoroutineStart.DEFAULT,
parent: Job? = null,
block: suspend CoroutineScope.() -> Unit) = bindLaunch(this, context, start, parent, block)


bindLaunch()で起動したコルーチンの中でasyncResponse()を呼び出すことでDeferredResponseを得られそこからawait()を呼び出せば処理を開始して終了するまでコルーチンを中断し、終了すればResponseを返す、と言ったことができるようになります。

例としては


SampleActivity.kt

class AsyncSampleActivity : AppCompatActivity() {

//onCreate()とかいろいろ
....

private val mImageDownloadDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(10).asCoroutineDispatcher()
private suspend inline fun imageDownloadContext() = coroutineContext + mImageDownloadDispatcher

private suspend fun downloadImage(url: String): DeferredResponse<Bitmap> = asyncResponse(imageDownloadContext()) {
//画像をダウンロードして落としてくる処理
}

private fun startImageDownload() = bindLaunch {
val response = downloadImage("http://hoge.com/hoge.jpg").await()
response.successIf {
//ImageViewにセットするとか
}
}
}


Activityの中に画像のダウンロード処理みたいなロジックを書くのはあれですけど、あくまでサンプルです。

どうでしょうか? onDestroy()が呼ばれた際にコルーチンをキャンセルするということを意識せずに簡潔に書けるようになったと思うのですが。

こんな感じでCoroutineDispatcherを定義してasync()や先程定義したasyncResponse()に渡してやれば非同期処理を実行するスレッドを指定させられます。

bindLaunch()croutineContextはデフォルトでUIを指定しているので何もしなければコルーチンそのものはUIスレッドで起動されます。

そしたら起動したコルーチンの中で、定義したCoroutineDispatcherCoroutineScopecoroutineContextを持たせてasyncAsyncResponseに渡して呼び出してやれば、非同期処理中にonDestroy()が呼ばれた際に適切にコルーチンがキャンセルされます。

coroutineContext + CoroutineDispatcherを忘れてそのままasync()等にCoroutineDispatcherkotlinx-coroutinesに定義されているCommonPool等を渡すとコルーチンのキャンセルに失敗したりした気がするので、その処理をCoroutineScopeの拡張プロパティとして定義しておいてやれば忘れにくくていいのかなって思います。

例えば、画像のダウンロード処理をするためのCoroutineDispatcherとかデータベースに書き込み処理をするためのCoroutineDispatcherみたいなのを、まとめてどこかのファイルに定義しておいて、そこに一緒にCoroutineScopeの拡張プロパティとしてcoroutineContextをもたせたものも定義しておく、みたいな感じにすれば実際にasync/awaitを使う際にやらかさなくて済むかなと。

ちなみにasyncResponse()の引数expectedに期待する例外のリストを渡してやれば、それ以外の例外が発生した際にはFailureに例外を格納して返す、ということをせずにそのまま例外を投げるようにしています。


使用した依存関係

build.gradleのdependenciesの部分を抜粋

ext.kotlin_version = '1.2.50'

ext.kotlin_croutine_ver = '0.23.2'
ext.arch_ver = '1.1.1'

dependencies {
//kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlin_croutine_ver"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_croutine_ver"

//lifecycle
compileOnly "android.arch.lifecycle:common:$arch_ver"
compileOnly "android.arch.lifecycle:runtime:$arch_ver"
testImplementation "android.arch.lifecycle:common:$arch_ver"
testImplementation "android.arch.lifecycle:runtime:$arch_ver"
kapt "android.arch.lifecycle:compiler:$arch_ver"
}


おまけ

ActivityonSaveInstanceState()Fragmentの状態を保存するようにしているので、ActivityonSaveInstanceState()onRestoreInstanceState()の間でFragmentをコミットしたりすると例外が投げられてアプリが落ちます。

これでよくあるのが非同期処理した後にFragmentを生成してActivityに追加したら、非同期処理が終わった時にはonSaveInstanceState()の後だったみたいな事😌

他にもonSaveInstanceState()onRestoreInstanceState()で呼び出されたくない処理ってあると思います。

なのでonResume()からonPause()の間でだけ処理を走らせるためのユーティリティも作ってみました。


OnActiveRunner.kt

import android.arch.lifecycle.*

import java.lang.ref.WeakReference

/**
* A interface that run arbitrary functions when [LifecycleOwner] is Active
*/
interface IOnActiveRunner {
/**
* Set at the beginning of the life cycle
* @param owner owner
* @throws IllegalStateException When already set
*/
fun setOwner(owner: LifecycleOwner)

/**
* @param handle If the [handle] is called after [Lifecycle.Event.ON_PAUSE], the [handle] is called when [Lifecycle.Event.ON_RESUME] is called.
* @throws IllegalStateException When owner is not set
*/
fun runOnActive(handle: () -> Unit)
}

class OnActiveRunner : IOnActiveRunner {

private var mOwnerRef: WeakReference<LifecycleOwner>? = null
private val mOwner by lazy { requireNotNull(mOwnerRef?.get()) }
private val mObserver by lazy { createOnActiveLifeCycleObserver(mOwner) }
private var mIsOwnerInitialized = false

override fun setOwner(owner: LifecycleOwner) {
if (mIsOwnerInitialized) throw IllegalStateException("owner already set")
mOwnerRef = WeakReference(owner)
owner.lifecycle.addObserver(mObserver)
mIsOwnerInitialized = true
}

override fun runOnActive(handle: () -> Unit) {
if (!mIsOwnerInitialized) throw IllegalStateException("owner is not set")
mObserver.run(handle)
}

private fun createOnActiveLifeCycleObserver(owner: LifecycleOwner) = object : LifecycleObserver {
private val mOwnerRef = WeakReference(owner)
private var isSafe = false
private val tasks = ArrayList<() -> Unit>()

fun run(task: () -> Unit) {
if (isSafe) {
task()
} else {
tasks.add(task)
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
isSafe = false
}

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
tasks.forEach { it() }
tasks.clear()
isSafe = true
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
isSafe = false
val owner1 = mOwnerRef.get() ?: return
owner1.lifecycle.removeObserver(this)
}
}
}


使う際はこんな感じ


SampleActivity.kt

class SampleActivity : AppCompatActivity(), IOnActiveRunner by OnActiveRunner(){

//onCreateやらなんやら
....

//何かしらの非同期処理後にFragmentを初期化したりするメソッド
private fun initializeFragment() = bindLaunch {
val response = downloadHoge().await()
val result = response.success()?.result ?: return@bindLaunch
setFragment(result)
}

private fun setFragment(hoge:Hoge) = runOnActive {
//FragmentManagerにFragmentをcommitするとか
}

}


Android Architecture ComponentsのLifecycleを使えばいろんなめんどくさい処理を隠せる気がしてきました。