LoginSignup
581
478

More than 3 years have passed since last update.

Androidのライフサイクルの基礎からViewModel, LiveData, Kotlin Coroutinesまでを流れるように説明したい

Last updated at Posted at 2019-12-22

先日、Google Developer Expert for Androidになりました。 :tada: これからもよろしくおねがいいたします。
Androidの初心者がステップアップできるような記事を書いてみます。
なにかツッコミがあればコメントしてください🙏

ライフサイクルについて少し学んだ後に、ViewModel, LiveData, Kotlin Coroutinesについて、ライフサイクルに関連する課題とその解決策という位置づけで話していきます。

1. ライフサイクルの基礎

ライフサイクルとは生き物の蝶でいうと生まれて、卵から幼虫になってさなぎになって、蝶になって死んでいく感じですが、AndroidのActivityやFragmentという表示を持つコンポーネントにもライフサイクルがあります。
どのようなライフサイクルの状態があるのか、どのようなライフサイクルのメソッドがあるのかを説明していきます。
なんとなくわかる方は2.の画面回転からどうぞ。

Activityのライフサイクルの状態

Actiivtyのライフサイクルの変化で呼ばれるメソッドがあるので、それぞれのメソッドにログを入れて動作を確認してみましょう。
最近使われるActivity(AppCompatActivity)には今のライフサイクルの状態を確認できるgetLifecycle()というメソッドが追加されており、それにより、ライフサイクルの状態を確認できます。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d("MainActivity", "onCreate state:" + lifecycle.currentState)
    }

    override fun onStart() {
        super.onStart()
        Log.d("MainActivity", "onStart state:" + lifecycle.currentState)
    }

    override fun onResume() {
        super.onResume()
        Log.d("MainActivity", "onResume state:" + lifecycle.currentState)
    }

    override fun onPostResume() { // onPostResume()はonResumeの後に呼ばれます。
        super.onPostResume()
        Log.d("MainActivity", "onPostResume state:" + lifecycle.currentState)
    }

    override fun onPause() {
        super.onPause()
        Log.d("MainActivity", "onPause state:"+lifecycle.currentState)
    }

    override fun onStop() {
        super.onStop()
        Log.d("MainActivity", "onStop state:"+lifecycle.currentState)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("MainActivity", "onDestroy state:"+lifecycle.currentState)
    }
}

アプリを起動すると以下のようなログが出ます。

onCreate state:INITIALIZED
onStart state:CREATED
onResume state:STARTED
onPostResume state:RESUMED

まずそれぞれの状態を説明しましょう。Activityには以下のような状態があります。


