今回の記事では、MVP(Model-View-Presenter)と呼ばれるアーキテクチャについて説明していきます。
サンプルアプリ
詳細は前回の記事をご参照ください。
MVP(Model-View-Presenter)とは
MVPは、UIの複雑さを解決することを目的としたアーキテクチャです。
MVPの各要素の主な役割は以下の通りです。
- Model
- データ構造を定義する
- View
- UIを定義する
- ユーザ操作やライフサイクルイベントをPresenterへ伝える
- Presenter
- ビジネスロジックを定義する
- Modelからデータを取得し、Viewが使いやすい形でデータを提供する
- 表示するデータを保持する
※最後に書いた「UIに表示するデータはPresenterで保持する」という内容はしばらくの間きちんと意識できていなかった部分なので、自分のためにあえて強調しています。
TODOアプリの実装
ここからはTODOアプリが具体的にどのように実装されているかを見ていきます。
まず、TODOアプリのプロジェクト構成は以下の通りです。このプロジェクトは基本的に画面単位でパッケージ管理されています。
data
パッケージにはタスクを保存するためのリポジトリとインタフェースが格納されています。util
パッケージにはいわゆるユーティリティクラスが格納されています。
com.example.android.architecture.blueprints.todoapp ├ addedittask (追加画面、編集画面) ├ data ├ statistics (統計画面) ├ taskdetail (詳細画面) ├ tasks (トップ画面) └ util
ここからは、例として詳細画面のソースコードを見ていきたいと思います。
taskdetail
パッケージの中身は以下の通りです。
taskdetail ├ TaskDetailActivity ├ TaskDetailContract ├ TaskDetailFragment └ TaskDetailPresenter
この4つのクラスの役割と実装について、以下にまとめていきます。
Activityの役割と実装
まず、Activityの役割と実装を見ていきます。
TaskDetailActivity
の役割は、FragmentとPresenterを生成することです。
/** * Activity */ class TaskDetailActivity : AppCompatActivity() { companion object { val EXTRA_TASK_ID = "TASK_ID" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.taskdetail_act) ... // タスクIDを受け取る val taskId = intent.getStringExtra(EXTRA_TASK_ID) // タスク詳細部分のUIを作成する var taskDetailFragment = supportFragmentManager.findFragmentById(R.id.contentFrame) as? TaskDetailFragment if (taskDetailFragment == null) { taskDetailFragment = TaskDetailFragment.newInstance(taskId) ActivityUtils.addFragmentToActivity( supportFragmentManager, taskDetailFragment, R.id.contentFrame ) } // Presenterを作成する TaskDetailPresenter( taskId, Injection.provideTasksRepository(applicationContext), taskDetailFragment!! ) } ... }
Contractの役割と実装
TaskDetailContract
の役割は、ViewとPresenterを橋渡しすることです。
具体的には、TaskDetailContract.View
とTaskDetailContract.Presenter
という2つのインタフェースによって、TaskDetailFragment
とTaskDetailPresenter
を関連付けます。
- Contract.View
- PresenterがViewを操作して表示状態を変更したい場合に利用する
- Contract.Presenter
- ViewがPresenterにユーザ操作やライフサイクルイベントを伝える場合に利用する
TaskDetailContract.View
にisActive
というプロパティがあります。これはView側の状態、すなわちライフサイクルをチェックするためのプロパティです。
isActive
を利用することによって、Presenterが適切なタイミングでViewの更新を行うことが可能になります。ライフサイクルのチェックを行わない場合、エッジケースでエラーが発生する原因となります。
/** * Contract */ interface TaskDetailContract { // Presenterが呼び出して使う interface View : BaseView<Presenter> { val isActive: Boolean fun setLoadingIndicator(active: Boolean) fun showMissingTask() fun hideTitle() fun showTitle(title: String) fun hideDescription() fun showDescription(description: String) fun showCompletionStatus(complete: Boolean) fun showEditTask(taskId: String) fun showTaskDeleted() fun showTaskMarkedComplete() fun showTaskMarkedActive() } // Viewが呼び出して使う interface Presenter : BasePresenter { fun editTask() fun deleteTask() fun completeTask() fun activateTask() } }
Presenterの役割と実装
次に、Presenterの役割と実装を見ていきます。
TaskDetailPresenter
の役割は、Modelからデータを取得し、Viewが使いやすい形でデータを提供することです。
前述した通り、Contractを経由してViewからユーザ操作やライフサイクルイベントを受け取り、処理を開始します。
class TaskDetailPresenter( private val mTaskId: String, private val mTasksRepository: TasksRepository, private val mTaskDetailView: TaskDetailContract.View ) : TaskDetailContract.Presenter { init { mTaskDetailView.setPresenter(this) } override fun start() { openTask() } private fun openTask() { if (mTaskId.isEmpty()) { mTaskDetailView.showMissingTask() return } mTaskDetailView.setLoadingIndicator(true) mTasksRepository.getTask(mTaskId, object : TasksDataSource.GetTaskCallback { override fun onTaskLoaded(task: Task) { with (mTaskDetailView) { // UIが利用できないタイミングの場合は表示を諦める if (!isActive) { return@onTaskLoaded } setLoadingIndicator(false) } showTask(task) } override fun onDataNotAvailable() { with (mTaskDetailView) { // UIが利用できないタイミングの場合は表示を諦める if (!isActive) { return@onDataNotAvailable } showMissingTask() } } }) } override fun editTask() { with (mTaskDetailView) { if (mTaskId.isEmpty()) { showMissingTask() return } showEditTask(mTaskId) } } ... private fun showTask(task: Task) { val title = task.title val description = task.description with (mTaskDetailView) { if (title.isNullOrEmpty()) { hideTitle() } else { showTitle(title) } if (description.isNullOrEmpty()) { hideDescription() } else { showDescription(description) } showCompletionStatus(task.isCompleted) } } }
Fragmentの役割と実装
最後に、Fragmentの役割と実装を見ていきます。
TaskDetailFragment
の役割は、UIの初期設定とユーザ操作やライフサイクルイベントをPresenterへ伝えることです。
MVVMの場合と同様、タスク一覧以外のFloatingActionButton
やSnackbar
に関する処理はFragmentに書かなくても問題はまったくないのですが、ユーザ操作やライフサイクルイベントをPresenterへ伝える役割をFragmentに集約するためにActivityではなくFragmentで宣言しています。
class TaskDetailFragment : Fragment(), TaskDetailContract.View { companion object { private val ARGUMENT_TASK_ID = "TASK_ID" private val REQUEST_EDIT_TASK = 1 fun newInstance(taskId: String?): TaskDetailFragment { return TaskDetailFragment().apply { arguments = Bundle().apply { putString(ARGUMENT_TASK_ID, taskId) } } } } private var mPresenter: TaskDetailContract.Presenter? = null override fun setPresenter(presenter: TaskDetailContract.Presenter) { mPresenter = presenter } override val isActive: Boolean get() = isAdded override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.taskdetail_frag, container, false) // FloatingActionButtonの初期設定を行う activity?.findViewById<FloatingActionButton>(R.id.fab_edit_task)?.setOnClickListener { mPresenter?.editTask() } setHasOptionsMenu(true) return root } override fun onResume() { super.onResume() mPresenter?.start() } override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { inflater?.inflate(R.menu.taskdetail_fragment_menu, menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_delete -> { mPresenter?.deleteTask() return true } } return false } ... override fun showEditTask(taskId: String) { val intent = Intent(context, AddEditTaskActivity::class.java).apply { putExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId) } startActivityForResult(intent, REQUEST_EDIT_TASK) } ... override fun showTaskMarkedComplete() { view?.let { Snackbar.make(it, getString(R.string.task_marked_complete), Snackbar.LENGTH_LONG).show() } } override fun showTaskMarkedActive() { view?.let { Snackbar.make(it, getString(R.string.task_marked_active), Snackbar.LENGTH_LONG).show() } } ... }
ここまでの話をまとめると以下のようになります。
バリデーションチェックなどが多いアプリになるとView / Presenter間の往来が多くなり、責務がだんだん曖昧になってきそうだと感じました。
サンプルアプリでは編集画面へ遷移する際にView→Presenter→Viewと処理が往来していますが、気を抜くとView側で画面遷移の処理を直接呼びたくなってしまいそうです(もちろんそれで問題ないケースもありますが)。
実務でもMVPをベースとしたアーキテクチャを採用しているので、Viewの責務は表示、Presenterの責務はビジネスロジックということを改めて意識ながら実装していきたいと思います。
MVPアーキテクチャに関する説明は以上です。
追記
この記事はtodo-mvp
ブランチにあるJavaのソースコードを手元でKotlinに書き換えながら書いていたのですが、todo-mvp-kotlin
というブランチにKotlinで書かれたソースコードがそもそもありました。笑
github.com
内容としては、Nullチェックが減っていたりユーティリティクラスが拡張関数になっている程度で、記事に記載したものとも大きな乖離はありませんでしたが、気になる方はこちらもご参照いただければ幸いです🙇♂️