先日、Google Developer Expert for Androidになりました。 これからもよろしくおねがいいたします。
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:STARTED
、onResume
後にstate:RESUMED
になります。このように状態が遷移していくことが、このログから観察できます。
それぞれの状態について
INITIALIZED
Activityのインスタンスができているだけで画面などの用意はできていない状態。
CREATED
onCreate()によって画面などはできている(ActivityでViewが作られている)が、表示されてもおらず、フォーカスも持っていない状態。
(画面ができていると言っても表示するためのViewができているだけで、APIなどからデータを取得して表示するときには表示する内容がない場合もある。)
どういうときにこの状態のままになるか?
→ Activityが別のActivityの裏にいて、画面が隠れているとき、この状態のままになる。
STARTED
画面が表示されているが、フォーカスを持っていない状態
どういうときにこの状態のままになるか?
→ Activityが他の透過Activityの後ろに表示されているとき、マルチウインドウで他のActivityにフォーカスがあたっている(選択されている)とき。
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()
}
画面回転で初期化されます😇
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。。です。(間違っていたら教えて下さい)
ViewModelはActivityと違って画面回転を通じて生き残ります。つまり、いちいち保存したりしなくてもViewModelでデータを保持させることでうまく動作させることができます。
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()
}
}
画面回転を克服して実装していくことができました
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")
}
}
}
そしてViewModelではViewModelで取得したときに、Activityでデータを受け取れるようにしたとしましょう(※ダメな例なので真似して実装しないでください。)
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
}
}
}
}
一見問題なく動くように見えますがこのコードにはいくつかの問題点があります。
問題1. Activityがメモリリークしている
ViewModelの生存期間はActivityより長いので、画面回転するとonResponseChangedListeners
で保持されているので、Activityのインスタンスがリークします。ActivityのインスタンスがリークするとActivityは画面全体を保持しているので、メモリが枯渇するようになります。
↓でActivityのインスタンスが暗黙的に渡されています。
viewModel.addOnResponseChangedListener { response ->
デバッガーでJava heapをdumpしてActivity/Fragment Leaksにチェックを入れるとリークしているインスタンスを教えてくれます。
一応この問題に関しては、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以降で処理を動かすなどかなり工夫が必要になります。
昔は以下のような処理をキューにためておいて、onResume以降で処理するなどを頑張ってして、なんとかしていました。
https://stackoverflow.com/a/8122789/4339442
LiveDataを使った解決策
自分で上記の問題をそれぞれ対応していくのはかなかな大変です。
そこでArchitecture ComponentのLiveDataは上記の問題を解決するものになります。LiveDataはobserve(観測)できるAndroidのライフサイクルを考慮したデータホルダーとなります。
MutableLiveDataは変更可能なLiveDataでsetValue()
やpostValue()
を呼ぶことで変更することができます。setValue
はMainThread
で値を入れるとき、postValue
はMainThread
以外から呼ばれるときに利用します。
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しています。
これにより上記の問題が解決できます。
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()
の次の行に戻ってきて、メインスレッドで処理の続きをできる、再開
できます。
また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