(codelabより https://codelabs.developers.google.com/codelabs/kotlin-android-training-lifecycles-logging/index.html?index=..%2F..android-kotlin-fundamentals#2 )

まずActivityがインスタンス化されてすぐのデフォルトの状態ではstate:INITIALIZED状態であり、その後onCreateの後にstate:CREATEDになり、onStartの後にstate:STARTEDonResume後にstate:RESUMEDになります。このように状態が遷移していくことが、このログから観察できます。

それぞれの状態について

INITIALIZED

Activityのインスタンスができているだけで画面などの用意はできていない状態。

CREATED

onCreate()によって画面などはできている(ActivityでViewが作られている)が、表示されてもおらず、フォーカスも持っていない状態。
(画面ができていると言っても表示するためのViewができているだけで、APIなどからデータを取得して表示するときには表示する内容がない場合もある。)

どういうときにこの状態のままになるか?
→ Activityが別のActivityの裏にいて、画面が隠れているとき、この状態のままになる。

STARTED

画面が表示されているが、フォーカスを持っていない状態

どういうときにこの状態のままになるか?
→ Activityが他の透過Activityの後ろに表示されているとき、マルチウインドウで他のActivityにフォーカスがあたっている(選択されている)とき。

image.png

RESUMED

画面がフォーカスを持っている状態。

どういうときにこの状態のままになるか?
→ 普通にActivityが最前面で表示されているとき。

それぞれのメソッドについて

それぞれonCreate()、onStart()、onResume()について説明していきます。onPause()などのメソッドについてもこの中で触れます。

onCreate()

INITIALIZEDからCREATEDに変わるときに呼ばれる。

タイミング

Activityのインスタンスが作られたときに一度だけ呼び出されます。
逆にonDestroy()はActivityのインスタンスが破棄されるときに一度だけ呼ばれます。

何をするか?

通常、ここでActivityで表示するViewを作成setContentView(R.layout.activity_main)を行ったりして、Activityで必要になるものを作っていきます。

onStart()

CREATEDからSTARTEDに変わるときに呼ばれる。

タイミング

画面が表示されたタイミングで発火されます。
例えばMainActivityからDetailActivityに遷移していて、戻るボタンでMainActivity戻ってきたときには、MainActivityのインスタンスはメモリ不足で破棄されていなければそのまま使われるので、onCreateは呼ばれず、onStartから呼ばれることになります。
逆に画面が表示されなくなったらonStop()が呼び出されます。

何をするか?

例えば動画プレイヤーであれば画面が表示されていないのに動画を再生するのは微妙ですよね?そういうときにonStart()で動画の再生を開始しておいて、onStopで動画を止めるようにすれば、うまく動作させることができます。

onResume()

STARTEDからRESUMEDに変わるときに呼ばれる。

タイミング

画面がフォーカスを持ったタイミングで発火されます。
Activityは透過させることができ、裏に以前に表示されていたActivityを表示させておくことができたりします。その場合に表示はされているのですが、フォーカスを持っていない状態になります。
また、Androidにはマルチウインドウという機能があり、それを使っている場合は画面に複数のActivityが表示されます。その場合にはフォーカスされている画面でだけフォーカスを持ちます。(Android 10から一部仕様が変わりました)
逆にフォーカスを失ったときにonPauseが呼ばれます。
onPostResume()はonResumeの後に呼ばれます。

何をするか?

ログ計測などで使われたりはよくありますが、そこまで頻繁には使われるメソッドではないです。フォーカスがあたったときに更新したいなどは行うことができます。

2. 画面回転とViewModel

Androidのライフサイクルの難しいところと言われる画面回転について記述しておきます。
Androidでは画面回転するとActivityのインスタンスが作り直されます。

アプリの起動起動
onCreate state:INITIALIZED
onStart state:CREATED
onResume state:STARTED
onPostResume state:RESUMED
--- ここで画面回転開始
onPause state:STARTED
onStop state:CREATED
onDestroy state:DESTROYED ← 一度Activityのインスタンスが作り直される
onCreate state:INITIALIZED
onStart state:CREATED
onResume state:STARTED
onPostResume state:RESUMED

画面回転すると何が困るのか?

こんな感じのTextViewがあったとしましょう。

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="100sp"
        />

これをクリックで+1ずつしていくコードを書いたとしましょう。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView = findViewById<TextView>(R.id.text)
        textView.setOnClickListener {
            textView.text = (textView.text.toString().toInt() + 1).toString()
        }

画面回転で初期化されます😇

screenrotate.gif

Activityにデータを保持しておくとActivityのインスタンスごと作り直されるので、このような現象が起きます。
(FragmentというActivity内の部品を表すコンポーネントがあり、このFragmentで画面回転での破棄を無効にするオプションがあるのですが、deprecatedになりました)

onSaveInstanceState()を使った解決策

onSaveInstanceState()はonStop()の前に呼び出されるメソッドです。
savedInstanceStateはActivityが破棄されるときにデータを保存しておいて、取り出すことができます。
具体的にはonSaveInstanceStateでbundleにデータを入れ、onCreateの引数のsavedInstanceStateで取り出す形になります。
savedInstanceStateは画面回転だけでなく、アプリケーションのプロセスが破棄されても、システムサービスでデータが保持されているため、かなり強力な解決策です。

class MainActivity : AppCompatActivity() {
    lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById<TextView>(R.id.text)
        textView.setOnClickListener {
            textView.text = (textView.text.toString().toInt() + 1).toString()
        }
        if (savedInstanceState != null) {
            textView.text = savedInstanceState.getString("count")
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("count", textView.text.toString())
    }

ただ、この方法にもデメリットがあります。
大きすぎるデータを保存するとTransactionTooLargeExceptionになってしまうため、ユーザーのリストや画像などには適していません。
また1つ1つデータを保存していくのは結構骨が折れます。

ViewModelを使った解決策

ViewModelはArchitecture Componentと言われるライブラリの一部で、このデータの保持を楽にしてくれるコンポーネントです。
よくJetpackやAndroidXという言葉が出てくると思うので、以下に自分の理解している構造を書いておきます。つまりAndroid Jetpackの中にあるAndroidXの中にあるArchitecture Componentの中にあるViewModel。。です。(間違っていたら教えて下さい)
image.png

ViewModelはActivityと違って画面回転を通じて生き残ります。つまり、いちいち保存したりしなくてもViewModelでデータを保持させることでうまく動作させることができます。

image.png
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=en より

ViewModelはViewModelProviderから取得しており、これにより画面回転してアクセスする元のActivityのインスタンスが変わっても同じViewModelのインスタンスを取得することができます。

class MainViewModel : ViewModel() {
    var count: Int = 0
}

class MainActivity : AppCompatActivity() {
    lateinit var textView: TextView
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById<TextView>(R.id.text)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        textView.text = viewModel.count.toString()
        textView.setOnClickListener {
            viewModel.count++
            textView.text = viewModel.count.toString()
        }
    }

lifecycle.gif

画面回転を克服して実装していくことができました :tada:

3. ViewModelとLiveData

もうViewModelを使えばAndroidアプリの実装は完璧でしょうか?
カウンターを作る人は多くないと思うので試しにもう少し実用的な例でやっていきましょう。
Androidのアプリではサーバーからデータを取得して表示するようなアプリはかなりよくあるパターンです。

例えば以下のようなAPIからデータを取得するクラスがあったとしましょう。実際は通信して、データを取ってくる形になりますが、ここではスレッドを作ってコールバックでデータを返すだけにします。

class Api {
    fun fetch(url: String, onFetched: (response: String) -> Unit) {
        thread {
            // simulate api call
            Thread.sleep(5000)
            onFetched("$url:fetched")
        }
    }
}

(APIが返してきた文字列を表示している例)
image.png

そしてViewModelではViewModelで取得したときに、Activityでデータを受け取れるようにしたとしましょう(※ダメな例なので真似して実装しないでください。)

:x:

class MainViewModel : ViewModel() {
    val api = Api()
    var response: String? = null

    init {
        fetch()
    }

    private var onResponseChangedListeners: List<((response: String) -> Unit)> = mutableListOf()

    fun addOnResponseChangedListener(onResponseChangedListener: (response: String) -> Unit) {
        this.onResponseChangedListeners += onResponseChangedListener
    }

    private fun fetch() {
        api.fetch("http://api.example.com/hogehoge") { response ->
            this.response = response
            this.onResponseChangedListeners.forEach { it(response) }
        }
    }
}

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        val textView = findViewById<TextView>(R.id.text)
        textView.text = viewModel.response
        viewModel.addOnResponseChangedListener { response ->
            runOnUiThread {
                textView.text = response
            }
        }
    }
}

