【Android Studio】Kotlin Coroutinesの使い方!非同期処理とスレッド

【Android Studio】Kotlin Coroutinesの使い方!非同期処理とスレッド

この記事からわかること

  • Android StudioKotlin Coroutines実装方法の使い方
  • 非同期処理を実装する方法
  • コルーチンとは?
  • runBlockinglaunchasyncメソッドの役割
  • suspend関数Dispatcherとは?
  • Unresolved reference: launch原因
  • Suspend function '関数名' should be called only from a coroutine or another suspend functionの原因

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

公式リファレンス:Kotlin coroutines on Android

環境

Kotlin Coroutinesとは?

Kotlin CoroutinesとはAndroidアプリ開発で非同期処理を実装できる公式ライブラリです。また「コルーチン」という言葉自体はAndroidで使用できる並行実行のデザインパターンのことを指しており、日本語に訳すと「特定の機能を担うルーチン」という的な意味になると思います。

Kotlin Coroutinesを使用することでこれまでコールバックなどで実行していたネットワーク処理やデータベースへの書き込み処理(I/O)、また処理に時間のかかるものなどを非同期的に(通常の流れとは異なる流れで)実行することでメインスレッドを活かし、アプリの対話性と生産性の向上を期待することができます。

処理のタイミングが異なる非同期処理を通常の同期処理の間に記述できることで見通しが良く、保守しやすいコードを実装することができます。

用語集

Kotlin Coroutinesを触るにあたって登場する言葉やクラスなどを自分なりにまとめてみました。

認識がずれていたら申し訳ないですが大体こんなイメージだと思います。

コルーチンとは?

Kotlin Coroutinesでは「コルーチン」という単位で非同期処理が管理されています。非同期処理を実装する場合バックグラウンドスレッドで実行するためコードの実行にラグが生じコールバックなどを使用する必要がありました。しかしKotlin Coroutinesを使用することで非同期処理をより簡潔に記述することが可能になります。また非同期のタスクを一時停止したり再開したりすることができるため、待ち時間のラグやコールバックネストの問題を解決できるようになるようです。

導入方法

Kotlin Coroutinesを使用できるようにするには以下のコードを「build.gradle(Module)」ファイルに記述し「Sync Now」を押します。私はAndroid Studio Flamingo(最新版)を使用していたところ依存関係を追記しなくても使用することができました。Android Studioのバージョンによっては依存関係の追加が必要ないかもしれません。

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Kotlin Coroutinesを使用した非同期処理

テスト実行環境で試しています。

Kotlin Coroutinesを使用した非同期処理のコードサンプルを見てみます。

import kotlinx.coroutines.*

fun main() = runBlocking { 
    launch { 
        delay(1000L) // 1秒遅らせる
        print("World!")
    }
    print("Hello") 
} 
// Hello
// World!

記述されている順番とは逆に出力されているのがわかります。このコードではrunBlockinglaunchがKotlin Coroutinesのコードになります。そしてasyncを含めた3つがよく使うコルーチンを作成するCoroutineBuilderです。

  1. runBlocking
  2. launch
  3. async

runBlocking

公式リファレンス:runBlocking

expect fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext, 
    block: suspend CoroutineScope.() -> T
): T

runBlocking現在実行されているスレッドをブロックして処理を実行するコルーチンを作成するCoroutineBuilderです。スレッドをブロックして非同期処理を実行するため、アプリ内で使用するのであればメイン(UI)スレッドでの使用は避ける必要があります。実行してみると中で実行されるのは非同期処理のはずですがコードの流れ通りの処理順になっているのがわかります。

fun main()  {
    print("1") 
    runBlocking { 
      print("2") 
    }
    print("3") 
} // 123

launch

公式リファレンス:launch

fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext, 
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> Unit
): Job

launchメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。コルーチンへの参照をJobインスタンスとして返します。そのJobインスタンスには値を含めることができないのでコルーチン内での計算結果などを返すことはできません。またlaunchはCoroutineScopeでのみ宣言されているためrunBlockingなどのコルーチンの中か明示的にスコープを指定しないと呼び出すことはできません

fun main() = runBlocking { 
    launch { 
        delay(1000L) // 1秒遅らせる
        print("World!")
    }
    print("Hello") 
}

明示的にスコープを指定した場合

fun main()  {
    print("1") 
    GlobalScope.launch { 
      print("2") 
    }
    print("3") 
} // 213

そのためスコープのない場所で呼び出そうとするとUnresolved reference: launchというエラーになります。

CoroutineScopeの種類

CoroutineScopeには使用する場所や状況に応じて種類が分けられています。GlobalScopeはアプリが終了するまで存在し続けるので使用はあまり推奨されていません。

  1. GlobalScope:アプリケーション全体のライフサイクルに関連付けられたスコープ。
  2. viewModelScope:ViewModel内で使えるスコープ。ViewModelが破棄されるとコルーチンも自動でキャンセル
  3. lifecycleScope:ActivityやFragment内で使えるスコープ。特定のライフサイクル状態に合わせたコルーチンを実行

async

公式リファレンス:async

fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext, 
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> T
): Deferred<T>

asynclaunch同様にメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。launchと異なるのは戻り値を指定できることです。戻ってくる型はJobの1種であるDeferred<T>型です。またasynclaunch同様にCoroutineScopeでのみ宣言されているためrunBlockingなどのコルーチンの中か明示的にスコープを指定しないと呼び出すことはできません

