課題
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ヶ月以上かかった。修正が激烈に面倒。リリース前に気づけて良かった。