Android - Kotlin Coroutineの使い方

AndroidでCoroutineを使用する方法と例を紹介します。

1. Coroutine

Coroutineは軽いスレッドと言えます。スレッドではありませんが、非同期(asynchronous)プログラミングが可能になります。

Coroutines は Co+ Routines の略で、

  • CoはCooperationを意味します
  • Routinesはfunctionsを意味します

互いに協力する関数という意味ですが、これは単にコルーチンと呼ばれます。 Coroutineは複数の関数が交互に実行され、非同期プログラミングが可能であることを理解してください。

KotllineのCoroutineはFrameworkで実装されており、どのように動作するのかを確認していませんが、次の記事を読んでいくらでも理解できます。

この記事では、原論的な説明よりも、初めてコルーチンに触れる人がすぐにコルーチンを利用できるようにガイドしようとしています。

2. プロジェクトでライブラリを設定する

Androidでコルーチンを使用するには、gradleに次のライブラリを追加する必要があります。

dependencies {
  ...
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
}

3. 軽いスレッド

コルーチンはスレッドではありませんが、スレッドのように非同期プログラミングが可能です。 コードだけを見れば、同期的に(synchronous)動作するようですが非同期的に動作します。 まず、コードと実行結果を見ると、どのように動作するかを理解できます。

次のコードは、メインスレッドでコルーチンを実行する例です。

Log.d(TAG, "doing something in main thread")    // 1

GlobalScope.launch {    // 2
    delay(3000)
    Log.d(TAG, "done something in Coroutine")   // 3
}

Log.d(TAG, "done in main thread")     // 4

「// 1」のように数字で表記したものを以下に詳しく説明しました。

  1. メインスレッドからログを出力します。
  2. launch { ... }は、コルーチンで操作を実行する命令です。括弧内のコードは非同期的に実行されます。
  3. コルーチンで3秒休んでログを出力します。
  4. コルーチンを実行し、メインスレッドからログを再度出力します。

結果を見ると、メインスレッドのログがすべて出力され、コルーチンはdelayのために後でログが後で出力されました。

10-26 19:52:42.975  7699  7699 D MainActivity: doing something in main thread
10-26 19:52:42.985  7699  7699 D MainActivity: done in main thread
10-26 19:52:45.992  7699  7727 D MainActivity: done something in Coroutine

コードを見ると delay() が最初に実行され、メインスレッドのログが後で出力されるようでしたが、コルーチンは非同期的に実行されるため、メインスレッドコードが最初に呼び出されました。

GlobalScopeは、コルーチンが実行される期間を意味します。スコープについては後述する。

4. launch、asyncでコルーチンを実行する

launchasyncはコルーチンを実行する命令という共通点がありますが、次のような違いがあります。

  • launchには戻り値はありません
  • asyncはDeferred<T>オブジェクトを返します

Deferred<T> クラスは await() メソッドを提供します。 このメソッドは、ジョブが完了するのを待ち、T型のオブジェクトを返します。

// Deferred.kt
public interface Deferred<out T> : Job {
  public suspend fun await(): T
  ...
}

次の例は、launchと戻り値を持つasyncの使用方法を示しています。

GlobalScope.launch {    // 1
    launch {    // 2
        Log.d(TAG, "Launch has NO return value")
    }

    val value: Int = async {   // 3
        1 + 2  // 4
    }.await()   // 5

    Log.d(TAG, "Async has return value: $value")
}
  1. 「GlobalScope.launch」はコルーチンを実行します。
  2. GlobalScope.launch{ ... }内でlaunch { ... }で他のコルーチンを実行できます。
  3. async{...}でコルーチンを実行します。
  4. "1 + 2"演算後に3を返します。
  5. await() は、async のコルーチンが終了するのを待って結果を返します。

結果は次のとおりです。

10-26 20:52:13.597  8158  8187 D MainActivity: Launch has NO return value
10-26 20:52:13.599  8158  8186 D MainActivity: Async has return value: 3

5. Suspend functions

コルーチン内では、一般的なメソッドを呼び出すことはできません。コルーチンのコードは、しばらく実行を停止したり(suspend)再実行することができるためです。 コルーチンで実行できるメソッドを作成するには、関数を定義するときに suspend を付けてください。 suspend 関数は内部で他のコルーチンを実行することもできます。

