Coroutine:launchビルダーでコルーチン開始

投稿日:  更新日:

コルーチン(Coroutine)は「非同期処理の手法」の1つです。

Kotlinが提供します。

コルーチンはビルダー(Builder)により開始されます。

ビルダーは3つの種類があり、その中の1つがlaunchです。

このlaunchビルダーについて、まとめます。

スポンサーリンク

ビルダーとは

ビルダー(Builder)は、CoroutineScopeを起点にスレッドを起動して、引数で指定されたタスクブロックを実行します。

つまり、ビルダーの役割はコルーチンを開始することです。

        var scope: CoroutineScope? = null
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            scope = SampleScope()
            scope?.launch() {	                 // launchビルダー
                /* --- ここから ---              */
				/* 重い処理(長時間の処理)      */
				/* launch { ... }                */
				/*        ^^^^^^^ タスクブロック */
                println("Thread = ${Thread.currentThread().name}")
				/* --- ここまで ---              */
            }
        }
        findViewById<Button>(R.id.btnCancel).setOnClickListener {
            scope?.cancel()
        }
class SampleScope : CoroutineScope {
    override val coroutineContext: CoroutineContext =
        Job() + Dispatchers.Default + CoroutineName("Hoge") // Contextを定義
}
スポンサーリンク

launchの特徴

ビルダーは3つの種類があります。

その中のlaunchはタスクブロックの戻り値を返さないビルダーです。

ビルダー概要関数の戻り値
launchスレッドを起動してタスクブロックを実行
タスクブロックの戻り値なし
スレッドをブロックしない(一時停止)
起動したスレッドのJobインスタンス
asyncスレッドを起動してタスクブロックを実行
タスクブロックの戻り値あり
スレッドをブロックしない(一時停止)
起動したスレッドのDeferredインスタンス
※DeferredはJobのサブクラス
runBlockingスレッドを起動してタスクブロックを実行
タスクブロックの戻り値あり
スレッドをブロックする(休止)
タスクブロックの戻り値

また、launchはCoroutineScopeに定義された拡張関数です。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
スポンサーリンク

スレッドの起動(第3引数)

スレッドの起動はlaunchの引数(第3引数)にラムダ式(タスクブロック)を指定して実行するだけです。※引数の最後がラムダ式の場合、「( )」の外に出せる

この場合、起動されるスレッドはCoroutineScopeに実装されたCoroutineContextに従います。

            // CoroutineScopeのCoroutineContextに従い、Defaultスレッドを起動
            scope = SampleScope()
            scope?.launch {                     // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _resultA = taskA()
                println("Time = ${getTime()}   ResultA = ${_resultA}")

                println("Time = ${getTime()} End")
            }
    fun taskA(): String {
        Thread.sleep(1000)     // 重い処理の代わり
        return "AAA"           // 重い処理の戻り値
    }
    fun taskB(): String {
        Thread.sleep(2000)     // 重い処理の代わり
        return "BBB"           // 重い処理の戻り値
    }
    fun taskC(): String {
        Thread.sleep(3000)     // 重い処理の代わり
        return "CCC"           // 重い処理の戻り値
    }
getThread, getTime
    inline private fun getTime(): String {
        return "%04d".format(System.currentTimeMillis() % 10000)
    }
    inline private fun getThread(): String {
        return Thread.currentThread().name
    }

launchでスレッドの起動

Time = 3901 Start Thread = DefaultDispatcher-worker-1
Time = 4910   ResultA = AAA
Time = 4911 End

launchは入れ子が可能です。子コルーチンが開始できます。

            // ビルダーの入れ子
            scope = SampleScope()
            scope?.launch {                     // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                launch {                        // launchビルダー
                    val _resultB = taskB()
                    println("Time = ${getTime()}   ResultB = ${_resultB}")
                    val _resultC = taskC()
                    println("Time = ${getTime()}   ResultC = ${_resultC}")
                }
                val _resultA = taskA()
                println("Time = ${getTime()}   ResultA = ${_resultA}")

                println("Time = ${getTime()} End")
            }

launchの入れ子

Time = 9137 Start Thread = DefaultDispatcher-worker-2
Time = 0142   ResultA = AAA
Time = 0142 End
Time = 1141   ResultB = BBB
Time = 4143   ResultC = CCC
スポンサーリンク