一見問題なく動くように見えますがこのコードにはいくつかの問題点があります。 :sob:

問題1. Activityがメモリリークしている

ViewModelの生存期間はActivityより長いので、画面回転するとonResponseChangedListenersで保持されているので、Activityのインスタンスがリークします。ActivityのインスタンスがリークするとActivityは画面全体を保持しているので、メモリが枯渇するようになります。

↓でActivityのインスタンスが暗黙的に渡されています。

        viewModel.addOnResponseChangedListener { response ->

デバッガーでJava heapをdumpしてActivity/Fragment Leaksにチェックを入れるとリークしているインスタンスを教えてくれます。
image.png

一応この問題に関しては、ActivityのonDestroy()でonResponseChangedListenersのlistenerを消すようにすれば一応うまく動きます。

問題2. UIへのデータの反映忘れが起こりやすい

addOnResponseChangedListenerではfetch()が終わったときしか反映されないので、画面回転後にActivityが再生成された後にはTextViewに値が反映されません。

        val textView = findViewById<TextView>(R.id.text)
        // **↓の行を忘れても一応動くが画面回転でtextが表示されなくなる**
        textView.text = viewModel.response
        // ** ↑ **
        viewModel.addOnResponseChangedListener { response ->
            runOnUiThread {
                textView.text = response
            }
        }

問題3. onStop以降でもaddOnResponseChangedListenerのコールバックが呼ばれる

APIの応答に5秒かかっている間に普通にホームボタンが押されたりする可能性があります。そのときにActivityはCREATED状態になります。
AndroidではonStopとonDestoryの間、CREATED状態の時に呼び出すとクラッシュするAndroidのFrameworkのAPIが存在します。(幸い、TextViewへのsetでは大丈夫です。)例えばこのタイミングでFragmentをレイアウトに追加する(FragmentTransactionのcommit()など)とクラッシュが発生します。
これを対策するには今の状態をみて、次のonStart以降で処理を動かすなどかなり工夫が必要になります。 :innocent:
昔は以下のような処理をキューにためておいて、onResume以降で処理するなどを頑張ってして、なんとかしていました。
https://stackoverflow.com/a/8122789/4339442

LiveDataを使った解決策

自分で上記の問題をそれぞれ対応していくのはかなかな大変です。
そこでArchitecture ComponentのLiveDataは上記の問題を解決するものになります。LiveDataはobserve(観測)できるAndroidのライフサイクルを考慮したデータホルダーとなります。
MutableLiveDataは変更可能なLiveDataでsetValue()postValue()を呼ぶことで変更することができます。setValueMainThreadで値を入れるとき、postValueMainThread以外から呼ばれるときに利用します。
MutableLiveDataの親クラスにLiveDataクラスがあります。これはデータの変更をobserve()を呼ぶことで観測することができます。
以下の例ではfetch()でデータを_responseにデータをセットして、MainActivity内でデータをobserve()することで反映しています。

class MainViewModel : ViewModel() {
    val api = Api()
    private val _response: MutableLiveData<String> = MutableLiveData()
    val response: LiveData<String> get() = _response

    init {
        fetch()
    }

    private fun fetch() {
        api.fetch("http://api.example.com/hogehoge") { response ->
            _response.postValue(response)
        }
    }
}

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        val textView = findViewById<TextView>(R.id.text)
        viewModel.response.observe(this, Observer { response ->
            textView.text = response
        })
    }
}

