Android Architecture ComponentsのLifecycleとKotlin-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
sealed class Response<R>
//成功時のResponse
data class Success<R>(val result: R) : Response<R>()
//失敗時のResponse
data class Failure<R>(val error: Throwable) : Response<R>()
Response
をsealed class
にすることで継承を制限し、when
式などで型チェックを行うことで成功時と失敗時のそれぞれの値を取り出すことができるようにしてみました。
与えた型変数はSuccess
でしか使いませんが、ここはResponse
をダウンキャストしたときの利便性重視です。
値を取り出す際は例えば
when (response) {
is Success -> {
//成功時の処理
}
is Failure -> {
//失敗時の処理
}
}
というふうにすればそれぞれの枝でResponse
型のオブジェクトがスマートキャストされて値が取り出せるようになります。
いちいちwhen
やif
等を用いてキャストするのが面倒なのであれば、何かしらの関数を定義してやればいいと思います
//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が発生して死んでしまう。ということを防げるはず。
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()
を定義
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
を返す、と言ったことができるようになります。
例としては
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スレッドで起動されます。
そしたら起動したコルーチンの中で、定義したCoroutineDispatcher
にCoroutineScope
のcoroutineContext
を持たせてasync
やAsyncResponse
に渡して呼び出してやれば、非同期処理中にonDestroy()
が呼ばれた際に適切にコルーチンがキャンセルされます。
coroutineContext + CoroutineDispatcher
を忘れてそのままasync()
等にCoroutineDispatcher
やkotlinx-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"
}
おまけ
Activity
はonSaveInstanceState()
でFragment
の状態を保存するようにしているので、Activity
のonSaveInstanceState()
とonRestoreInstanceState()
の間でFragment
をコミットしたりすると例外が投げられてアプリが落ちます。
これでよくあるのが非同期処理した後にFragment
を生成してActivity
に追加したら、非同期処理が終わった時にはonSaveInstanceState()
の後だったみたいな事😌
他にもonSaveInstanceState()
とonRestoreInstanceState()
で呼び出されたくない処理ってあると思います。
なのでonResume()
からonPause()
の間でだけ処理を走らせるためのユーティリティも作ってみました。
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)
}
}
}
使う際はこんな感じ
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
を使えばいろんなめんどくさい処理を隠せる気がしてきました。