今回の記事では、MVVM(Model-View-ViewModel)と呼ばれるアーキテクチャについて説明していきます。
サンプルアプリ
今後しばらくの記事では、Google社によって提供されているAndroid Architecture BlueprintsのTODOアプリをベースに話を進めていきます。
TODOアプリは以下の5画面を持っています。
ソースコードはこちらにあります。
なお、このアプリはMVVMの実装の正解というわけではありません。また、重要なことはコードを構造化する方法を理解することなので、ここでは実装すべてには触れません。
ここで触れなかった部分につきましては、上記のリンクをご参照ください。
MVVM(Model-View-ViewModel)とは
MVVMは、UIの分離を目的としたアーキテクチャです。
後述するデータバインディングという仕組みによってViewを抽象化し、ビジネスロジックから分離します。
MVVMの各要素の主な役割は以下の通りです。
データバインディングとは
データバインディングは、データとUIを紐付ける仕組みです。
データバインディングには以下の2種類があります。
Androidでは、Android Jetpackの一部として提供されているデータバインディングライブラリによって後者の双方向データバインディングを実現できます。
TODOアプリの実装
アーキテクチャの概要を説明したところで、ここからはTODOアプリが具体的にどのように実装されているかを見ていきます。
まず、TODOアプリのプロジェクト構成は以下の通りです。このプロジェクトは基本的に画面単位でパッケージ管理されています。
data
パッケージにはタスクを保存するためのリポジトリとインタフェースが格納されています。util
パッケージにはいわゆるユーティリティクラスが格納されています。
com.example.android.architecture.blueprints.todoapp ├ addedittask (追加画面、編集画面) ├ data ├ statistics (統計画面) ├ taskdetail (詳細画面) ├ tasks (トップ画面) └ util
ここからは、例として詳細画面のソースコードを見ていきたいと思います。
taskdetail
パッケージの中身は以下の通りです。
taskdetail ├ TaskDetailActivity ├ TaskDetailFragment ├ TaskDetailNavigator └ TaskDetailViewModel
この4つのクラスの役割と実装について、以下にまとめていきます。
ViewModelの役割と実装
まず、ViewModelの役割と実装を見ていきます。
TaskDetailViewModel
の実装の大部分は継承元のTaskViewModel
で行われているため、内容が分かりづらいかもしれません。
TaskDetailViewModel
の役割は、Navigatorを経由してFragmentとActivityを橋渡しすることです。
TaskViewModel
はレポジトリへアクセスし、データを取得したり更新する役割を担っています。
/** * ViewModel */ class TaskDetailViewModel( context: Context, tasksRepository: TasksRepository ) : TaskViewModel(context, tasksRepository) { private var mTaskDetailNavigator: TaskDetailNavigator? = null // Navigator(Activity)の参照を受け取る fun setNavigator(taskDetailNavigator: TaskDetailNavigator) { mTaskDetailNavigator = taskDetailNavigator } // メモリリークを防ぐため、Activity(Navigator)が破棄されるときに // 参照を破棄する fun onActivityDestroyed() { mTaskDetailNavigator = null } // タスクを削除し、トップ画面へ遷移する override fun deleteTask() { super.deleteTask() mTaskDetailNavigator?.onTaskDeleted() } // 編集画面へ遷移する fun startEditTask() { mTaskDetailNavigator?.onStartEditTask() } }
Fragmentの役割と実装
次に、Fragmentの役割と実装を見ていきます。
TaskDetailFragment
の役割は、UIの初期設定とユーザ操作をViewModelへ伝えることです。
タスク一覧以外のFloatingActionButton
やSnackbar
に関する処理はFragmentに書かなくても問題はまったくないのですが、ユーザ操作をViewModelへ伝える役割をFragmentに集約するためにActivityではなくFragmentで宣言しています。
なお、データバインディングに利用されているTaskdetailFragBinding
はtaskdetail_frag.xmlの内容を基にコンパイル時に自動生成されるクラスです。
/** * Fragment */ class TaskDetailFragment : Fragment() { private var mViewModel: TaskDetailViewModel? = null private lateinit var mSnackbarCallback: Observable.OnPropertyChangedCallback // ViewModelの参照を受け取る fun setViewModel(taskViewModel: TaskDetailViewModel) { mViewModel = taskViewModel } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupFab() setupSnackbar() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // タスク詳細部分のUIを作成する val view = inflater.inflate(R.layout.taskdetail_frag, container, false) // ViewとViewModelをバインドする TaskdetailFragBinding.bind(view).apply { viewmodel = mViewModel } setHasOptionsMenu(true) return view } override fun onDestroy() { mViewModel?.snackbarText?.removeOnPropertyChangedCallback(mSnackbarCallback) super.onDestroy() } ... // FloatingActionButtonの初期設定を行う private fun setupFab() { activity?.findViewById<FloatingActionButton>(R.id.fab_edit_task)?.setOnClickListener { mViewModel?.startEditTask() } } // Snackbarの初期設定を行う private fun setupSnackbar() { mSnackbarCallback = object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(observable: Observable, i: Int) { SnackbarUtils.showSnackbar(view, mViewModel?.getSnackbarText()) } } mViewModel?.snackbarText?.addOnPropertyChangedCallback(mSnackbarCallback) } ... }
Navigatorの役割と実装
TaskDetailNavigator
の役割は、ユーザ操作をFragmentからActivityへと橋渡しすることです。
TODOアプリではActivityがNavigatorの処理を実装しているので、詳細は後述します。
/** * Navigator */ interface TaslDetailNavigator { fun onTaskDeleted() fun onStartEditTask() }
Activityの役割と実装
最後に、Activityの役割と実装を見ていきます。
TaskDetailActivity
の役割は、インスタンスのライフサイクルを管理することです。具体的には、ViewModelのライフサイクルを管理します。
そのため、TaskDetailViewModel
インスタンスはFragmentではなくActivityで生成しています。
詳細は割愛しますが、このこととfindOrCreateViewFragment()
によってViewModelのライフサイクルをActivityと一致させています。
/** * Activity */ class TaskDetailActivity : AppCompatActivity(), TaskDetailNavigator { private lateinit var mTaskViewModel: TaskDetailViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.taskdetail_act) setupToolbar() val taskDetailFragment = findOrCreateViewFragment() mTaskViewModel = findOrCreateViewModel() // ViewModelにNavigator(Activity)の参照を渡す mTaskViewModel.setNavigator(this) // FragmentにViewModelの参照を渡す taskDetailFragment.setViewModel(mTaskViewModel) } override fun onDestroy() { mTaskViewModel.onActivityDestroyed() super.onDestroy() } ... // TaskDetailNavigator#onTaskDeleted()の実装 override fun onTaskDeleted() { setResult(DELETE_RESULT_OK) finish() } // TaskDetailNavigator#onStartEditTask()の実装 override fun onStartEditTask() { val taskId = intent.getStringExtra(EXTRA_TASK_ID) val intent = Intent(this, AddEditTaskActivity::class.java).apply { putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) } startActivityForResult(intent, REQUEST_EDIT_TASK) } ... }
ここまでの話をまとめると以下のようになります。
この図からもViewModelがViewを参照していない、すなわちUIが分離されていることが分かります。
このようにMVVMでは、原則としてViewとViewModelの依存方向を単方向に制限することで、それぞれのクラスの役割を明確にしています。
依存方向の原則に反する処理とその対応
上記の通り、MVVMではViewとViewModelの依存方向を単方向に制限することを原則としていますが、Androidではその原則に反する処理があります。
TODOアプリにおいては、Snackbar
によるメッセージ表示処理がそれに当たります。
この理由として、AndroidフレームワークではToast
やSnackbar
を表示する際にContext
やView
への参照が必要になるためです。
このContext
やView
といったUIに関する情報をViewModelが持つことは原則できません。そのため、TODOアプリでSnackbar
を表示するためには何かしらの形でViewModelからActivityやFragmentへ通知する方法を採らなければなりません。
TODOアプリでは、この課題をデータバインディングライブラリのObservableField
という仕組みを利用して解決しようとしています。
TaskDetailFragment
のソースコードを再掲します。ここでの説明に不要な処理は省略しています。
/** * Fragment */ class TaskDetailFragment : Fragment() { private var mViewModel: TaskDetailViewModel? = null private lateinit var mSnackbarCallback: Observable.OnPropertyChangedCallback override fun onActivityCreated(savedInstanceState: Bundle?) { ... setupSnackbar() } ... override fun onDestroy() { mViewModel?.snackbarText?.removeOnPropertyChangedCallback(mSnackbarCallback) super.onDestroy() } ... // Snackbarの初期設定を行う private fun setupSnackbar() { mSnackbarCallback = object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(observable: Observable, i: Int) { SnackbarUtils.showSnackbar(view, mViewModel?.getSnackbarText()) } } mViewModel?.snackbarText?.addOnPropertyChangedCallback(mSnackbarCallback) } ... }
snackbarText
というプロパティに対してコールバックを設定しています。
このsnackbarText
プロパティはTaskViewModel
が持っているObsevableField
です。このsnackbarText
の値が更新されるとonPropertyChanged(observable, i)
が呼び出されます。
ViewModelで処理が完結せずFragmentでView
を参照してUIを更新することにはなってしまいますが、このような対応が現実的であると考えられます。
MVVMアーキテクチャに関する説明は以上です。
次回の記事ではMVPアーキテクチャを取り扱います。