こんにちは。ギフトモールで Android アプリの開発をしている @KeithYokoma です。
前回の記事では最初の技術選定や設計にあった課題のうち、ViewModel の肥大化についてどのように解決したかを解説しました。今回は次の課題として浮上した Fragment
と Activity
の連携について解説します。
ギフトモールアプリにおける Fragment と Activity の役割
ギフトモールアプリでは Jetpack Navigation を用いて Fragment
ベースの画面遷移を実装しています。このため、様々な画面のレイアウトや UI の振る舞いは Fragment
単位で分離され、Activity
は BottomNavigationView のタブに対応する画面を切り替える設定を持つ構成になります。
開発開始当初は Toolbar
も Activity
に持たせていましたが、画面ごとに Toolbar
の振る舞いが異なったり、リストのスクロールに合わせて Toolbar
に動きをもたせたりしたくなったことで Toolbar
を Fragment
へと移設しました。
その後、Fragment
内部にある UI に合わせて BottomNavigationView
の動きも制御したくなってきました。ギフトモールアプリはギフトを探すことに重点を置いたアプリで、画面内にできるだけたくさんのギフトを表示できるよう、ギフトの一覧をスクロールしながら眺めている状況下では画面を切り替える BottomNavigationView
を隠し、少しでも一覧の表示領域を増やすことを考えたのです。
ところが BottomNavigationView
は Toolbar
とは異なり、Jetpack Navigation との連携があるため Fragment
内部への移設はできません。そのため、何らかの形で Fragment
内部から Activity
へ指示を出し、BottomNavigationView
の表示を切り替えるための仕組みが必要になりました。
Fragment から Activity に指示を出すための設計
Fragment
から Activity
に対して指示を出す方法にはいくつかの実装が考えられます。
1. リスナーインタフェースを経由して指示を出す
Fragment
から Activity
に対して何らかの指示を出すときの実装パターンとして、Fragment
で定義したリスナーインタフェースを Activity
が実装するパターンがあります。このパターンは Fragment
が登場して以来頻繁に用いられてきた実装パターンです。
仕組み自体は単純で、Fragment#onAttach
で Activity
がインタフェースを実装していればそれを使うようにします。
interface SampleFragmentCallback { fun showNavigation() fun hideNavigation() } class SampleFragment : Fragment() { private var callback: SampleFragmentCallback? = null override fun onAttach(context: Context) { super.onAttach(context) if (context is SampleFragmentCallback) { callback = context as SampleFragmentCallback } } } class SampleActivity : AppCompatActivity(), SampleFragmentCallback { override fun showNavigation() { /* BottomNavigationView を表示する */ } override fun hideNavigation() { /* BottomNavigationView を隠す */ } }
この方法は非常に単純な実装で Fragment
と Activity
の協調動作が実現できますが、課題もあります。
BottomNavigationView
の表示・非表示を切り替えたい画面がひとつとは限らないため、複数の Fragment
がそれぞれにインタフェースを定義し始めると、どれも結局は同じことをしたいだけにも関わらずActivity
が実装すべきインタフェースも増えていってしまいます。インタフェースを共通化すれば乱立は解決できますが、複数の Fragment
で上記の onAttach
ですべきボイラープレートのような実装を手書きしなければいけないことにはかわりありません。また実行時の型チェックに頼るため、万が一何らかの実装エラーがあった場合コンパイル時に気づけなくなることにも注意が必要です。
使い古された手法ではあるもののすこしナイーブな実装になるため、他の方法を考えることにしました。
2. 共通の ViewModel を経由して指示を出す
ちょうどギフトモールアプリでは Android Jetpack が提供する ViewModel を使っており、Dagger-Hilt による DI の仕組みも導入していたため、Activity
のスコープで保持する ViewModel
を Fragment
でもすぐに利用可能な状態でした。Activity
のスコープにある ViewModel
は activityViewModels
を使って Fragment
に注入します。この方法なら、BottomNavigationView
の表示・非表示の状態を扱うことに特化した ViewModel
を Activity
と Fragment
で共有でき、Fragment
での手続きも簡略化できます。
ViewModel
の役割は View
の状態を扱うことです。BottomNavigationView
の表示・非表示の状態を変更する役割を ViewModel
に持たせることも自然です。
data class SampleViewState( val bottomNavigationVisibility: BottomNavigationVisibility = VISIBLE ) { enum class BottomNavigationVisibility { VISIBLE, HIDDEN } } class SampleViewModel : ViewModel() { fun showNavigation() { /* SampleViewState.bottomNavigationVisibility を VISIBLE にする */ } fun hideNavigation() { /* SampleViewState.bottomNavigationVisibility を HIDDEN にする */ } }
何より、Fragment
の onAttach
で引数の Context
をキャストするボイラープレートがすべて ViewModel
の注入だけに置き換えられます。onAttach
で引数の Context
がインタフェースを実装しているかどうかを考慮する必要もなく、SampleViewModel
に定義してある関数を呼び出すだけで指示を出せるようになります。
class SampleFragment : Fragment() { private val sampleViewModel: SampleViewModel by activityViewModels() }
また Activity
から見ると、View の操作はすべて ViewModel
の状態の変更に合わせて実行するよう実装します。この実装はどの Fragment
でも共通の実装方法であり、Activity
と Fragment
の両方で設計・実装を統一できます。
class SampleActivity : AppCompatActivity() { private val sampleViewModel: SampleViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle) { // ... sampleViewModel.states .map { state -> state.bottomNavigationVisibility } .distinctUntilChanged() .subscribe { bottomNavigationVisibility -> when (bottomNavigationVisibility) { VISIBLE -> // BottomNavigationView を表示する HIDDEN -> // BottomNavigationView を隠す } } } }
ギフトモールアプリの設計思想にあわせて ViewModel
で BottomNavigationView
の表示状態を管理でき、仕組みとしても ViewModel
を直接 Fragment
に注入できることから、Fragment
から Activity
へ指示を出す方法として共通の ViewModel
を使うことにしました。
まとめ
今回は Fragment
と Activity
の役割の違いと、Fragment
の UI の振る舞いから Activity
に指示を出すための設計について解説しました。
Fragment
から Activity
に対して指示を出す実装には、コールバックインタフェースを用いる実装と共通の ViewModel
を使う実装の 2 通りの実装があります。
従来からあるコールバックインタフェースを用いた実装は使い古されている分定番の実装パターンではありますが、実行時の型チェックに依存した実装を必要とし、さらにその型チェックのコードがボイラープレートとなって様々な画面にあらわれてくるなど課題もあります。
一方、共通の ViewModel
を使う場合はJetpack の仕組みで Fragment
と Activity
で同じ ViewModel
のインスタンスを利用できます。また UI の状態管理を ViewModel
にまとめることで、他の Fragment
と共通の設計思想を適用でき、コードの統一性が保てます。
DI や ViewModel の仕組みがなければコールバックインタフェースを使う他によい方法はなかったかもしれません。あるいは、Event Bus の考え方を RxJava や Coroutines で実現して使う方法も考えられましたが、実現したいことを整理していくと、うまく UI の状態を ViewModel
でまとめて管理するシンプルな構成で必要十分でした。ViewModel
はすでにアプリ全体で導入しており設計方針も固まっていたため、UI の操作に関する部分の設計や実装の一貫性を保つことにも繋がりました。
次回は、「素のままの RecyclerView ではコードが複雑になってきた」という課題について解説します。
ギフトモールでは、一緒に働く仲間を募集しています!
まずは一度話してみるだけなどのカジュアル面談も歓迎ですので、少しでも興味を持った方はお気軽に連絡ください!