スレッドの切り替え(第1引数)

launchビルダーの第1引数はCoroutineContextです。

ここに新たなCoroutineContextを指定すると、CoroutineScopeに格納されたCoroutineContexへ加算(上書き)されます。つまり、引数で指定されたCoroutineContextが優先されます。

次の例は、Dispatchers.Mainを指定することで、スレッドプールをDefaultからMainに切り替える例です。launchによりMainスレッドが起動されます。

※CoroutineContextの詳細は「Kotlin:コルーチン(Coroutine)」を参照

            // ビルダーの引数でプールを指定、Mainスレッドで起動
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {   // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _resultA = taskA()
                println("Time = ${getTime()}   ResultA = ${_resultA}")

                println("Time = ${getTime()} End")
            }

launchでスレッドの起動

Time = 3092 Start Thread = main
Time = 4095   ResultA = AAA
Time = 4095 End
スポンサーリンク

並列処理(Parallel)

launchは実行される毎にスレッドを起動します。よって、連続して実行すれば、複数のスレッドが同時に起動して並列処理になります。

ただし、並列に動作するスレッド数は、プールが管理できる数に制限されます。
※DefaultブールはCore数まで管理可能

            // 並列処理
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {   // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultA = taskA()
                    println("Time = ${getTime()}   ResultA = ${_resultA}")
                }
                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultB = taskB()
                    println("Time = ${getTime()}   ResultB = ${_resultB}")
                }
                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultC = taskC()
                    println("Time = ${getTime()}   ResultC = ${_resultC}")
                }

                println("Time = ${getTime()} End")
            }

launchによる並列処理

Time = 2808 Start Thread = main
Time = 2810 End
Time = 3811   ResultA = AAA
Time = 4811   ResultB = BBB
Time = 5811   ResultC = CCC
スポンサーリンク

逐次処理(Serial)

Job#join( )はタスクブロックの終了を待ちます。join( )を使えば逐次処理が出来ます。

            // 逐次処理
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {   // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultA = taskA()
                    println("Time = ${getTime()}   ResultA = ${_resultA}")
                }.join()
                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultB = taskB()
                    println("Time = ${getTime()}   ResultB = ${_resultB}")
                }.join()
                launch(Dispatchers.Default) {   // launchビルダー
                    val _resultC = taskC()
                    println("Time = ${getTime()}   ResultC = ${_resultC}")
                }.join()

                println("Time = ${getTime()} End")
            }

launchによる逐次処理

Time = 1713 Start Thread = main
Time = 2716   ResultA = AAA
Time = 4719   ResultB = BBB
Time = 7724   ResultC = CCC
Time = 7725 End

join( )はSuspend関数です。自身の実行後にスレッドを一時停止(Suspend)します。

そして、子コルーチンの処理が完了したところで、一時停止していたスレッドを再取得して、タスクブロックの続きの処理を再開します。

※Suspend関数の詳細は「Coroutine:Suspend関数とその仕組み」を参照

スポンサーリンク

待ち合せ

joinAll( )はタスクブロックの終了を待つ関数です。

引数は可変長引数になっており、複数のJobインスタンスが指定できます。つまり、複数のタスクの終了を待ちます。

joinAll( )を使えば、「タスクAとBの終了を待ってタスクCの処理を行う」と言った、待ち合せを表現できます。

            // タスク終了の待ち合せ( 1 )
            scope = SampleScope()
            scope?.launch(Dispatchers.Main) {   // launchビルダー
                println("Time = ${getTime()} Start Thread = ${getThread()}")

                val _jobA = launch(Dispatchers.Default) {   // launchビルダー
                    val _resultA = taskA()
                    println("Time = ${getTime()}   ResultA = ${_resultA}")
                }
                val _jobB = launch(Dispatchers.Default) {   // launchビルダー
                    val _resultB = taskB()
                    println("Time = ${getTime()}   ResultB = ${_resultB}")
                }
                joinAll(_jobA, _jobB)
                launch(Dispatchers.Default) {
                    val _resultC = taskC()
                    println("Time = ${getTime()}   ResultC = ${_resultC}")
                }

                println("Time = ${getTime()} End")
            }

タスク終了の待ち合せ

Time = 1171 Start Thread = main
Time = 2184   ResultA = AAA
Time = 3182   ResultB = BBB
Time = 3183 End
Time = 6184   ResultC = CCC

