androiddagger-2android-lifecycleandroid-viewmodelviewmodel-savedstate

How to dynamically set the bundle from AbstractSavedStateViewModelFactory


My ViewModelFactory:

class ViewModelFactory @Inject constructor(
    private val viewModelMap: MutableMap<Class<out ViewModel>, ViewModelAssistedFactory<out ViewModel>>,
    owner: SavedStateRegistryOwner,
    defaultArgs: Bundle?
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {

    @Throws(IllegalStateException::class)
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
        return viewModelMap[modelClass]?.create(handle) as? T ?: throw IllegalStateException("Unknown ViewModel class")
    }
}

Activity:

@Inject
lateinit var viewModelFactory: ViewModelFactory
protected val viewModel: ViewModel by lazy { ViewModelProvider(this, viewModelFactory).get(getViewModelClass()) }

ViewModel:

@AssistedInject.Factory
interface Factory : ViewModelAssistedFactory<SplashViewModel>

And I was wondering how can I provide the defaultArgs dynamically instead of:

ActivityModule

@Module
    companion object {
        @JvmStatic
        @Nullable
        @Provides
        fun provideDefaultArgs(): Bundle? {
            return null
        }
    }

The idea is to have the possibility to send a parameter to the ViewModel, let's say an ID for a DetailActivity. Normally I use an "init" method, but If I could use the StateHandle map would be even better. Something like this and this.

Makes sense? Is it possible?


Solution

  • I came across 2 solutions:

    Solution A:

    Change our ViewModelFactory to:

    class ViewModelFactory @Inject constructor(
        private val viewModelMap: MutableMap<Class<out ViewModel>, ViewModelAssistedFactory<out ViewModel>>
    ) {
        fun create(owner: SavedStateRegistryOwner, defaultArgs: Bundle? = null): AbstractSavedStateViewModelFactory {
            return object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
                    return viewModelMap[modelClass]?.create(handle) as? T ?: throw IllegalStateException("Unknown ViewModel class")
                }
            }
        }
    }
    

    And finally:

    @Inject
    lateinit var viewModelFactory: ViewModelFactory
    protected val viewModel: ViewModel by lazy { ViewModelProvider(this, viewModelFactory.create(this, args).get(getViewModelClass()) }
    

    Also, we can simplify our ActivityModule to (example):

    @AssistedModule
    @Module(includes = [AssistedInject_SplashActivityModule::class])
    abstract class SplashActivityModule{
    
        @Binds
        @IntoMap
        @ViewModelKey(SplashViewModel::class)
        abstract fun bindFactory(factory: SplashViewModel.Factory): ViewModelAssistedFactory<out ViewModel>
    }
    

    This post by Tomáš Mlynarič helped with this solution.

    Solution B:

    Create a Factory without injected dependencies:

    class ViewModelFactoryAlt<out V : ViewModel>(
        private val viewModelFactory: ViewModelAssistedFactory<V>,
        owner: SavedStateRegistryOwner,
        defaultArgs: Bundle? = null
    ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
    
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
            return viewModelFactory.create(handle) as T
        }
    }
    

    Create our ViewModelAssitedFactory like:

    class HelloWorldViewModelFactory @Inject constructor(
        private val dependencyA: DependencyA,
        ...
    ) : ViewModelAssistedFactory<HelloWorldViewModel> {
        override fun create(stateHandle: SavedStateHandle): HelloWorldViewModel {
            return HelloWorldViewModel(stateHandle, dependencyA, ...)
        }
    }
    

    And finally:

    @Inject
    lateinit var factory: HelloWorldViewModelFactory
    private val viewModel: HelloWorldViewModel by viewModels { ViewModelFactoryAlt(factory, this, intent.extras) }
    

    This post by Elye helped with this solution.