以下は suspend メソッドの例です。

GlobalScope.launch {
    doSomething()
    Log.d(TAG, "done something")
}

private suspend fun doSomething() {
    GlobalScope.launch {
        sleep(1000)
        Log.d(TAG, "do something in a suspend method")
    }
}

結果は次のとおりです。

10-26 21:05:18.990  8418  8446 D MainActivity: done something
10-26 21:05:19.990  8418  8447 D MainActivity: do something in a suspend method

6. コルーチンが動作するスレッド

コルーチンは複数の関数を交互に動作します。コルーチンが実行されるスレッドを指定できます。 スレッドを指定しなければならない理由は、タスクが種類に応じて迅速に処理されなければならないことがあり、AndroidのUIタスクはMainスレッドでのみ行わなければならないため、この場合Mainスレッドでコルーチンが実行されるようにする必要があります。

以下は、コルーチンがIOスレッドで実行されるようにするコードです。 launch() に引数としてスレッドタイプを渡すと、コルーチンはそのスレッドで実行されます。

GlobalScope.launch(Dispatchers.IO) {
    doOnIOthread() // do on IO thread
}

Androidは3つのDispatchersを提供しています。

  • Dispatchers.Main:Androidのメインスレッドです。 UI操作はここで処理する必要があります
  • Dispatchers.IO:Diskまたはネットワークからデータを読み取るI / O操作は、このスレッドで処理する必要があります。例えば、ファイルを読んだり、AACのRoomなどもここに該当します
  • Dispatchers.Default:他のCPUによって処理されるほとんどのタスクはこのスレッドで処理する必要があります

メインスレッドでコルーチンを実行しますが、いくつかのコルーチンは他のスレッドで実行することもできます。

GlobalScope.launch(Dispatchers.Main) {    // 1
    val userOne = async(Dispatchers.IO) {     // 2
      fetchFirstUser()
    }
    val userTwo = async(Dispatchers.Default) {     // 3
      fetchSeconeUser()
    }
    showUsers(userOne.await(), userTwo.await())    // 4
}
  1. コルーチンをメインスレッドで実行します。
  2. これは IO スレッドで行います。
  3. これは Default スレッドで行います。
  4. このコードは Main スレッドで実行され、2 と 3 のジョブの両方が終了するのを待って結果を出力します。

withContext() というメソッドもあります。これは async と同じ役割を果たすキーワードです。違いは await() を呼び出す必要がないことです。結果が返されるまで待ちます。

suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T (source)

以下はwithContextを使用した例です。

GlobalScope.launch(Dispatchers.IO) {
    Log.d(TAG, "Do something on IO thread")   //  1
    val name = withContext(Dispatchers.Main) {    // 2
        sleep(2000)
        "My name is Android"
    }   
    // 3
    Log.d(TAG, "Result : $name")    // 4
}
  1. このコードは IO スレッドで実行されます。
  2. このコードはメインスレッドで実行されます。
  3. withContext() 次のコードは実行しません。 await() を呼び出したかのように結果が返されるのを待ちます。
  4. withContext() のコルーチンがすべて実行されると、このコードが実行されます。

結果は次のとおりです。

10-26 22:44:16.488  9723  9752 D MainActivity: Do something on IO thread
10-26 22:44:18.649  9723  9752 D MainActivity: Result : My name is Android

7. コルーチンのスコープ

Scope は、コルーチンが実行される範囲です。これまでGlobalScopeを使用していましたが、このスコープはアプリケーションが終了するまでコルーチンを実行できます。 ActivityがコルーチンをGlobalScope領域で実行した場合、Activityが終了してもコルーチンはジョブが終了するまで機能します。

Activityで表示する画像をダウンロードしていますが、Activityが終了した場合は、不要なリソースを無駄にしています。 Activityが終了したときに実行中のコルーチンも一緒に終了したい場合は、ActivityのLifecycleと一致するScopeにコルーチンを実行します。

次の例は、コルーチンをActivity scopeに実行させるコードです。実際、Activity scopeはないため、ActivityのLifecycleと一致するScopeを作成する必要があります。

