SavedStateHandleとDaggerとの連携方法については
がありますが、これら以外の方法を考えます。
また、この際Daggerを捨ててしまおうか、ということについても検討してみます。
- 以下のコードは全てを検証しているわけではないので、何らかの誤り等があるかもしれません。
- 2019/09/21時点での
lifecycle-viewmodel-savedstate
ライブラリの最新は1.0.0-alpha05ですが、AbstractSavedStateViewModelFactoryに致命的なバグ3 があるので、動作させる場合は一つ前のバージョン(1.0.0-alpha04)を使用する必要があります。
Daggerとの連携方法 その1
SavedStateHandleを提供するプロバイダクラスを作って、Daggerモジュール側でそれを利用する方法です。
まず、以下のようにSavedStateHandleを提供するプロバイダとファクトリ関数を作ります。
// ファクトリ関数
@Suppress("FunctionName")
fun <O> SavedStateHandleProvider(
owner: O,
defaultArgs: Bundle?
): SavedStateHandleProvider
where O : ViewModelStoreOwner,
O : SavedStateRegistryOwner {
val factory = object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T = SavedStateViewModel(handle) as T
}
return SavedStateHandleProvider(ViewModelProvider(owner.viewModelStore, factory))
}
private const val KEY_PREFIX = "com.example.SavedStateKey:"
// SavedStateHandleを提供するプロバイダ
class SavedStateHandleProvider internal constructor(
private val provider: ViewModelProvider
) {
@MainThread
fun get(key: String): SavedStateHandle =
provider.get("$KEY_PREFIX$key", SavedStateViewModel::class.java).handle
}
}
// SavedStateHandleを保持するViewModel
private class SavedStateViewModel(@JvmField val handle: SavedStateHandle) : ViewModel()
SavedStateHandleを保持するSavedStateViewModel
を用意して、ViewModelProviderを介して取得できるようにします。
次に、上記クラスを利用してSavedStateHandleを提供するDaggerモジュールを作成します。
@Module
object FooFragmentModule {
@JvmStatic
@Provides
fun provideSavedStateHandle(fragment: FooFragment): SavedStateHandle {
return SavedStateHandleProvider(fragment, fragment.arguments)["foo"]
}
}
作成したDaggerモジュールを、@ContributesAndroidInjector
や@Subcomponent
等のFragmentを解決できるDaggerコンポーネントに設定します。
@Module
interface FooModule {
@ContributesAndroidInjector(
modules = [FooFragmentModule::class] // <-- 追加
)
fun contributeFooFragmentInjector(): FooFragment
}
最後に、利便性のためにProviderをViewModelファクトリに変換する関数を用意しておきます。
fun Provider<out ViewModel>.toViewModelFactory(): ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = get() as T
}
これで、以下のようにしてViewModelを取得することができるようになります。
@Inject
lateinit var provider: Provider<FooViewModel>
val viewModel by viewModels<FooViewModel> { provider.toViewModelFactory() }
一つのFragment内で複数のViewModelを扱う場合は、@Named
を使ってSavedStateHandleを識別する方法が考えられます。
class FooViewModel @Inject constructor(
fooRepository: FooRepository,
@Named("foo") handle: SavedStateHandle
) : ViewModel()
class BarViewModel @Inject constructor(
barRepository: BarRepository,
@Named("bar") handle: SavedStateHandle
) : ViewModel()
@Named("foo")
@Provides
fun provideFooSavedStateHandle(fragment: Fragment): SavedStateHandle {
return SavedStateHandleProvider(fragment, fragment.arguments)["foo"]
}
@Named("bar")
@Provides
fun provideBarSavedStateHandle(fragment: Fragment): SavedStateHandle {
return SavedStateHandleProvider(fragment, fragment.arguments)["bar"]
}
Daggerとの連携方法 その2
ViewModel生成時に、一時的にグローバル変数にSavedStateHandleを保持する方法です。
以下のようなViewModelファクトリとDaggerモジュールを作ります4 。
private var savedStateHandle: SavedStateHandle? = null
// グローバル変数にSavedStateHandleを一時保存するViewModelファクトリ
class ViewModelFactory(
private val provider: Provider<out ViewModel>,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle?
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
check(ArchTaskExecutor.getInstance().isMainThread) {
"Cannot create the ViewModel on a background thread"
}
savedStateHandle = handle
return try {
@Suppress("UNCHECKED_CAST")
provider.get() as T
} finally {
savedStateHandle = null
}
}
}
// グローバル変数のSavedStateHandleを提供するDaggerモジュール
@Module
object SavedStateHandleModule {
@JvmStatic
@Provides
fun provideSavedStateHandle(): SavedStateHandle {
check(ArchTaskExecutor.getInstance().isMainThread) {
"Cannot get the SavedStateHandle on a background thread"
}
return checkNotNull(savedStateHandle) {
"Cannot get the SavedStateHandle except while creating a ViewModel"
}
}
}
ViewModelファクトリ側では、Provider.get
の前にSavedStateHandleをグローバル変数に一時的に保持します。
Daggerモジュール側では、保持されたSavedStateHandleを返すようにします。
後は、適当なDaggerコンポーネントもしくはモジュールに、作成したSavedStateHandleModuleを設定するだけです。
@Component(
module = [
AppModule::class,
SavedStateHandleModule::class // <-- 追加
]
)
interface AppComponent
これで、以下のようにしてViewModelを取得することができるようになります。
@Inject
lateinit var provider: Provider<FooViewModel>
val viewModel by viewModels<FooViewModel> {
ViewModelFactory(provider, this, arguments)
}
Daggerを捨てる
思い切ってDaggerを捨てるのであれば
- オブジェクトのスコープ管理
- モックへの差し替え
をどのようにするかを考える必要があります。
以下は、管理すべきオブジェクトのスコープをシングルトンに限定した上で、Dagger利用時と同程度の簡便さでモックへ差し替えできるようにしつつ、SavedStateHandleを利用する方法の一案です。
lifecycle-viewmodel-savedstate
のバージョン1.0.0-alpha03がリリースされたタイミングで、ComponentActivity, Fragment, NavBackStackEntryはデフォルトのViewModelファクトリとして、SavedStateViewModelFactoryを持つようになりました。
SavedStateViewModelFactoryは
- 引数なし
- 引数が1つで、SavedStateHandleのみ
- 引数が1つで、Applicationのみ (AndroidViewModelを継承している場合)
- 引数が2つで、Application, SavedStateHandleの順番 (AndroidViewModelを継承している場合)
の4タイプのコンストラクタを持つViewModelについて、リフレクションによってViewModelインスタンスを生成することができるファクトリです。
したがって、例えば、
class FooViewModel(handle: SavedStateHandle) : ViewModel()
のようなViewModelについて、以下のようにファクトリを指定せずに取得できるようになっています。
val viewModel by viewModels<FooViewModel>()
ここでは、そのデフォルトファクトリを使用します。
まず、Application継承クラスとRepository等の参照をまとめたComponentsクラスの二つを作ります。
private lateinit var app: App
class App : Application() {
init {
app = this
}
}
object Components {
val applicationContext: Context get() = app
val database: AppDatabase by lazy {
Room.databaseBuilder(app, AppDatabase::class.java, "appdb").build()
}
val fooRepository: FooRepository by lazy { FooRepository(database.fooDao()) }
}
App側でAppインスタンスをグローバル変数に保持します。
Components側では、(必要であれば)それを利用してRepository等のインスタンスを構築します。各々Lazyを使って、必要な時までインスタンス化を遅延させます。
このComponentsを利用するViewModelを以下のように作成します。
class FooViewModel @JvmOverloads constructor(
handle: SavedStateHandle,
fooRepository: FooRepository = Components.fooRepository
) : ViewModel()
handle
以外の引数にはデフォルトとしてComponentsのメンバを設定しておきます。
また、@JvmOverloads
を付けることで、デフォルトファクトリがViewModelインスタンスを生成できるようにします。
@JvmOverloads
を付け忘れると実行時まで気付かないので、付け忘れを警告するカスタムLintを別途用意したほうが良いかもしれません。
モック差し替えについては、Dagger利用時と同様
val handle = SavedStateHandle()
val fooRepository = mockk<FooRepository>()
val viewModel = FooViewModel(handle, fooRepository)
といった感じで、Repository等を差し替えることができます。
この仕組みは、(使うかどうかは別にして)ActivityやFragmentでも利用することができます。
class FooFragment @JvmOverloads constructor(foo: Foo = Components.foo) : Fragment()
Appクラスにおいて、app
フィールドの設定を(onCreate
ではなく)init
ブロックで行っているので、ContentProviderでも利用することができるでしょう。
スコープの縛りに耐えられるのであれば、(アプリの規模等にも依るのでしょうが)この方法でも問題ないように思います。
以上です。
-
https://satoshun.github.io/2019/05/viewmodel-savedstate-dagger/ の3番目 ↩
-
https://y-anz-m.blogspot.com/2019/08/savedstatehandle-dagger-viewmodel.html ↩
-
実行スレッドの精査に
ArchTaskExecutor
を使用していますが、当該クラスには@RestrictTo(LIBRARY_GROUP_PREFIX)
が付与されています。代替方法としてThread.currentThread()
とLooper.getMainLooper().thread
とを比較する(minSdkVersionが23以上であればLooper.getMainLooper().isCurrentThread()
を使う)方法がありますが、その場合、ローカルでテストするためにはRobolectricやunmock-pluginが必要になるでしょう。 ↩