実際にどのように上記の問題を解決しているのか見ていきましょう。

  • 問題1. LiveDataが"Activityがメモリリークしている"に対してどうやって対応しているか?

observeメソッドにthisを渡しています。ライフサイクルを保持するActivityやFragmentはLifecycleOwnerというinterfaceを実装しています。LiveDataはライフサイクルがDESTROYED に変わったときにオブザーバーを削除してくれます。そのため、メモリリークが発生しません。

        viewModel.response.observe(this, Observer { response ->
            textView.text = response
        })
  • 問題2. LiveDataが"UIへのデータの反映忘れが起こりやすい"に対してどうやって対応しているか?

LiveDataをobserve()したとき、ライフサイクルがonStart以降になったときに、オブザーバーを呼び出してくれます。
そのため画面回転後にonCreate()が呼ばれ、そこでobserve()した場合に、ちゃんと呼び出してくれるのでデータを反映することができます。
そのため、データが変更されていなくても呼び出されるので、onCreateメソッドの中でViewModelが保持しているLiveDataのデータを直接見ることなしに、UIを変更していくことができます。

  • 問題3. LiveDataが"onStop以降でもaddOnResponseChangedListenerのコールバックが呼ばれる"に対してどうやって対応しているか?

observeしたときに渡すActivityやFragment(LifecycleOwner)のライフサイクルがSTARTED以降でないと変更を呼び出さないので、問題ありません。

他にもLiveDataはいくつかの問題を解決してくれます。Android Developerを確認してみましょう。
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja

4. ViewModelとKotlin Coroutiens

ここまでで、一応いい感じに動くアプリができました。
ただ、まだ残念ながら少し問題は残っています。

問題1. ViewModelのリーク

以下を実行したときに、スレッドによってコールバックが保持されてしまっています。もしコールバックがずっと呼び出されなければ、ViewModelのインスタンスが破棄されなかったり、ViewModelはもう使われないのに不要に呼び出されてしまったりします。

    private fun fetch() {
        api.fetch("http://api.example.com/hogehoge") { response ->
            _response.postValue(response)
        }
    }

問題2. コールバック地獄

DBから読み出して、なければAPIから取得したい場合はどうでしょうか?行おうとすると以下のようになります。このようにコールバックが深くなっていくことをコールバックヘルといいます。この場合はそんなにはわかりにくくないですが、実際にはもっと複雑になっていきます。 (本来このような処理はRepositoryなどに分離するのが普通ですが、分離してもこの問題は残ります。)