joinAll( )はSuspend関数です。自身の実行後にスレッドを一時停止(Suspend)します。

中身は単純で、引数のJobインスタンスに対して、Job#join( )を一つ一つ実行しているだけです。

public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }

※Suspend関数の詳細は「Coroutine:Suspend関数とその仕組み」を参照

スポンサーリンク

スタートオプション(第2引数)

launchビルダーの第2引数はスタートオプションです。

コルーチンを開始する時の動作が指定できます。

スタートオプション
CoroutineStart.XXX
コルーチン
開始のタイミング
コルーチン
キャンセルのタイミング
DEFAULT
(引数なし)
ビルダーの実行後、直ちに開始する開始時にキャンセル可能
起動中にキャンセル可能
LAZYビルダーの実行後、開始しない
Job#start( )・Deferred#await( )で開始
DEFAULTと同じ
ATOMICDEFAULTと同じ開始時にキャンセル不可
起動中にキャンセル可能
UNDISPATCHEDDEFAULTと同じDEFAULTと同じ
起動されるスレッドは
Dispatchers.Unconfinedを指定して開始されるコルーチンと同じ

引数がなければデフォルトのCroutineStart.DEFAULTが指定された事になります(拡張関数の定義より)。

CoroutineStart.LAZY

オプションLAZYはlaunchビルダーの実行後にコルーチンを開始しない動作です。

コルーチンの開始はJob#start( )関数によって行われます。

        var _scope: CoroutineScope? = null
        var _job: Job? = null
        findViewById<Button>(R.id.btnReady).setOnClickListener {  // Readyボタン
            println("Time = ${getTime()} Ready coroutine !!")
            _scope = SampleScope()
            _scope?.launch(Dispatchers.Main) {
                _job = launch(Dispatchers.Default, CoroutineStart.LAZY) {
                    println("Time = ${getTime()} Start TaskC Thread = ${getThread()}")
                    val _resultC = taskC()
                    println("Time = ${getTime()} End   TaskC Result = ${_resultC}")
                }
            }
        }
        findViewById<Button>(R.id.btnStart).setOnClickListener {  // Startボタン
            println("Time = ${getTime()} Start coroutine !!")
            _job?.start()
        }
Time = 0827 Ready coroutine !!         ... Readyボタン押下
     :
〔時間経過〕
     :
Time = 4698 Start coroutine !!         ... Startボタン押下
Time = 4710 Start TaskC Thread = DefaultDispatcher-worker-1 
                                       ... Job#Start( )で開始
Time = 7712 End   TaskC Result = CCC

LAZYに対しDEFAULTはlaunchビルダーの実行後にコルーチンを開始する動作です。

        var _scope: CoroutineScope? = null
        var _job: Job? = null
        findViewById<Button>(R.id.btnReady).setOnClickListener {
            println("Time = ${getTime()} Ready coroutine !!")
            _scope = SampleScope()
            _scope?.launch(Dispatchers.Main) {
                _job = launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
                    println("Time = ${getTime()} Start TaskC Thread = ${getThread()}")
                    val _resultC = taskC()
                    println("Time = ${getTime()} End   TaskC Result = ${_resultC}")
                }
            }
        }
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            println("Time = ${getTime()} Start coroutine !!")
            _job?.start()
        }
Time = 2276 Ready coroutine !!         ... Readyボタン押下
Time = 2279 Start TaskC Thread = DefaultDispatcher-worker-1
                                       ... 直ちに開始
Time = 5282 End   TaskC Result = CCC

CoroutineStart.ATOMIC

オプションATOMICはコルーチンの開始時にキャンセルしない動作です。

ただし、起動中のコルーチンはキャンセルできます。

以下のサンプルはDefaultプールを使って、10個のコルーチンの開始を試みます。しかし、プールで管理可能なスレッドは4つのため、直ちに開始されるコルーチンはTaskC_0,1,2,3になり、残りはスレッドが空くまで待機します。

