はじめまして。Misocaモバイルチームのtijinsです。
この記事は、弥生アドベントカレンダー14日目の記事です。
MVPからMVVMへ
Android版MisocaはModel-View-Presenter構造で作られていたのですが、Model-View-ViewModel構造へのリファクタリングが完了しました。
コードがスッキリしたので、リファクタリングの内容を紹介します。
MVPパターンとMVVMパターン
Model-View-Presenter(MVPパターン)
MVPパターンでは、PresenterがViewの参照(intrefaceで)を保持し、結果をViewに表示します。
Viewを直接参照せずにInterfaceにしておくことで、PresenterからViewの依存を排除しています。
classDiagram class PresenterResult class View class Presenter class Model <<interface>> PresenterResult View --|> PresenterResult View --> Presenter: 読み込み・新規作成等の操作 Presenter --> PresenterResult : 結果の表示 Presenter --> Model
Model-View-ViewModel(MVVMパターン)
MVVMパターンでは、ViewModelからViewの参照は不要です。
表示用のデータはViewModelが保持しており、ViewがViewModel上のデータを監視(Observe)して表示します。
classDiagram class View class ViewModel class Model View --> ViewModel: ViewModel上のデータを監視(変更があれば表示を更新する) ViewModel --> Model:操作結果により、ViewModel上のデータを更新する
ViewModel化のメリット
ViewModelにも色々あるみたいなのですが、ここでのViewModelはデータを保持できるPresenterといった感じです。
PresenterからViewの参照を排除できる
ViewModel化前は、Presenterに非同期処理完了のコールバックとして、Viewの参照を(interfaceとしてですが)渡していました。
ViewModelを利用すると、ViewModelからViewへの参照は不要になります。
AndroidアプリでのViewModelのメリット
- FragmentでもActivityライフサイクルでのデータ保持が可能
Fragmentにデータを保持していると、画面の回転等でFragmentが破棄された際に、データの読み込みが必要でした。
ActivityライフサイクルのViewModelを利用する事で、Fragmentが破棄されても、データの引き継ぎが可能になります。
- ライフサイクル管理の単純化
Presenterが行う非同期処理の結果をFragmentで表示する場合、先にFragmentが破棄されていると、クラッシュしてしまう問題がありました。
ViewModelのライフサイクルはFragmentから分離されている為、Fragmentの状態を気にせずに、非同期処理が可能です。
リファクタリング前のコード
最初にリファクタリング前のコードです。
Viewを直接参照しないのは、PresenterからViewの依存を排除する為です。
こうしておくと、テスト用のダミークラスに差し替えたりできます。
classDiagram class ItemsResult{ onItemLoaded(items) onFailed(message) } class ItemsFragment{ items:List<Item> } class ItemsPresenter{ loadItems(page) } class UseCase class Repository <<interface>> ItemsResult ItemsFragment --|> ItemsResult ItemsFragment --> ItemsPresenter: itemsの読み込み操作 ItemsPresenter --> ItemsResult : 結果(items) ItemsPresenter --> UseCase UseCase --> Repository
ItemsViewInterface.kt
interface ItemsResult { fun onItemsLoaded(items:List<Item>) fun onFailed(message:String) }
ItemsPresenter.kt
class ItemsPresenter( private val context:Context, private val coroutineScope: CoroutineScope, private val view: ItemsResult) { private val useCase = ItemsUseCase() fun loadItems(page:Int) { coroutineScope.launch{ try { val result = useCase.loadItems(context, page) view.onLoadItems(result) } catch (ex: Exception) { view.onFailed(ex.message) } } } }
ItemsFragment.kt
class ItemsFragment:Fragment,ItemsResult { // 請求書一覧のアダプターです private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>? private var presenter: ItemsPresenter? override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter = ItemsPresenter(requireContext(), viewLifecycleOwner.lifecycleScope, this) presenter?.loadItems(1) } override fun onItemsLoaded(items:List<Item>){ adapter?.submitList(items) } override fun onFailed(message: String){ // エラー表示する } }
リファクタリング後のコード
ViewModelの実装
ViewModelからFragmentへの参照が無くなり、すっきりしていると思います。
classDiagram class ItemsViewModel{ - itemsBuffer:List<Item> + items:MutableLiveData<List<Item>> + networkState:MutableLiveData<NetworkState> laodItems(page) } class Fragment class UseCase class Repository Fragment --> ItemsViewModel : items<LiveData>をobserveして、アイテム一覧の変更を監視 ItemsViewModel --> UseCase UseCase --> Repository
ItemsViewModel.kt
MisocaのAndroid版では、APIとの通信にContextが必要な為、AndroidViewModel
を継承しています。
AndroidViewModel
を継承することで、ApplicationContextをgetApplication()
で参照可能になります。
ただし、ApplicationContextには言語設定や画面向きの変更が反映されない為、ViewModel内でUIに関する処理を行わないよう注意が必要です。
class ItemsViewModel(context: Context) : AndroidViewModel(context.applicationContext as Application){ // 通信中状態、エラーの通知用のobserve可能なプロパティ val networkState = MutableLiveData<NetworkState>() // 表示するデータの実体(LiveData経由でFragmentに渡す) private val itemsBuffer = ArrayList<Item>() // observe可能なプロパティ val items = MutableLiveData<List<Item>>() // データの一覧を読み込みます fun loadItems(page: Int) { viewModelScope.launch { try { networkState.postValue(NetworkState.LOADING) // loadItemsはDispatchers.IOで動作するsuspend functionです。 val result = useCase.loadItems(getApplication(), page) itemsBuffer.addAll(result) // observeしているViewに更新を通知する items.postValue(itemsBuffer) networkState.postValue(NetworkState.LOADED) } catch (ex: Exception) { // エラーメッセージを表示 networkState.postValue(NetworkState.error(ex.message)) } } } }
NetworkStateの実装は、下記のコードを参考にしました。
https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample
ViewModel上のデータ変更を監視する
ViewModelをFragmentから使う場合は、fragment-ktxにあるExtension(viewModels
,activityViewModels
)を使用すると便利です。
build.gradle
dependencies { implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion" }
ItemsFragment.kt
class ItemsFragment:Fragment { // fragment-ktxのactivityViewModelsでviewModelを取得します private val viewModel: ItemsViewModel by activityViewModels() // 請求書一覧のアダプターです private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>? override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) observeViewModel() // itemsの読み込みを開始します viewModel.loadItems(1) } private fun observeViewModel() { // itemsの更新を監視します viewModel.items.observe(viewLifecycleOwner) { items -> // List<Item>をそのまま通知しても、DiffUtilが効率的に再描画してくれるので気にしない adapter?.submitList(items) } // 通信中状態の更新を監視します viewModel.networkState.observe(viewLifecycleOwner) { when (it.status) { Status.RUNNING ->{ // プログレスバーを表示 } Status.FAILED -> { // エラー表示 } Status.Success -> { // プログレスバーを消す } } } } }
おまけ
この画面は2つのタブがそれぞれFragmentになっているのですが、ViewModelを通じて2つのタブがデータを共有しています。
タブ間(Fragment間)でのデータの移動も、ViewModelを使用する事で、簡単に実現できています。
まとめ
Viewの操作が不要になって、スッキリしました
宣伝
Misoca 開発チームでは、モバイルアプリエンジニア(iOS, Android)を募集しています。