ArchitectureComponentsのViewModelとDataBindingを使う
触るがてら既存のDataBinding + MVVMなアプリにAACのViewModelを入れてみました。対象は結構前にKotlinで書いたアプリ。
GitHub - rei-m/HBFav_material: HBFav for Android with kotlin
2018/07/26 更新 こっちが自分なりの最新の設計なのでこっち見たほうがいいです。 GitHub - rei-m/android_hyakuninisshu
AACのViewModel
- Activity/Fragmentとは別のライフサイクルをもったDataHolderの模様
- 説明ざざっと読んだ感じ
onSavedInstanceState
いらないのか!と思ったもののそうではなく、普通にActivity保持しないとかにすると消し飛ぶので、自分が死ぬタイミングがわかっていて、Activity/Fragmentとは別のライフサイクルをもったModelであると解釈
書いてみる
Model層
- 既存の作りと変わらずにObservableなプロパティを公開しているアプリケーションスコープなModelとして作っています。
ViewModel層
- 既存のDataBindingに食わせていたViewModelは基底クラスにライフサイクルメソッドを生やして、Activity/Fragmenntからそれを呼んでModelの購読/解除をしていました。ここを変えてAACのViewModelを継承するようにします。DataBindingに公開するObservableFieldは変えていません。
自分のブックマーク表示する画面のVM / UserBookmarkFragmentViewModel.kt
class UserBookmarkFragmentViewModel(private val userBookmarkModel: UserBookmarkModel, userModel: UserModel, readAfterFilter: ReadAfterFilter) : ViewModel() { 略) init { userId.addOnPropertyChangedCallback(userIdChangedCallback) hasNextPage.addOnPropertyChangedCallback(hasNextPageChangedCallback) this.readAfterFilter.addOnPropertyChangedCallback(readAfterFilterChangedCallback) disposable.addAll(userBookmarkModel.bookmarkList.subscribe { if (it.isEmpty()) { bookmarkList.clear() } else { bookmarkList.addAll(it - bookmarkList) } isVisibleEmpty.set(bookmarkList.isEmpty()) }, userBookmarkModel.hasNextPage.subscribe { hasNextPage.set(it) }, userBookmarkModel.isLoading.subscribe { isVisibleProgress.set(it) }, userBookmarkModel.isRefreshing.subscribe { isRefreshing.set(it) }, userBookmarkModel.isRaisedError.subscribe { isVisibleError.set(it) }, userModel.user.subscribe { userId.set(it.id) }) } override fun onCleared() { userId.removeOnPropertyChangedCallback(userIdChangedCallback) hasNextPage.removeOnPropertyChangedCallback(hasNextPageChangedCallback) readAfterFilter.removeOnPropertyChangedCallback(readAfterFilterChangedCallback) disposable.dispose() super.onCleared() } 略)
- 自分が破棄されるタイミングがわかるようになったので、生成時にModelのプロパティの購読開始。onClearedで購読解除するようにしました。ここではuserModel.userの購読開始するとBehaviorSubjectから値が流れてくる -> ViewModelがUserIdの変更を検知してブックマークとる命令を出す、という感じでViewModel生成時にバックグラウンドでデータを取りに行っています。
ViewModelとFactory
- ViewModelにBundleから取り出した引数を与えたいというような場合、
ViewModelProviders
にFactoryを与えることで、ViewModel生成時の処理に介入できるようになります。FactoryはDagger経由でDIするようにしてます。
viewModel = ViewModelProviders.of(this, viewModelFactory).get(OthersBookmarkFragmentViewModel::class.java)
他人のブックマーク表示する画面のVM / OthersBookmarkFragmentViewModel.kt
略) // ViewModelProvidersに渡すViewModelを提供するFactory class Factory(private val userBookmarkModel: UserBookmarkModel, private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(OthersBookmarkFragmentViewModel::class.java)) { return OthersBookmarkFragmentViewModel(userBookmarkModel, userId) as T } throw IllegalArgumentException("Unknown class name") } }
Factoryを提供するModule / OthersBookmarkFragmentViewModelModule.kt
@Module class OthersBookmarkFragmentViewModelModule(private val userId: String) { @Provides @ForFragment fun provideViewModelFactory(@Named("othersUserBookmarkModel") userBookmarkModel: UserBookmarkModel): OthersBookmarkFragmentViewModel.Factory = OthersBookmarkFragmentViewModel.Factory(userBookmarkModel, userId) }
- userBookmarkModelはDaggerからDIされますが対象のユーザーIDはModule生成時に設定してあげる必要があります。
他人のブックマーク表示する画面 / OthersBookmarkFragment.kt
略) @dagger.Subcomponent(modules = arrayOf( OthersBookmarkFragmentViewModelModule::class, BookmarkListItemViewModelModule::class)) interface Subcomponent : AndroidInjector<OthersBookmarkFragment> { @dagger.Subcomponent.Builder abstract class Builder : AndroidInjector.Builder<OthersBookmarkFragment>() { abstract fun viewModelModule(module: OthersBookmarkFragmentViewModelModule): Builder override fun seedInstance(instance: OthersBookmarkFragment) { viewModelModule(OthersBookmarkFragmentViewModelModule(instance.userId)) } } } @dagger.Module(subcomponents = arrayOf(Subcomponent::class)) abstract inner class Module { @Binds @IntoMap @FragmentKey(OthersBookmarkFragment::class) internal abstract fun bind(builder: Subcomponent.Builder): AndroidInjector.Factory<out Fragment> } }
- Fragment内のinner classとしてSubComponentを定義してその中のseedInstance内でFragmentのインスタンスからuserIdを取り出してModuleを作成しています。
onSavedInstanceState
から値を復元したい場合はDIされたviewModelFactoryに取り出した値をセットしてからViewModelProviderに渡すようにしました。
書いてみて
- 毎回Activity/Fragmentのライフサイクルで
viewModel.onXXXX
を呼んでたのでそれがなくなったのはだいぶ楽 - こういう使い方でいいのか、あるいはDataBindingのViewModelとは別のレイヤーとして考えるべきなのか?とか一人で書いてるとどっちがいいのかよくわからんという感じになったのでトークしたい。