MVP (Model-View-Presenter) — PresenterがViewContractインターフェースを介してModelとViewの間の仲介役となるアーキテクチャパターンです。ControllerがUIKitを介してViewを直接管理するMVCとは異なり、Presenterはフレームワークに依存しません — 抽象化を通じて動作するため、Android SDKやUIKitなしでテスト可能です。MVPはJetpack登場前のAndroid開発で広く使用され、レガシープロジェクトで今も重要です。詳細はマーティン・ファウラーの記事をご覧ください。
ポイント
MVP (Model-View-Presenter) — マーティン・ファウラーが2000年代初頭にユーザーインターフェースのテスト容易性を向上させるためにMVCの進化形として提案したアーキテクチャパターンです。Modelはデータとビジネスロジックを管理し、Viewはレンダリングとユーザー入力の処理を担当し、PresenterはViewからイベントを受け取り、Modelからデータを取得し、表示用の状態を形成する中心的なコンポーネントです。
MVPとMVCの主な違い — PresenterはViewへの直接参照を持ちません。代わりに、PresenterはViewContractインターフェースを介してViewとやり取りします。Viewはこのインターフェースを実装し、自身をPresenterに渡します。これによりUIKit(iOS)やAndroid Frameworkへの依存が断ち切られ — PresenterはViewContractのモック実装で隔離してテストできます。MVCではUIViewControllerコントローラーが直接UILabelを更新しますが、MVPではPresenterがview.showName(name)を呼び出し、Viewが表示方法を決定します。
| コンポーネント | 責任 | テスト容易性 |
|---|---|---|
| Model | データ、ビジネスロジック、ネットワーク呼び出し | 単体テスト(UIに依存しない) |
| View | UIレンダリング、Presenterへのイベント転送 | インターフェース経由のモック実装 |
| Presenter | ビジネスロジック、状態管理、ナビゲーション | 単体テスト(ViewContractモック経由) |
単一責任の原則はMVPでMVCよりも厳密に守られます。Viewはレンダリングのみ、Modelはデータのみ、Presenterはロジックと調整のみを担当します。実際のプロジェクトでは、Presenterが画面コードの40〜60%、Viewが20〜30%、Modelが20〜30%を占めます。この分散により、AndroidエミュレーターやiOSシミュレーターを起動せずに主要なビジネスロジックをテストできます。
AndroidのMVP では、ActivityまたはFragmentがViewとして機能し、データ表示メソッドを持つインターフェースであるViewContractを実装します。PresenterはActivity内で作成され、Viewを自身にアタッチしてデータ読み込みを管理します。画面が回転するとActivityは再作成されます — Presenterはretainフラグメントや外部ストレージを通じて保持でき、純粋なMVCに特徴的な状態喪失問題を解決します。
// ViewContract — PresenterとViewを接続するインターフェース
interface UserView {
fun showLoading()
fun hideLoading()
fun showUser(user: User)
fun showError(message: String)
}
// Presenter — テスト可能なロジック層
class UserPresenter(
private val repository: UserRepository
) {
private var view: UserView? = null
fun attachView(view: UserView) {
this.view = view
}
fun detachView() {
view = null
}
fun loadUser(userId: Int) {
view?.showLoading()
repository.getUser(userId) { result ->
view?.hideLoading()
result.onSuccess { user ->
view?.showUser(user)
}.onFailure { e ->
view?.showError(e.message ?: "Unknown error")
}
}
}
}
// View(Activity)がインターフェースを実装
class UserActivity : AppCompatActivity(), UserView {
private val presenter = UserPresenter(UserRepository())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
presenter.loadUser(42)
}
override fun onDestroy() {
presenter.detachView()
super.onDestroy()
}
override fun showUser(user: User) { /* UIを更新 */ }
override fun showLoading() { /* ProgressBarを表示 */ }
override fun hideLoading() { /* ProgressBarを非表示 */ }
override fun showError(message: String) { /* Snackbarを表示 */ }
}
ライフサイクル管理 — AndroidにおけるMVPの主要な問題です。画面回転時にActivityは破棄され、presenter.attachView()がonCreate()で再度呼び出されます。データ読み込みが非同期(RxJava、コルーチン)の場合、完了時にViewがデタッチされている可能性があります。解決策 — detachView()でサブスクリプションをキャンセルするか、Support LibraryのLoaderを使用します(Jetpackなしのプロジェクト向け)。IT Sectrでは商業プロジェクトで長年MVP + RxJavaの組み合わせを使用してきました — パターンは安定していますが、サブスクリプション管理に規律が必要です。
Retainフラグメント — 画面回転時にPresenterを保持するメカニズムです。UIなしのフラグメント(setRetainInstance(true))はActivityより長く生存し、Presenterへの参照を保持します。Activityが再作成されると、フラグメントは同じPresenterを新しいActivityに渡します。RetainフラグメントはAndroidX以降非推奨ですが、pre-Jetpack版(Fragment.setRetainInstance)はレガシープロジェクトで今も動作します。現代の開発では、Googleはretainフラグメントの代わりにViewModelを推奨しています。
iOSのMVP はViewプロトコルを通じて構築されます。UIViewControllerがプロトコルを実装し、PresenterはUIKitをインポートせず、純粋にテスト可能です。UIViewController自体がロジックと直接のIBOutlet接続を含むApple MVCとは異なり、Presenterは状態を管理し、プロトコルメソッドを通じてViewに指示を出します。Viewは決定を行わず — showUser、showLoading、navigateToProfileなどのPresenterのコマンドを実行します。
import Foundation
// View Protocol — Presenterの抽象化
protocol UserViewProtocol: AnyObject {
func showLoading()
func hideLoading()
func display(user: User)
func displayError(message: String)
}
// Presenter — UIKitなしの純粋ロジック
final class UserPresenter {
private weak var view: UserViewProtocol?
private let service: UserServiceProtocol
init(service: UserServiceProtocol) {
self.service = service
}
func attach(view: UserViewProtocol) {
self.view = view
}
func detach() {
view = nil
}
func loadUser(id: Int) {
view?.showLoading()
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
self.view?.hideLoading()
switch result {
case .success(let user):
self.view?.display(user: user)
case .failure(let error):
self.view?.displayError(message: error.localizedDescription)
}
}
}
}
// View(UIViewController)がプロトコルを実装
final class UserViewController: UIViewController, UserViewProtocol {
private let presenter = UserPresenter(service: UserService())
override func viewDidLoad() {
super.viewDidLoad()
presenter.attach(view: self)
presenter.loadUser(id: 42)
}
func display(user: User) {
nameLabel.text = user.name
}
// ... プロトコルの残りのメソッド
}
Weak reference — iOS MVPではViewへの弱参照が必須です。UIViewControllerは(ナビゲーションスタックからのpopで)破棄される可能性があり、Presenter内のクロージャーがretain cycleを引き起こします。弱参照(weak var)により、Presenter内の非同期操作に関係なく、Viewが画面から離れると解放されることが保証されます。Androidでは同様の問題がdetachView()で解決されます — onDestroy()での呼び出しによりViewへの参照が無効化されます。
Passive View vs Supervising Controller — マーティン・ファウラーによるMVPの2つのバリエーション。Passive View: Viewはロジックを持たず、Presenterが完全に状態を管理します。Supervising Controller: View自体が単純なデータバインディング(例:data binding)を行い、Presenterは複雑なシナリオでのみ介入します。モバイル開発ではPassive Viewがより頻繁に使用され — 最大のテスト容易性と画面状態の予測可能性を提供します。
主な違いは、MVPとMVCのViewとの通信方法です。MVCではControllerがView(UIViewController.IBOutlets、Activity.findViewById)への直接参照を持ちます。MVPではPresenterがViewContractインターフェースを介してViewとやり取りします。この違いがテスト容易性を根本的に変えます:ViewContractを実装するモックオブジェクトにより、アプリ、エミュレーター、UIフレームワークを起動せずにPresenterのロジックをテストできます。
| 基準 | MVC | MVP |
|---|---|---|
| Viewとの接続 | 直接(Controller → View) | インターフェース経由(Presenter → ViewContract) |
| ロジックテスト | UIKit/Android Frameworkが必要 | プラットフォーム依存のない単体テスト |
| ライフサイクル | Controllerは画面とともに生存 | Presenterはより長く生存可能(retain) |
| 複雑さ | 最小 | 画面ごとに+1インターフェース |
| Massive Controller | 典型的な問題 | ロジックはPresenter、Viewは薄い |
Presenterの単体テストの例 Kotlin: mock UserViewを作成し、Presenterに渡し、loadUserを呼び出し、showUserが正しいデータで呼び出されたことを確認します。テストはミリ秒で実行され、エミュレーターは不要です。iOSでも同様に — OCMockまたはプロトコルスタブがUserViewProtocolメソッド呼び出しを検証します。IT SectrのMVPプロジェクトでは、ビジネスロジックの単体テストカバレッジが85〜90%に達し、同様のMVCプロジェクトより2〜3倍高くなりました。
MVPがMVCより好ましい場合 — 安定性に厳しい要件があるプロジェクト:銀行アプリ、医療システム、決済端末。これらの分野ではエラーのコストが高く、単体テストが重要です。ポストMVPプロジェクト(製品が既に市場に出ているがコードベースがレガシーな場合)では、MVPは完全なアーキテクチャ書き換えなしにMassive View Controllerからテスト可能な層へロジックを段階的に抽出できます。
MVPの主な欠点 — インターフェース数の増加と手動サブスクリプション管理。各画面に少なくとも1つのViewContract + Presenterが必要で、50画面で50のインターフェースと50のPresenterクラスになります。MVVMでは、ViewModelがPresenterを置き換え、リアクティブメカニズム(LiveData、StateFlow、ObservableObject)を使用することで、手動のattach/detachやViewContractインターフェースが不要になります。
RxJavaとMVP — Android 2015〜2019で人気の組み合わせ。PresenterがRepositoryのObservableにサブスクライブし、ViewContractを通じて結果を表示します。問題:disposableをdetachView()で明示的にキャンセルする必要があり、そうしないとサブスクリプションリークがデタッチされたViewの更新時にクラッシュを引き起こします。RxLifecycleとAutoDisposeライブラリが部分的にサブスクリプション解除を自動化しましたが、依存関係が追加されました。IT Sectrでは2020年にMVP+RxJavaからMVVM+Flowに移行しました — ViewContractの廃止によりコードが25〜30%短くなりました。
MVPからMVVMへの移行 — 段階的なプロセスです。1) PresenterのViewContractをLiveData/StateFlowに置き換える。2) attach/detachメソッドを削除 — サブスクリプションはobserve()を通じて行う。3) PresenterをViewModelに名前変更。4) ViewModelFactory用にDI(Hilt/Koin)を統合。1画面の移行に2〜4時間、50〜100画面のプロジェクト全体で2〜4週間かかります。移行後、ViewContractインターフェースは削除され、コードは縮小し、テストは維持されます。
現代の開発におけるMVP — パターンは生きていますが、MVVMやMVIに譲っています。Googleは新規プロジェクトにJetpack + MVVMを公式に推奨しています。AppleはSwiftUI + MVVMです。しかし、レガシーコードを扱うにはMVPの知識が必須です:Google Playの何百ものAndroidアプリが今もMVPで動作しており、大手銀行、小売業者、運輸会社のアプリを含みます。MVPの理解はMVIとClean Architectureを習得する基盤であり、Presenterはロバート・マーティンの用語でUse Caseの直接の前身です。
よくある質問
MVPでは、PresenterはViewContractインターフェースを介してViewとやり取りし、直接ではありません。MVCでは、ControllerはIBOutlet/findViewByIdを通じてViewへの直接参照を持ちます。MVPはPresenterがUIKitやAndroid Frameworkに依存しないため、iOS SimulatorやAndroid Emulatorなしで単体テストでPresenterをテストできます。MVCではコントローラーのテストにアプリの起動が必要です。
MVPは、既にこのパターンで構築されたレガシープロジェクトや、リアクティブメカニズム(LiveData、StateFlow、Combine)をサポートしていないアプリで正当化されます。新規プロジェクトでは、GoogleはJetpack + MVVM(Android)、AppleはSwiftUI + MVVM(iOS)を推奨しています。MVPは、Combineなしの純粋なUIKitプロジェクトでビジネスロジックの単体テストが必要な場合に最適な選択肢です。
Android — retainフラグメント(setRetainInstance(true))またはJetpackのViewModelを使用します。Retainフラグメントは回転時にPresenterを保存し、新しいActivityに渡します。GoogleのViewModelは、retainフラグメントなしで回転時に自動的に状態を保持する現代的な代替手段です。iOS — PresenterはviewDidLoadごとに再作成されますが、別のコーディネーターサービスにキャッシュされます。
最低4つ:ViewContractインターフェース、ViewContract実装(Activity/Fragment)、Presenter、Model(Repository)。Dagger/Hiltを使用する場合はDIモジュールが追加されます。50画面で200以上のクラスになります。MVVMは画面ごとに1ファイル削減(ViewContract不要)、MVIはStateクラスとIntentクラスを追加します。クラス数は大規模プロジェクトでのMVP反対の主な論拠です。
Passive View — Viewはロジックを持たず、Presenterが状態とデータを完全に管理します。Supervising Controller — View自体が単純なバインディング(data binding)を行い、Presenterは複雑なシナリオでのみ介入します。モバイル開発ではPassive Viewが優勢で — 最大のテスト容易性と予測可能性を提供します。Supervising ControllerはWebフレームワーク(ASP.NET Web Forms、GWT)で使用されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。