もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

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

developer.android.com

  • 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とは別のレイヤーとして考えるべきなのか?とか一人で書いてるとどっちがいいのかよくわからんという感じになったのでトークしたい。