class MainViewModel : ViewModel() {
    val db = Db()
    val api = Api()
    private val _contents: MutableLiveData<String> = MutableLiveData()
    val contents: LiveData<String> get() = _contents

    init {
        fetch()
    }

    private fun fetch() {
        db.read { contents: String? ->
            if(contents != null) {
                _contents.postValue(contents)
                return@read
            }
            api.fetch("http://api.example.com/hogehoge") { response ->
                _contents.postValue(response)
            }
        }
    }
}

問題3. メインスレッドかどうかを気にしたプログラミングが必要となる

LiveDataはメインスレッド以外で値をセットするにはpostValueを使う必要があります。コールバックなどで今はメインスレッドか?などを気にしながらプログラミングしていく必要があります。

    private fun fetch() {
        api.fetch("http://api.example.com/hogehoge") { response ->
// **このコールバック内はメインスレッドではないため、
// ここでsetValue()ではなく、postValue()を使わないとクラッシュする**
            _response.postValue(response) 
        }
    }

Kotlin Corotuinesを使った解決策

Coroutinesは非同期処理のデザインパターンで、Kotlinに実装されたものがKotlin Coroutinesです。
AndroidはFirst Class Coroutines Supportしています。
image.png

これにより上記の問題が解決できます。

Coroutinesのコードを理解するにはいくつか理解しなくてはいけない概念が存在します。

Coroutinesの中断と再開

コルーチンを使うと、launch{}の中で以下のようにメインスレッドを使うコードとAPIの呼び出しをするようなコードを混ぜて書くことができます。

launch {
    progress.isVisible = true
    val result = api.fetch()
    progress.isVisible = false
    show(result)
}

Androidのメインスレッドで普通に上記のようなコードを書くと、メインスレッドを通信中にブロックしてアプリがタップしても何をしても反応しなくなり、フリーズ状態になります。Application Not Responding(ANR)が発生します。
適切に実装されたコルーチンのメソッドであれば、このコードでANRなどの問題は起こりません。
なぜならKotlin Coroutinesには中断、再開という概念があるからです。
具体的にはこのapi.fetch()を呼んだときにCorotuinesを中断状態に入り、中断に入っている間は他のタップしたときの反応などメインスレッドを使う処理を実行させることができ、fetch()が終わったときにまたこのapi.fetch()の次の行に戻ってきて、メインスレッドで処理の続きをできる、再開できます。

image.png
Google I/O 2019より

またCoroutinesをlaunch()するとJobのインスタンスが取得でき、cancel()を呼ぶことで、途中で処理を止めることができます。

val job = launch {
    progress.isVisible = true
    val result = api.fetch()
    progress.isVisible = false
    show(result)
}

// 不要になったらキャンセルする
job.cancel()

Coroutines Scope

先程のコードの例は少し間違っており、実際はコルーチンスコープがないとコルーチンはlaunch()メソッドを呼ぶことができません。
Kotlin Corotuinesは構造化することができ、親のJobをCoroutiensScopeに渡して作成し、CoroutineScopeのcancelを呼ぶことで、子のCoroutinesを全てキャンセルしていくことができます。

val scope = CoroutineScope(Job())
scope.launch {
...
}
scope.launch {
...
}

// 不要になったらcancelする
scope.cancel()

Coroutines Dispatcher

実際どのスレッドで処理が実行されるのかが気になると思います。
以下のように書くことで途中でスレッドを切り替えて処理することができます。

Dispatchers.MAIN = メインスレッド(AndroidではUIを触る)
Dispatchers.IO = I/O関連を処理するためのスレッドが利用される
Dispatchers.DEFAULT = それ以外の計算系に利用される

scope.launch {
    progress.isVisible = true
    val result = withContext(Dispatchers.IO){
        URL("").openConnection().getInputStream()...
        ...
    }
    progress.isVisible = false
    show(result)
}

