MVVM(Model-View-ViewModel)は、ViewModelがPresenterを置き換え、Viewとの通信にリアクティブメカニズム(SwiftUIのObservableObject、AndroidのLiveData/StateFlow)を使用するアーキテクチャパターンです。ViewModelはViewへの参照を持ちません。データはサブスクリプションを介して渡されるため、ViewContractインターフェースが不要になり、テストがさらに簡単になります。Appleは2019年からSwiftUIでのMVVMを推奨しており、GoogleはJetpackでのMVVMをAndroidの公式アーキテクチャとして推奨しています。詳細はAndroid Architecture Guideをご覧ください。
重要ポイント
MVVM(Model-View-ViewModel)は、2005年にJohn GossmanがMicrosoftのWindows Presentation Foundation(WPF)向けに記述したアーキテクチャパターンです。ViewModelは画面の状態とビジネスロジックを含む中心的なコンポーネントですが、Viewへの参照は持ちません。データはリアクティブバインディングメカニズムを介して渡されます。ViewはViewModelの変更をサブスクライブし、データが変更されると自動的に再レンダリングされます。
MVVMとMVPの主な違い — ViewContractがないことです。MVPでは、Presenterがview.showUser(data)メソッドを呼び出します。つまり、Presenterが能動的にViewにデータを「プッシュ」します。MVVMでは、View自体がサブスクリプションを介してViewModelからデータを「プル」します。ViewModelは自分にサブスクライバーがいるかどうかを知りません。これにより、切断されたViewの問題が解消されます。回転時にActivityが破棄されても、ViewModelは動作を続け、新しいActivityは単に現在のデータをサブスクライブします。IT Sectrでは、2020年以降すべての新規プロジェクトでMVVMを使用しています。コードの予測可能性が高まり、テストの安定性も向上しました。
| コンポーネント | 責任 | プラットフォーム |
|---|---|---|
| Model | データ、ビジネスロジック、リポジトリ | Android/iOS |
| View | 表示、ViewModelへのサブスクリプション | Activity/Composable、UIView/SwiftUI View |
| ViewModel | 画面状態、ロジック、ナビゲーション | ViewModel(Jetpack)、ObservableObject |
リアクティブバインディング — MVVMの基盤です。Androidでは、LiveData(Jetpackの一部)は監視可能なデータホルダーです。Activityはobserve()を介してサブスクライブします:viewModel.user.observe(this) { user -> binding.name.text = user.name }。userが変更されると、すべてのサブスクライバーが自動的に新しい値を受け取ります。iOSでは、SwiftUIはViewModelの@Publishedプロパティを使用します。変更は自動的にViewを再レンダリングします。これにより、MVPで必要な手動のshowUser/hideLoading呼び出しが不要になります。
JetpackのViewModel — MVVMを実装するためのGoogle公式コンポーネントです。ViewModelは画面回転を生き延びます。構成が変更されると、Activityは破棄されて再作成されますが、ViewModelはメモリに残ります。新しいActivityインスタンスはViewModelProviderを介して同じViewModelを取得します。ViewModelはActivity、Context、Viewへの参照を持ちません。クリーンで、Robolectricなしで単体テストが可能です。
// StateFlowを使用したViewModel — モダンなMVVM実装
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View(Activity)がstateをサブスクライブ
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* ローディングを表示 */
is UserState.Success -> /* データを表示 */
is UserState.Error -> /* エラーを表示 */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData(2017)はJetpack初のリアクティブコンポーネントで、Activityのライフサイクルに最適化されています(onStopで自動サブスクリプション解除)。StateFlow(2021)はKotlin Flowの実装で、ライフサイクルに縛られませんが、lifecycleScopeによる手動のサブスクリプション解除が必要です。StateFlowはcoroutines、concat、map、その他のFlow演算子をサポートしており、LiveDataにはありません。IT Sectrでは、すべての新しいViewModelにStateFlowを使用しています。より短く、より強力で、coroutinesとの統合が優れています。
DataBindingとViewBinding — DataBindingはViewModelをXMLに@{viewModel.user.name}で直接レイアウト内でバインドし、Activityのコードを排除します。ViewBindingはビューにアクセスするための型安全なクラスを生成します。GoogleはシンプルなプロジェクトにはViewBinding、複雑なデータバインディングのプロジェクトにはDataBindingを推奨しています。Jetpack ComposeではDataBindingは不要です。@Composable関数はStateの変更時に自動的に再レンダリングされます。
iOSのMVVMはCombineのObservableObjectを介して実装されます。ViewModelはObservableObjectを継承するクラスで、@Publishedプロパティを持ちます。SwiftUI Viewは@ObservedObjectまたは@StateObjectを介してViewModelをサブスクライブします。@Publishedプロパティが変更されると、SwiftUIはそのプロパティに依存するViewを自動的に再レンダリングします。Appleは2019年のWWDCでSwiftUIをCombineとともに発表しました。それ以来、MVVMはiOSで公式に推奨されるパターンになりました。
import SwiftUI
import Combine
// ViewModel — @Publishedフィールドを持つObservableObject
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View — ViewModelをサブスクライブ
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject vs @ObservedObject — @StateObjectはViewModelを作成し、そのライフサイクルを管理します(Viewの存続期間中に1回)。@ObservedObjectはViewModelが外部で作成され、Viewに渡されます。WWDC 2022は、作成には@StateObject、Views間でのViewModel受け渡しには@ObservedObjectを推奨しています。iOS 17(2023)では、@Observableマクロが登場しました。これによりサブスクリプションが自動化され、@Publishedアノテーションが不要になります。@ObservableはCombineの進化形で、iOS開発をKotlin Flowのリアクティビティに近づけます。
UIKit + MVVM — UIKitプロジェクト(SwiftUIなし)の場合、MVVMはCombineと@Publishedを使用し、UIViewControllerでsink()を介したサブスクリプションで実装されます。ViewModelは同じで、Viewは@Publishedへのサブスクリプションを持つUIViewControllerです。CombineはiOS 13(2019)から利用可能で、システムに組み込まれています。追加の依存関係は必要ありません。Apple Developer Survey(2025)によると、iOSプロジェクトの45%がUIKitでもCombineを使用し、35%がSwiftUI + Combine、20%がRxSwift(レガシー)を使用しています。
MVVMはMVPに勝ります 3つの主要な点で:ViewContractインターフェースの不在、自動サブスクリプション管理、画面回転への耐性。MVPでは、各画面にViewContractインターフェース + Presenterクラス + onStart/onStopでのサブスクリプション/解除が必要です。MVVMでは、ViewModelのみが作成され、Activityでのサブスクリプションは手動のdetach()なしでobserve()を介して行われます。
| 基準 | MVP | MVVM |
|---|---|---|
| ViewContractインターフェース | 画面ごとに1つ | 不要 |
| サブスクリプション管理 | 手動のattach/detach | 自動(lifecycle-aware) |
| 画面回転 | Retain-fragment | ViewModelが回転を生き延びる |
| テスト | Mock ViewContract | 依存関係のないクリーンなクラス |
| リアクティビティ | Presenterのコールバック | LiveData/StateFlow/Combine |
MVVMの欠点 — リアクティブチェーンのデバッグの複雑さと、不適切なサブスクリプションによるメモリリークのリスク。LiveDataはライフサイクルの安全性を解決し、StateFlowはlifecycleScopeを必要とし、CombineはAnyCancellable付きのsinkを必要とします。MVPではすべての呼び出しが明示的(view.showUser)ですが、MVVMではデータはリアクティブストリームを介して届きます。トレースにはsubscribeクロージャでのデバッグブレークポイントが必要です。複数のStateFlowを持つ大規模なViewModelでは、Viewが特定のFlowをサブスクライブしていない場合、UI更新を見逃す可能性があります。
MVPがまだ優れている場合 — 最小AndroidバージョンがAPI 21(Android 5)未満のプロジェクト(Jetpack ViewModelがAndroidXなしでは利用不可)、およびCombineなしの純粋なUIKit(iOS 12以下)を使用するプロジェクト。コードベース全体がすでにMVPであるレガシープロジェクトの場合、MVVMへの完全な移行は常に正当化されるとは限りません。3ヶ月で100画面を書き換えるよりも、ロジックをサービスに段階的に抽出しながらMVPを維持する方が安上がりです。
ViewModelはプラットフォーム依存関係なしで単体テストでテストされます — これがMVVMを支持する主な論拠です。Androidでは、ViewModelはActivity、Context、Viewを含みません。すべての依存関係(Repository、UseCase)はコンストラクタを介して渡され、モックオブジェクトで置き換えられます。iOSでは、ObservableObjectはアプリケーションを起動せずにXCTestを介してテストされ、安定性とテスト実行速度を提供します。
// MockKを使用したAndroid ViewModelの単体テスト
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
iOS ViewModelも同様にテストされます。モックのUserServiceを注入し、loadUserを呼び出し、XCTestExpectationを介して状態を確認します。Combine PublisherはXCTestCaseとwait(for: expectations, timeout: 1.0)でテストされます。UserState構造体は、関連値を持つenumで、操作後の正確な画面状態を確認できます。
コードカバレッジは、MVVMを使用するIT Sectrのプロジェクトでは、ViewModelとRepositoryで75〜90%です。ViewModelは単体テストでカバーされ、Repositoryはテストデータベースを使用した統合テストでカバーされます。SwiftUIとJetpack ComposeのViewは、重要なシナリオに対してUIテスト(XCUITest、Compose Test)でテストされます。残りのUIはスクリーンショットテスト(Snapshot Testing)でチェックされます。これはUIテストよりも高速で、表示の正確性に95%の信頼性を提供します。
よくある質問
MVVMでは、ViewModelはViewへの参照を持ちません。データはリアクティブメカニズム(LiveData、StateFlow、@Published)を介して渡されます。MVPでは、PresenterはViewContractインターフェースを介してViewのメソッドを直接呼び出します。MVVMはViewContractと手動のattach/detachを排除しますが、リアクティブストリームの理解が必要です。ViewModelはAndroidで画面回転を生き延びますが、Presenterはretain-fragmentを必要とします。
最小セット:lifecycle-viewmodel-ktx(ViewModel)、lifecycle-livedata-ktxまたはkotlinx-coroutines-core(StateFlow)。注入にはHiltまたはKoin。非同期操作にはKotlin Coroutines。複雑なデータバインディングにはDataBinding。Jetpack Compose(2022年からGoogle推奨)では、compose-runtimeとlifecycle-viewmodel-composeで十分です。
SwiftUI(2019)はリアクティブアーキテクチャ向けに設計されています。@Stateと@Publishedはデータ変更時に自動的にViewを再レンダリングします。MVVMはSwiftUIに自然に適合します。Viewは@ViewBuilder、ViewModelはObservableObjectです。AppleはMVVMを唯一のパターンとして強制していませんが、2019年以降のすべてのトレーニング資料はViewModel + SwiftUIを使用しています。UIKitの場合、AppleはMVCまたはCoordinatorを推奨しています。
Android:viewModelScopeはViewModelのクリア時に自動的にcoroutinesをキャンセルします。iOS:CombineのAnyCancellableは、保持オブジェクトの解放時に自動的にサブスクリプションを解除します。SwiftUIの@StateObjectはライフサイクルを自動的に管理します。主なルール:ViewModelにView/Contextへの参照を保存しない、クリーンアップ時に長時間実行操作をキャンセルする、クロージャでweak selfを使用する。
StateFlowが現代的な選択です。LiveDataはよりシンプルでライフサイクルセーフですが、StateFlowはより強力です。coroutinesと連携し、flatMap、combine、filterをサポートし、@Nullableアノテーションが不要です。LiveDataが好ましい唯一のシナリオは、StateFlow(Kotlin Flow API)が利用できないJavaコードでの作業です。Googleは新しいKotlinプロジェクトにStateFlowを推奨しています。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。