まずはサンプルコードをみてみます。これは非同期の取得処理を並列につなげて実行しています。実行してみるとfetchDataFromRemotefetchDataFromDatabaseは並列に実行され、開始から5秒後には両方の値が出力されます。fetchDataFromRemoteの2秒待機中にもfetchDataFromDatabaseのカウントが進んでいることになります。

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        // Deferredオブジェクトを取得
        val result1 = async { fetchDataFromRemote() }
        val result2 = async { fetchDataFromDatabase() }
        // 中身を取得
        val remoteData = result1.await() 
        val databaseData = result2.await()

        print("$remoteData")
        print("$databaseData")
    }
}

suspend fun fetchDataFromRemote(): String {
    // 本来ならデータを取得する処理
    delay(2000)  // 擬似的にディレイで再現
    return "リモートサーバーからフェッチしたデータ"
}

suspend fun fetchDataFromDatabase(): String {
    // 本来ならデータを取得する処理
    delay(5000)  // 擬似的にディレイで再現
    return "データベースからフェッチしたデータ"
}

asyncメソッド内で実行された関数はString型を返す関数です。ですが戻り型はDeferred<String>なのでawaitメソッドを使用してStringのみを取り出します。awaitメソッドは非同期処理が完了するまで待機し結果を取得するメソッドです。

suspend関数

suspend関数はコルーチン内で実行する非同期処理が終了するまで待機して終了できる関数のことです。suspendキーワードを関数名の前に付与することでsuspend関数になります。またsuspend関数は通常の関数内から呼び出そうとするとSuspend function '関数名' should be called only from a coroutine or another suspend functionというエラーが発生するようになっています。

suspend関数を使用することで非同期処理の完了を待ちつつコールバックなどを使用しない直接的な記法のままコードを記述することができます。

例えばクラウドやローカルからデータを取得するような時間のかかる処理を以下のように返り値として返すことができるようになります。

suspend fun fetchDataFromRemote(): String {
    // 本来ならデータを取得する処理
    delay(2000)  // 擬似的にディレイで再現
    return "リモートサーバーからフェッチしたデータ"
}

suspend fun fetchDataFromDatabase(): String {
    // 本来ならデータを取得する処理
    delay(5000)  // 擬似的にディレイで再現
    return "データベースからフェッチしたデータ"
}

suspend関数の中ならsuspend関数をそのまま呼び出せる

suspend関数の中でなら他のsuspend関数をそのまま呼び出すことができます。内部で呼び出した別のsuspend関数も終了するまで待機します。

suspend fun fetchDataFromDatabase(): String {

    print("開始")
    
    delay(5000)

    print("ディレイ")

    val data = fetchDataFromRemote()

    print("終了")
    return "データベースからフェッチしたデータ"
}

出力されるのは以下の通りになります。

時間      ログ
21:00:00 開始
21:00:05 ディレイ
21:00:07 終了

直列の処理する

並列処理ではなく直列に実行したい場合は以下のようにawaitで待機するタイミングを変更すればOKです。

GlobalScope.launch {
    println("開始")
    // Deferredオブジェクトを取得
    val result1 = async { fetchDataFromRemote() }
    val remoteData = result1.await()
    println("$remoteData")

    val result2 = async { fetchDataFromDatabase() }
    val databaseData = result2.await()
    println("$databaseData")
}

非同期処理をループで作成して直列に実行する

forループなどで処理を回して非同期処理を生成し、それを直列に実行したい場合は以下のように実装することで実現できます。

  1. 対象の非同期処理(printNumber)を定義
  2. 非同期処理を直列で実行するためのメソッド(runSerialTasks)を定義
  3. 直列で実行する処理を順番に保持する配列(tasks)を用意
  4. 処理を配列の中に詰めていく
  5. Taskの中で処理を順番に実行するメソッドを呼び出す
suspend fun printNumber(num: Int): Boolean {
    delay(2000) // 2秒待機
    println("$num")
    return true
}

// 直列に非同期処理を実行する関数
suspend fun runSerialTasks(tasks: List<suspend () -> Boolean>) {
    for (task in tasks) {
        val success = task()
        if (success) {
            println("Task completed successfully")
        } else {
            println("Task failed")
        }
    }
    println("All tasks completed")
}

GlobalScope.launch {

    val nums = (1..10).toList()

    val tasks = mutableListOf<suspend () -> Boolean>()

    for (num in nums) {
        tasks.add { printNumber(num) }
    }

    // 作成したタスクを直列に実行
    runSerialTasks(tasks)
}

CoroutineContext

CoroutineContextコルーチンの実行に必要な情報を保持するオブジェクトです。コルーチンがどのスレッドで実行されるか、エラーハンドリングの方法、タイムアウトの設定など、コルーチンの動作に影響を与えるさまざまな属性をキーと値のペアの集合として保持します。

Dispatcher

Dispatcherコルーチンを実行するスレッドを指定するオブジェクトです。この情報はCoroutineContextに保管されます。スレッドを指定するには引数にDispatchersを渡します。

GlobalScope.launch(Dispatchers.IO) { 
    print("2") 
}
Dispatchers.Main(メインスレッド)やDispatchers.IO(I/O処理に適したスレッド)などが定義されています。

スレッドの種類

withContext

withContextメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。async/awaitと同じような振る舞いをしますが、async/awaitがcoroutineを新たに生成する一方、withContextは実行中のcoroutineのコンテキストを切り替えているだけです。なのでwithContextの方がパフォーマンスがよくメモリ消費も軽減されています。

fun main()  {
    print("1") 
    GlobalScope.launch { 
        withContext(Dispatchers.IO) {
            print("2") 
        }
      print("3") 
    }
    print("4") 
} // 2314

またsuspend関数を使用する場合以下のように記述することもできます。

val data = withContext(Dispatchers.IO) {
  return fetchDataFromRemote()
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index