class MainActivity : AppCompatActivity(), CoroutineScope {    // 1
    private lateinit var job: Job     // 2
    override val coroutineContext: CoroutineContext   // 3
        get() = Dispatchers.Main + job
}
  1. CoroutineScope インターフェイスを実装します。
  2. Job オブジェクトを宣言します。
  3. CoroutineScope インターフェイスの CoroutineContext 変数をオーバーライドします。 「Dispatchers.Main」に上記で作成したジョブを追加します。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    job = Job()   // 1
    ....
}

override fun onDestroy() {
    super.onDestroy()
    job.cancel()    // 2
}
  1. Job オブジェクトを作成します。

2.アクティビティが終了したときに実行中のジョブがある場合はキャンセルします。

この Activity は CoroutineScope を実装しているため、コルーチンを実行するときは、次のようにキーワード launch {} を入力するだけです。

launch {
    Log.d(TAG, "do something")
    ....
}

上記のように、クラスがCoroutineScopeを実装せずに、クラス内にオブジェクトを作成してScopeを作成できます。

以下は、ViewModelと同じLifecycleを持つScopeを生成する例です。 ViewModel オブジェクトは、Activity が完全に消滅すると一緒に消滅します。

class MyViewModel : ViewModel() {
    private val job = Job()     // 1
    private val uiScope = CoroutineScope(Dispatchers.Main + job)    // 2

    fun doSomeOperation() {
        uiScope.launch {    // 3
            val deferred = async(Dispatchers.Default) {
                10 + 10
            }
            Log.d(TAG, "await: ${deferred.await()}")
        }
    }

    override fun onCleared() {    // 4
        super.onCleared()
        job.cancel()     // 5
    }
}
  1. Jobを作成します。
  2. CoroutineScope オブジェクトを作成します。
  3. コルーチンを実行するときは、「uiScope.launch{..}」のように、上記で作成したuiScopeを使用します。
  4. ViewModelオブジェクトが破棄されると、onCleared()メソッドが呼び出されます。
  5. 実行中のコルーチンがある場合は、ジョブをキャンセルします。

8. Exception handling

もし以下のコードのように、コルーチン内でExceptionが発生した場合はどうなりますか?

GlobalScope.launch(Dispatchers.IO) {
    launch {
        throw Exception()
    }
}

アプリが突然死ぬでしょう...上記のコードは次のように例外を処理できます。

GlobalScope.launch(Dispatchers.IO + handler) {    // 1
    launch {
        throw Exception()   // 2
    }
}

val handler = CoroutineExceptionHandler { coroutineScope, exception ->   // 3
    Log.d(TAG, "$exception handled!")
}
  1. launch() の引数にプラス演算子として "handler" を追加します。例外が発生すると、このハンドラーにコールバックが送出され、例外処理を実行できます。
  2. 例外を発生させます。
  3. ここで例外を処理します。

上記のコードを実行すると、プログラムは死ぬことなく、以下のログのみを出力します。

10-26 22:54:16.756 10279 10306 D MainActivity: java.lang.Exception handled!

asyncwithContextを使用している場合は、上記の方法で例外を処理しないでください。以下のようにtry-catch構文で例外処理を行う必要があります。

GlobalScope.launch(Dispatchers.IO) {
    try {
        val name = withContext(Dispatchers.Main) {
            throw Exception()
        }
    } catch (e: java.lang.Exception) {
        Log.d(TAG, "$e handled!")
    }
}

9. Proguard

Proguard(難読化)を使用する場合は、以下のルールをProguardファイルに追加する必要があります。

# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}

10. まとめ

コルーチンの使い方を中心に調べました。コトリンが何で、正確にどのように動作するのか疑問に思うなら、私が参考にした記事をもっと読んでみると役に立ちます。もっと詳しく知りたいのなら、コードを分析してみてください。

他の方々のブログを見ると、コルーチンを使用しながらパフォーマンスなどの問題が発生して使用してはいけないようだという文が見られました。 コルーチンが出たばかりなので、予期しないバグに出会うことができます。ビジネスコードを書いている方は、この点を念頭に置いて使用すると良いと思います。

11. 参考

codechachaCopyright ©2019 codechacha