前人未踏の領域へ Androidアプリ開発編

Androidアプリ開発に関する調査メモ置き場。古い記事にはアプリ以外も含まれます。

Android:FragmentのライフサイクルとLiveData、Flowの関係

内容

LiveDataやFlowを使って値の変更をwatchするようになり、Fragmentのライフサイクルに対してどのタイミングで発動するのかが気になったので調べてみた。

結果

イベント タイプ
onCreate Fragment
launchWhenCreated Flow
onCreateView Fragment
onViewCreated Fragment
onViewStateRestored Fragment
onStart Fragment
lifecycleOwner LiveData
launchWhenStarted Flow
viewLifecycleOwner LiveData
onResume Fragment
launchWhenResumed Flow
onStop Fragment
onSaveInstanceState Fragment
onDestroyView Fragment
  • onStart 以降に viewModelの値が変化した場合、コードの記述順に関わらず同じ順番でイベントが呼ばれている
  • viewLifecycleOwnerlaunchWhenStarted は後から値がセットされたか、最初から値があったかで順序が異なる。
    とはいえ同じ値を同時にwatchすることはないだろうから気にしなくても良さそう。

検証コード

コードは大体以下のような感じ。super()は省略。viewModel.hogeにはあらかじめ値がセットされているものと仮定する。

override fun onCreate() {
    viewModel.hoge.observe(this){
         Log.d(TAG, "lifecycleOwner")
    }

    lifecycleScope.launchWhenCreated {
        viewModel.hoge.asFlow().collectLatest {
            Log.d(TAG, "launchWhenCreated")
        }
    }

    lifecycleScope.launchWhenStarted {
        viewModel.hoge.asFlow().collectLatest {
            Log.d(TAG, "launchWhenStarted")
        }
    }

    lifecycleScope.launchWhenResumed {
        viewModel.hoge.asFlow().collectLatest {
            Log.d(TAG, "launchWhenResumed")
        }
    }

    Log.d(TAG, "onCreate")
}

override fun onCreateView() {
    viewModel.hoge.observe(viewLifecycleOwner) {
        Log.d(TAG, "viewLifecycleOwner")
    }
    Log.d(TAG, "onCreateView")
}

override fun onViewStateRestored() {
    Log.d(TAG, "onViewStateRestored")
}

override fun onStart() {
    Log.d(TAG, "onStart")
}

override fun onResume() {
    Log.d(TAG, "onResume")
}

override fun onStop() {
    Log.d(TAG, "onStop")
}

override fun onSaveInstanceState() {
    Log.d(TAG, "onSaveInstanceState")
}

override fun onDestroyView() {
    Log.d(TAG, "onDestroyView")
}

感想

概ね予想通り。 Fragmentのライフサイクルの後にFlowイベントが発火するのでその辺は意識しておいた方がいいかもしれない。 LiveDataとFlowに関してはViewModelの値が変更された場合のライフサイクルを含めた挙動などについても今後確認をしておきたい。

Kotlin:String.ifBlank{}が便利そう

AndroidStudioでリファクタリングのアシストが表示された。

文字列がblankの場合に特定の値を設定したい場合に

param.q = if (query.isNotBlank()) {
    query
} else {
    "hoge"
}

ifBlank()を使えば対象の文字列を代入しつつ、nullまたは空の場合にだけ代替の値を挿入できるようになる

param.q = query.ifBlank {
    "hoge"
}

AndroidStudio使っていれば勝手に教えてくれるので気にする必要はないのだけど、ちょっと感動したので。いや〜Kotlin便利やわ。

Android:Jetpack Navigationでボトムナビのタブ切り替えでハマる

課題

Jetpack NavigationがBottomNavigationのMultiple BackStackに対応したとのことで喜んで実装してみたところ、 ボトムナビを切り替えてまた元の画面に戻った場合の挙動がおかしくなった。

原因

Jetpack Navigationでボトムナビの実装を行うと、ボトムナビを切り替えた時点で切り替え前に表示していたFragmentのonDestroy(onDetachも)が呼ばれて状態が破棄されてしまう。 なのでボトムナビを元のタブに戻した時点でFragmentのプロパティに持たせていた情報は失われているため、表示がおかしくなる。

class BookFragment: Fragment() {

    private var book: Book? = null // ←ボトムナビを切り替えた時点で失われる!

}

対応

1:ViewModelを使う Fragmentのプロパティにはステータス情報を持たせず、全部ViewModel側に移動する。FragmentがonDetachされた時点でViewModelも 破棄されそうなものだが、なぜかそうはならない。Jetpack Navigationがその辺りはうまくやっていくれているようだ。

2:onSaveInstanceStateとonViewStateRestoredを使う 実は殆ど使ってなかったけど、今こそ使う時かもしれない。

以下はViewModelの場合

class BookViewModel:ViewModel(){
    val book:Book?
}

もちろんLiveDataやStateFlowが使えそうならそちらでも。

class BookFragment: Fragment() {

    // GetterとSetterをViewModel参照にする
    private val book: Book?
        get() = viewModel.book
        set(value) {
             viewModel.book = value
        }
}

ちょっと気に入っているViewModelの内容をFragmentのプロパティっぽく使う小技。バッドプラクティスかもだけど。 LiveDataやStateFlowなどにする必要がなさそうな場合はとりあえず上記のようにしてViewModel経由で参照するようにしてしまえば多くの変更をしなくて済む。

これで一応多くの問題は回避できるのだけど、ViewModelの内容がFragmentのライフサイクルよりも長くいることに なるので今度はLiveDataのobserveのタイミングなどが問題になって結構面倒くさい。

まとめ

  • Jetpack Navigationを使ってボトムナビを実装する場合、ボトムナビ切り替え時に元いた画面のFragmentが都度破棄(destroy)される
    • Fragmentのプロパティに持たせていたステータス情報が失われてしまう
    • ViewModel側に移動することで対応する
  • 初期状態がnullのはずのobserverが値を持った状態で反応してしまうため、初期表示時なのかタブ切り替えの戻りなのか判別をする必要がある
  • 画面遷移してバックボタンが押下された場合と、ボトムナビを切り替えて戻った場合とで挙動が異なるのを考慮して実装する必要がある
    • 初期表示時
    • 画面遷移からの戻り時
    • ボトムナビを切り替えてから元のタブに戻ってきた場合

Navigationの実装してから問題に気づくまでに1ヶ月以上かかった。修正が激烈に面倒。リリース前に気づけて良かった。