この待機中にキャンセルの発行を行ったとしても、ATOMICの場合は待機中のコルーチンをキャンセルしません。スレッドに空きが出ればコルーチンを開始します。

        var _scope: CoroutineScope? = null
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            _scope = SampleScope()
            _scope?.launch(Dispatchers.Main) {
                for(i in 0..9) {
                    println("TaskC_${i} Start")
                    launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
                        if(isActive) {
                            val _resultC = taskC()
                            println("TaskC_${i} = ${_resultC}")
                        }
                        else
                            println("TaskC_${i} Cancel!")
                    }
                }
            }
        }
		findViewById<Button>(R.id.btnCancel).setOnClickListener {
            _scope?.cancel()
        }
Time = 9730 TaskC_0 Start  ... コルーチンの開始を試みる
Time = 9737 TaskC_1 Start
Time = 9745 TaskC_2 Start
Time = 9747 TaskC_3 Start
Time = 9750 TaskC_4 Start
Time = 9751 TaskC_5 Start
Time = 9751 TaskC_6 Start
Time = 9751 TaskC_7 Start
Time = 9752 TaskC_8 Start
Time = 9752 TaskC_9 Start
     :
〔キャンセル発行〕
     :
Time = 2746 TaskC_0 = CCC   ... コルーチン開始、taskC( )が起動済み
Time = 2747 TaskC_4 Cancel! ... コルーチン開始、キャンセル状態⇒強制終了
Time = 2747 TaskC_5 Cancel!
Time = 2747 TaskC_6 Cancel!
Time = 2748 TaskC_7 Cancel!
Time = 2748 TaskC_8 Cancel!
Time = 2748 TaskC_1 = CCC
Time = 2749 TaskC_9 Cancel!
Time = 2762 TaskC_2 = CCC
Time = 2763 TaskC_3 = CCC

ATOMICに対しDEFAULTは待機中のコルーチンのキャンセルが行われます。

        var _scope: CoroutineScope? = null
        findViewById<Button>(R.id.btnStart).setOnClickListener {
            _scope = SampleScope()
            _scope?.launch(Dispatchers.Main) {
                for(i in 0..9) {
                    println("TaskC_${i} Start")
                    launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
                        if(isActive) {
                            val _resultC = taskC()
                            println("TaskC_${i} = ${_resultC}")
                        }
                        else
                            println("TaskC_${i} Cancel!")
                    }
                }
            }
        }
        findViewById<Button>(R.id.btnCancel).setOnClickListener {
            _scope?.cancel()
        }
Time = 5509 TaskC_0 Start
Time = 5519 TaskC_1 Start
Time = 5523 TaskC_2 Start
Time = 5524 TaskC_3 Start
Time = 5525 TaskC_4 Start
Time = 5526 TaskC_5 Start
Time = 5526 TaskC_6 Start
Time = 5526 TaskC_7 Start
Time = 5527 TaskC_8 Start
Time = 5527 TaskC_9 Start
     :
〔キャンセル発行〕
     :
Time = 8522 TaskC_0 = CCC   ... コルーチン開始、taskC( )が起動済み
Time = 8524 TaskC_1 = CCC
Time = 8526 TaskC_2 = CCC
Time = 8526 TaskC_3 = CCC
                            ... 残りはコルーチン開始時にキャンセル
スポンサーリンク

関連記事:

近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 特徴としてnon-blocking動作をサポートします。 このnon-blocking動作についてまとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンの構成要素であるSuspend関数について、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するlaunchビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがasyncです。 このasyncビルダーについて、まとめます。 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンを開始するasyncビルダーの仕組みについて、まとめます。 ※仕組みの解析は次のバージョンを対象に行っています。    Kotlin:Ver 1.6.10    org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 ...
コルーチン(Coroutine)は「非同期処理の手法」の1つです。 Kotlinが提供します。 コルーチンはビルダー(Builder)により開始されます。 ビルダーは3つの種類があり、その中の1つがrunBlockingです。 このrunBlockingビルダーについて、まとめます。 ...
CoroutineContextはコルーチンで起動されるスレッドの属性を格納しています。 その中にコルーチンの名前を表現するName属性があります。 Name属性を出力する方法を紹介します。 ...
コルーチン(Coroutine)は「非同期処理プログラミングの手法」の1つです。 Kotlinが提供します。 withContextはCoroutineContextを切り替えてスレッドを起動するSuspend関数です。 このwithContextについて、まとめます。 ...
コルーチン間でメッセージ(データ)の送受信を行うことが出来ます。 ここで紹介する「メッセージの送受信」を使えば、非同期処理の間で確実にデータを受け渡し出来ます。 それにより、非同期処理の連携が容易になります。 今回は、メッセージの送受信についての基礎と、Channelを使った最も基本的な送受信の動作をまとめます。 ※環境:Android Studio Flamingo | 2022.2.1    :org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 ...
KotlinのコルーチンAPIは「コルーチン間でメッセージを送受信する仕組み」を提供しています。 Channel、Produce、Flow、SharedFlow、StateFlowなどです。 これらは、「メッセージを送受信する」という本命の動作は変わりませんが、特徴や違いを持ちます。 プログラミングで利用する際は、特徴や違いを理解して、使い分けが必要になります。 ですので、各々を比較しつつ、まとめました。 この記事は「Produce」について、まとめたものです。 ※環境:Android Studio Koala Feature Drop | 2024.1.2     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3     org.jetbrains.kotlinx:kotlinx-coroutines-core 1.7.3 ...
KotlinのコルーチンAPIは「コルーチン間でメッセージを送受信する仕組み」を提供しています。 Channel、Produce、Flow、SharedFlow、StateFlowなどです。 これらは、「メッセージを送受信する」という本命の動作は変わりませんが、特徴や違いを持ちます。 プログラミングで利用する際は、特徴や違いを理解して、使い分けが必要になります。 ですので、各々を比較しつつ、まとめました。 この記事は「Flow」について、まとめたものです。 ※環境:Android Studio Koala Feature Drop | 2024.1.2     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3     org.jetbrains.kotlinx:kotlinx-coroutines-core 1.7.3 ...
KotlinのコルーチンAPIは「コルーチン間でメッセージを送受信する仕組み」を提供しています。 Channel、Produce、Flow、SharedFlow、StateFlowなどです。 これらは、「メッセージを送受信する」という本命の動作は変わりませんが、特徴や違いを持ちます。 プログラミングで利用する際は、特徴や違いを理解して、使い分けが必要になります。 ですので、各々を比較しつつ、まとめました。 この記事は「SharedFlow」について、まとめたものです。 ※環境:Android Studio Koala Feature Drop | 2024.1.2     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3     org.jetbrains.kotlinx:kotlinx-coroutines-core 1.7.3 ...
KotlinのコルーチンAPIは「コルーチン間でメッセージを送受信する仕組み」を提供しています。 Channel、Produce、Flow、SharedFlow、StateFlowなどです。 これらは、「メッセージを送受信する」という本命の動作は変わりませんが、特徴や違いを持ちます。 プログラミングで利用する際は、特徴や違いを理解して、使い分けが必要になります。 ですので、各々を比較しつつ、まとめました。 この記事は「StateFlow」について、まとめたものです。 ※環境:Android Studio Koala Feature Drop | 2024.1.2     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3     org.jetbrains.kotlinx:kotlinx-coroutines-core 1.7.3 ...
Flowはメンバー関数や拡張関数で様々な機能を提供しています。 これらの関数は大きく分けて、中間演算(Intermediate operators)と終端演算(Terminal operators)に分けられます。 中間演算とは、withIndex、map、filter、drop、take、zip、merge、combineなどです。通信経路(Flow)の途中に位置して、ストリームデータを変更したり、Flowを統合したりします。 終端演算とは、collect、single、reduce、toListなどです。通信経路の末端に位置して、ストリームデータを収集します。 今回は、この「Flowの中間演算」のwithIndex、map、filter、drop、takeを取り上げて、まとめます。 ※環境:Android Studio Koala Feature Drop | 2024.1.2 Patch 1     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-androi ...
Flowはメンバー関数や拡張関数で様々な機能を提供しています。 これらの関数は大きく分けて、中間演算(Intermediate operators)と終端演算(Terminal operators)に分けられます。 中間演算とは、withIndex、map、filter、drop、take、zip、merge、combineなどです。通信経路(Flow)の途中に位置して、ストリームデータを変更したり、Flowを統合したりします。 終端演算とは、collect、single、reduce、toListなどです。通信経路の末端に位置して、ストリームデータを収集します。 今回は、この「Flowの中間演算」のzip、merge、combineを取り上げて、まとめます。 ※環境:Android Studio Koala Feature Drop | 2024.1.2 Patch 1     Kotlin 1.9.0     Compose Compiler 1.5.1     org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3     o ...
スポンサーリンク