Android開発において、開発者が長年直面してきた最大のボトルネックは、OS主導のライフサイクル管理と、それに伴う状態保持の複雑さです。特に、画面回転やシステムによるプロセス破棄が発生した際、非同期処理の結果が失われたり、UI参照が切れてクラッシュしたりする問題は日常茶飯事でした。実際、私が担当していた月間アクティブユーザー(MAU)50万人のECアプリでは、以下のようなスタックトレースがCrashlyticsのトップを独占していました。
God Class症候群とライフサイクルの罠
当時のプロジェクト環境は、Android 12ターゲット、Kotlin 1.6系を使用していましたが、アーキテクチャは古いMVC(Model-View-Controller)パターンを引きずっていました。ActivityやFragmentが「UIの描画責任」と「データ管理責任」の両方を負う、いわゆるGod Class(神クラス)と化していたのです。
具体的な症状としては、ユーザーが決済画面で「購入」ボタンを押した直後に画面を回転させると、APIレスポンスが返ってきたタイミングでActivityが既に破棄されており、再生成された新しいActivityには結果が渡らない、あるいは最悪の場合ヌルポインタ参照(NPE)でクラッシュするという事象でした。
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
または、非同期コールバック内でのView.findViewById(...) must not be null
本来、私たちはAndroid Architecture Components (AAC)のガイドラインに従うべきですが、レガシーコードの負債により、ライフサイクルメソッド(onCreate, onStop, onDestroy)の中にビジネスロジックが散乱し、メモリリークの温床となっていました。
失敗したアプローチ:onSaveInstanceStateへの過信
最初に試みた修正は、onSaveInstanceStateを活用して全てのデータをBundleに詰め込むことでした。しかし、このアプローチはすぐに破綻しました。
Bundleには容量制限(推奨50KB以下、最大でも1MB未満)があり、大量の商品リストや高解像度の画像データをシリアライズして保存しようとすると、TransactionTooLargeExceptionが発生しました。さらに、非同期処理(API通信)の状態そのものはBundleでは保存できないため、通信中に回転すると「ローディング表示が消えない」といったUI不整合が発生しました。これは根本的なアーキテクチャの欠陥であり、パッチワーク的な修正では対応できないことが明白でした。
ViewModelとStateFlowによるMVVMの実装
解決策は、UIコントローラー(Activity/Fragment)からデータ保持の責任を完全に分離することです。ここでは、AACの中核であるViewModelと、Kotlinの最新のリアクティブストリームであるStateFlowを組み合わせた実装を紹介します。
// 依存関係: build.gradle (App)
// implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
// implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
// UIの状態を表現するSealed Class(推奨)
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Product>) : UiState()
data class Error(val message: String) : UiState()
}
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
// Backing propertyによるカプセル化
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
fetchProducts()
}
fun fetchProducts() {
// viewModelScopeはViewModelのクリア時に自動的にキャンセルされる
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val result = repository.getProducts()
_uiState.value = UiState.Success(result)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown Error")
}
}
}
// ViewModelはActivity再生成後も生き続けるため、
// ここで保持したデータは回転後も即座に利用可能
}
上記のコードで重要なのは、viewModelScopeの使用とStateFlowによる状態管理です。viewModelScopeを使用することで、ViewModelが破棄される(ユーザーが画面を閉じる)際に自動的にコルーチンがキャンセルされ、メモリリークを防ぎます。また、StateFlowは初期値を持ち、常に最新の状態を保持するため、画面回転後の再講読時にも即座にデータをUIに反映できます。
Activity側の実装(Lifecycle-Aware)
Activity側では、ロジックを持たず、ViewModelの状態を「監視(Observe)」するだけに留めます。これにより、Activityは純粋なViewとしての役割に集中できます。
class ProductActivity : AppCompatActivity() {
// ‘by viewModels()’ delegateを使用(要Fragment KTX)
private val viewModel: ProductViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_product)
// repeatOnLifecycleを使用することで、バックグラウンド時の無駄な更新を停止
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UiState.Loading -> showProgressBar()
is UiState.Success -> updateList(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
}
}
ここでrepeatOnLifecycleを使用している点がパフォーマンス上の鍵です。アプリがバックグラウンドに回った際、古いLiveDataでは監視が継続されるケースがありましたが、このAPIを使用することで、UIが表示されていない間のリソース消費を完全にカットできます。
| 指標 | レガシー実装 (MVC/Callback) | モダン実装 (MVVM/AAC) |
|---|---|---|
| 画面回転時のクラッシュ率 | 2.4% | 0.05%未満 |
| Activityの行数 | 850行以上 | 220行 |
| メモリリーク発生件数 | 頻発 (Context保持) | ほぼゼロ |
| テスタビリティ | 困難 (Android依存) | 容易 (JUnitのみで可) |
導入後の成果は劇的でした。上記の表が示す通り、最も重要だったのはクラッシュ率の激減です。ViewModelがライフサイクルのオーナーシップを適切に分離したことで、Activityが破棄されてもデータ取得プロセスは継続し、再生成されたActivityが「結果だけを受け取る」構造が確立されました。これにより、ユーザー体験(UX)が大幅に向上しました。
公式ドキュメント: StateFlowとSharedFlowの詳細注意点:システムによるプロセス死への対応
ViewModelは「構成変更(Configuration Change)」には耐えられますが、「システムによるプロセス破棄(Low Memory Killer)」には耐えられません。アプリがバックグラウンドに長時間置かれ、OSによってプロセスごとキルされた場合、ViewModelのメモリ内データも消滅します。
SavedStateHandleをViewModelのコンストラクタに注入することで、最低限のIDや検索クエリなどを永続化し、プロセス復帰時にそのIDを使ってリポジトリからデータを再取得するロジックを組むのが、エンタープライズ品質のAndroidアプリにおける鉄則です。
結論
Android Architecture Components (AAC) の導入は、単なるコードの整理整頓ではありません。それは、Activityのライフサイクルという「制御不能な波」からビジネスロジックを守るための防波堤です。ViewModelとStateFlow、そしてCoroutinesを適切に組み合わせることで、開発者はボイラープレートコードとの戦いから解放され、本質的な機能開発に集中できるようになります。もし現在もGod Class化したActivityと格闘しているのであれば、まずは小さな画面からViewModelへの移行を試みることを強く推奨します。
Post a Comment