androidandroid-savedstatedagger-hiltdynamic-feature-moduleviewmodel-savedstate

How to inject SavedStateHandle to ViewModel in dynamic feature module?


Using @Assisted annotation with savedStateHandle and by viewModels() it's possible to inject SavedStateHandle object to ViewModel in modules that are not dynamic feature modules with dagger hilt as

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: MainActivityViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

class MainActivityViewModel @ViewModelInject constructor(
    @Assisted savedStateHandle: SavedStateHandle
) : ViewModel() {

    val stringData = savedStateHandle.getLiveData<String>("hello_world")
}

but it's not possible for dynamic feature modules to do like this. How is it done with dynamic feature module ViewModels?


Solution

  • My ViewModel is

    class DashboardViewModel @ViewModelInject constructor(
        @Assisted private val savedStateHandle: SavedStateHandle,
        private val coroutineScope: CoroutineScope,
        private val dashboardStatsUseCase: GetDashboardStatsUseCase,
        private val setPropertyStatsUseCase: SetPropertyStatsUseCase
    ) : ViewModel() {
    
    }
    

    Creating generic FragmentFactory for SavedStateHandle with

    interface ViewModelFactory<out V : ViewModel> {
        fun create(handle: SavedStateHandle): V
    }
    
    class GenericSavedStateViewModelFactory<out V : ViewModel>(
        private val viewModelFactory: ViewModelFactory<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
        }
    }
    
    /**
     * Convenience function to use with `by viewModels` that creates an instance of
     * [AbstractSavedStateViewModelFactory] that enables us to pass [SavedStateHandle]
     * to the [ViewModel]'s constructor.
     *
     * @param factory instance of [ViewModelFactory] that will be used to construct the [ViewModel]
     * @param owner instance of Fragment or Activity that owns the [ViewModel]
     * @param defaultArgs Bundle with default values to populate the [SavedStateHandle]
     *
     * @see ViewModelFactory
     */
    @MainThread
    inline fun <reified VM : ViewModel> SavedStateRegistryOwner.withFactory(
        factory: ViewModelFactory<VM>,
        defaultArgs: Bundle? = null
    ) = GenericSavedStateViewModelFactory(factory, this, defaultArgs)
    

    ViewModel factory for ViewModel

    class DashboardViewModelFactory @Inject constructor(
        private val coroutineScope: CoroutineScope,
        private val dashboardStatsUseCase: GetDashboardStatsUseCase,
        private val setPropertyStatsUseCase: SetPropertyStatsUseCase
    ) : ViewModelFactory<DashboardViewModel> {
    
        override fun create(handle: SavedStateHandle): DashboardViewModel {
            return DashboardViewModel(
                handle,
                coroutineScope,
                dashboardStatsUseCase,
                setPropertyStatsUseCase
            )
        }
    }
    

    And creating ViewModel using the DashBoardViewModelFactory in Fragment as

    @Inject
    lateinit var dashboardViewModelFactory: DashboardViewModelFactory
    
    private val viewModel: DashboardViewModel
    by viewModels { withFactory(dashboardViewModelFactory) }
    

    Here you can see the full implementation in action. I wasn't able to find the source i used to implement this solution, if you can comment the link, i would like to give credit to author.