ゆめみ社のAndroidインターンに参加してきたので学んだことをまとめておく(非同期処理編)
ゆめみ社の Android インターンに参加しました。
なかなかにありがたい経験をさせてもらえたので、アウトプットします 👍
- 基本的には課題を進めていく 🚶
- 課題は AndroidView で開発する前提になっていたが、Jetpack Compose の方が得意という旨を伝えると、Compose を使って UI を作っていくことに。柔軟に対応していただくことができた。感謝 😊
非同期処理 (Coroutine)
参考:
良さげな記事 1
JetBranin のドキュメント
良さげな記事 2
Coroutine とその実装に迫ったスライド
公式ドキュメント
個人的に、非同期処理は参加したインターンで一番勉強したかったところです。
(「suspend
とかCoroutine
とかScope
とかなんやねん」って感じだったので)
Kotlin の Coroutine で抑えたいのは Coroutine とsuspend
キーワードでしょう。
Coroutine を理解する
大事なこと 1: Coroutine を使うと処理を中断できる
Coroutine を理解するためにまずこれを頭に叩き入れます。Coroutine を使うと処理が中断できるようになります。
// playMusic()がはじまった後にそれを中断することはできない
playMusic()
launch{
playMusic()
}
// ここでとある書き方をするとplayMusic()を中断できる!
大事なこと 2: Coroutine とは
Coroutine とはとあるブロックが紐づいたインスタンス(ドキュメントでは計算インスタンス
と呼ばれていました)です。例えば、以下のコードを見てみます。
この main 関数にはコメントで開始と終了が示された 1 つブロックがあります。ブロックとは{ ... }
のこと(赤色部分)でこの中には処理がかけます。つまりブロックとは処理のまとまりを表します。
このブロック一つ一つはオブジェクトではありません(よってインスタンスでもありません)。ですがこのブロックをオブジェクトとして扱いたいのです。
なぜブロックをオブジェクトとして扱いたいのでしょうか?答えはブロックをオブジェクトとして扱うことでとあるブロック.中断する()
といったコードが書けるようになるためです。前項で Coroutine では処理を中断できると言いましたが、これができるのはブロックをオブジェクトとして扱うことができるからです。
Coroutine でブロックをオブジェクトとして扱う
実際にブロックをオブジェクトとして扱うためにはどうするのでしょうか?例を示します。
val job = launch {
playMusic()
}
以上です。launch 関数を使ってブロック(ラムダ式)をオブジェクトとして生成し、job
という変数に代入しています。Coroutine ではブロックをJob
クラスというクラスとして扱います。この job クラスにcancel
メソッドという中断できるメソッドが含まれているので、job.cancel()
とすることでブロック(処理)を中断することができるのです。
val job = launch {
playMusic()
playGame()
}
job.cancel() //ジョブをキャンセル
launch メソッドは引数として渡されたラムダ式を Job クラスのインスタンスに変換し(厳密にはちょっと違うけど)、その Job を実行してくれます。この動きをコルーチンを起動するとかいったりします。
Coroutine はあくまで処理を中断できるように分けるだけ
Coroutine はそれを使って通信だとか並列処理だとか重い処理だとかを行うことができますが、処理をいくつかに分けているだけです。その分けた処理を中断できるように制御したり、再開してみたりするのが Coroutine だと認識しています。
suspend 関数を理解する
大事なこと 1: suspend 関数の特徴
suspend fun doSomething(){
// ...
}
以下を頭に叩き込んでください
-
suspend
がついた関数はその実行中に中断できます。 - suspend 関数は他の suspend 関数内もしくは
launch
関数内でしか呼び出しできません。
suspend 関数の実装を見てみる
Coroutine の実装を見てみます。(この記事なんかがいいかもです)
suspend fun susFunc() {
println("test-1")
delay(1_000)
println("test-2")
}
このコードを
class susFunc(prevFunc) {
var current: Int // 現在の処理状況を表す
init {
current = 0
}
fun doNext() { // 次の処理を実行
when(current){
0 -> {
println("test-1")
current ++
delay(1_000,this) // delay終了時にthis.doNext()を呼び出してもらう
}
1 -> {
println("test-2")
current ++
}
2 -> {
prevFunc.doNext() // 呼び出しもとの中断していた後の処理を行う。
}
}
}
}
こんな感じに実装しているそうです。(だいぶ端折りました)特徴をまとめると、
- 引数として追加で呼び出しもとを受け取る(自身終了時に呼び出しもと.doNext()を呼ぶため)
- 関数実行時には doNext()が呼ばれる。
- 関数を実行しているときに
delay()
のようなsuspend
関数を呼び出すことになったら自身を渡して呼び出した関数が終わり次第this.doNext()
を読んでもらう。 - 自身の処理が終了したら自身の呼び出しもと.doNext()を呼び出す。(これにより中断されていた呼び出しもとの処理が再開される)
図解すると、
普通の関数は
に対し suspend 関数は
と関数の呼び出し時に再開したい関数(呼び出し元関数)を渡されるようになり、呼び出し元関数の再開ができるように、つまりは中断しても後で再開できるようになるわけです。
suspend 関数をいろんなところで実行したい
何度も言うように suspend 関数は中断ができる関数です。ここでは、Android アプリにおいてはどのように使うのか見てみましょう。
class MyViewModel : ViewModel(){
fun onClick(){
+ viewModelScope.launch{
+ // suspend関数を呼べる
+ }
}
}
viewModelScope.launch
を使用します。viewModelScope にて launch されたコルーチンはViewModel が死ぬと自動的に内部のコルーチンをキャンセルしてくれます(あら便利)。
別スレッドで実行したい
Coroutine を使えば別スレッドで処理を行いたい時もいい感じに処理をかけます。
class MyViewModel : ViewModel(){
fun onClick(){
viewModelScope.launch{
+ val userData = withContext(Dispatchers.IO) { getUserData() }
// ...
}
}
}
余談
今回紹介したlaunch
関数はCoroutine ビルダーと呼ばれます。Coroutine ビルダーはlaunch
以外にもasync
などがあります。async
はawait()
することで Coroutine の実行結果を得ることができます。
val user = async {
getUserData()
}.await()
Discussion