これをただメソッドに分けて書くと以下のようになります。このsuspend functionとは、中断可能なメソッドという意味です。勘違いしてほしくないのが、このsuspendを使ったからといって勝手にバックグラウンドスレッドになったりしないということです。

        val scope = CoroutineScope(Job())
        scope.launch {
            progress.isVisible = true
            val result = fetchApi()
            progress.isVisible = false
        }
    }

    private suspend fun fetchApi(): String {
        return withContext(Dispatchers.IO) {
            URL("").openConnection().getInputStream()...
            ...
        }
    }

Kotlin Corotuinesを使った解決策のコード

これを利用したKotlin Corotuinesを使った解決策では以下のようになります。
viewmodel-ktx 2.1.0を使うとviewModelScopeというものが用意されており、これを使うことで、ViewModelが破棄されるときにCorotuinesScopeをキャンセルすることができます。

class Db {
    suspend fun read(): String? {
        return withContext(Dispatchers.IO) {
            // simulate db read
            delay(5000)
            null
        }
    }
}

class Api {
    suspend fun fetch(url: String): String {
        return withContext(Dispatchers.IO) {
            // simulate api call
            delay(5000)
            "$url:fetched"
        }
    }
}

class MainViewModel : ViewModel() {
    val db = Db()
    val api = Api()
    private val _contents: MutableLiveData<String> = MutableLiveData()
    val contents: LiveData<String> get() = _contents

    init {
        fetch()
    }

    private fun fetch() {
        viewModelScope.launch {
            val contents = db.read()
            if (contents != null) {
                _contents.postValue(contents)
                return@launch
            }
            val response = api.fetch("http://api.example.com/hogehoge")
            _contents.value = response
        }
    }
}

このコードがどのように問題を解決するのかを見ていきましょう。

  • Kotlin Corotuiensが"問題1. ViewModelのリーク"に対してどうやって対応しているか?
    viewModelScopeがキャンセルされることによって、コルーチンがキャンセルされるので、問題なく動作します。

  • Kotlin Corotuiensが"問題2. コールバック地獄"に対してどうやって対応しているか?
    Kotlin Coroutinesの中断、再開によってコールバックなしに非同期処理をコーディングしていくことができます。

  • Kotlin Corotuiensが"問題3. メインスレッドかどうかを気にしたプログラミングが必要となる"に対してどうやって対応しているか?
    viewModelScopeはメインスレッドで実行され、明示的に切り替えなければ基本的にメインスレッドで行われるため、APIコールの後であっても今のスレッドを気にせずにコーディングしていくことができます。

実際どのようにKotlin CoroutinesでAPIやDBを呼び出すメソッドを実装していったら良いのか?

APIやDB呼び出しのコードがdelay()などを使ったデモコードになっておりわかりにくかったと思います。
実際にはRetrofitやRoomはsuspend functionに対応しているので、自動的にsuspend functionを定義しておくことで実装を生成してくれるため、問題なく実装できます。
またもしそのような方法が提供されていなくても以下のようにsuspendCancellableCoroutineを利用することで問題なく実装できます。

    suspend fun fetch(): String {
        return suspendCancellableCoroutine<String> { cancellableContinuation ->
            myApi.fetch( // 自分で用意したAPI
                    onSuccess = { result: String ->
                        cancellableContinuation.resume(result)
                    },
                    onFailure = { e: Throwable ->
                        cancellableContinuation.resumeWithException(e)
                    }
            )
            cancellableContinuation.invokeOnCancellation {
                // キャンセル処理
                myApi.cancelFetch()
            }
        }
    }

またはブロッキングして取得する方法があるのであれば以下のような方法も使えます。(CoroutinesはキャンセルしたときにThreadをintrerrupedしないので処理が途中でキャンセルされないので注意が必要です)

    suspend fun fetch(): String {
        return withContext(Dispatchers.IO) {
            api.blockingFetch()
        }
    }

まとめ

Androidのライフサイクルの基礎的なところから、画面回転の問題をViewModelで解決し、データを監視する問題をLiveDataで解決、非同期処理の問題をKotlin Coroutinesで解決していくことができました。
実際にAndroid Developerには以下のようにそれぞれの問題について書かれています。それをつないで書いてみたのが今回の記事になります。そのため、部分的にわかりにくい部分があれば、以下を参照していただけると結構わかっていくのではないかと思います。

ViewModel
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja
LiveData
https://developer.android.com/topic/libraries/architecture/livedata?hl=ja
Coroutines
https://developer.android.com/kotlin/coroutines

581
478
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
581
478