Skip to content

[AssistedInject] Integration with @HiltViewModel #2287

Description

@manuelvicnt

It'd be nice to have Assisted Injection support for Hilt ViewModels.

A nice API would be something like the following:

@HiltViewModel
class PlantDetailViewModel @AssistedInject constructor(
    savedStateHandle: SavedStateHandle,
    plantRepository: PlantRepository,
    @Assisted private val plantId: String
) : ViewModel() { ... }
@AssistedFactory
interface PlantDetailViewModelFactory {
    fun create(plantId: String): PlantDetailViewModel
}

And use it from the View like:

@AndroidEntryPoint
class PlantDetailFragment : Fragment() {

    private val args: PlantDetailFragmentArgs by navArgs()

    @Inject
    lateinit var plantDetailViewModelFactory: PlantDetailViewModelFactory

    private val plantDetailViewModel: PlantDetailViewModel by viewModels {
        plantDetailViewModelFactory.create(args.plantId)
    }
}

Activity

  1. manuelvicnt commented on Jan 17, 2021

    @manuelvicnt
    Author

    Trying to use @HiltViewModel with AssistedInject gives a ViewModel constructor should be annotated with @Inject instead of @AssistedInject using Dagger/Hilt 2.31

  2. manuelvicnt commented on Jan 17, 2021

    @manuelvicnt
    Author

    In Dagger 2.31, it's possible to achieve the above without using @HiltViewModel and passing everything manually

    class PlantDetailViewModel @AssistedInject constructor(
        plantRepository: PlantRepository,
        @Assisted private val savedStateHandle: SavedStateHandle,
        @Assisted private val plantId: String
    ) : ViewModel() {
    
        @AssistedFactory
        interface PlantDetailViewModelFactory {
            fun create(handle: SavedStateHandle, plantId: String): PlantDetailViewModel
        }
    
        companion object {
            fun provideFactory(
                assistedFactory: PlantDetailViewModelFactory,
                owner: SavedStateRegistryOwner,
                defaultArgs: Bundle? = null,
                plantId: String
            ): AbstractSavedStateViewModelFactory = object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
                    return assistedFactory.create(handle, plantId) as T
                }
            }
        }
    }
    

    And consume it in the Fragment as

    @AndroidEntryPoint
    class PlantDetailFragment : Fragment() {
    
        private val args: PlantDetailFragmentArgs by navArgs()
    
        @Inject
        lateinit var plantDetailViewModelFactory: PlantDetailViewModelFactory
    
        private val plantDetailViewModel: PlantDetailViewModel by viewModels {
            PlantDetailViewModel.provideFactory(plantDetailViewModelFactory, this, arguments, args.plantId)
        }
    }
    
  3. Ezike commented on Jan 17, 2021

    @Ezike

    In Dagger 2.31, it's possible to achieve the above without using @HiltViewModel and passing everything manually

    class PlantDetailViewModel @AssistedInject constructor(
        plantRepository: PlantRepository,
        @Assisted private val savedStateHandle: SavedStateHandle,
        @Assisted private val plantId: String
    ) : ViewModel() {
    
        @AssistedFactory
        interface PlantDetailViewModelFactory {
            fun create(handle: SavedStateHandle, plantId: String): PlantDetailViewModel
        }
    
        companion object {
            fun provideFactory(
                assistedFactory: PlantDetailViewModelFactory,
                owner: SavedStateRegistryOwner,
                defaultArgs: Bundle? = null,
                plantId: String
            ): AbstractSavedStateViewModelFactory = object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
                    return assistedFactory.create(handle, plantId) as T
                }
            }
        }
    }
    

    And consume it in the Fragment as

    @AndroidEntryPoint
    class PlantDetailFragment : Fragment() {
    
        private val args: PlantDetailFragmentArgs by navArgs()
    
        @Inject
        lateinit var plantDetailViewModelFactory: PlantDetailViewModelFactory
    
        private val plantDetailViewModel: PlantDetailViewModel by viewModels {
            PlantDetailViewModel.provideFactory(plantDetailViewModelFactory, this, arguments, args.plantId)
        }
    }
    

    thanks for the workaround :) @manuelvicnt

  4. manuelvicnt commented on Jan 18, 2021

    @manuelvicnt
    Author

    FTR, it's even easier without the SavedStateHandle dependency (although always consider using SavedStateHandle in your VMs)

    class PlantDetailViewModel @AssistedInject constructor(
        plantRepository: PlantRepository,
        @Assisted private val plantId: String
    ) : ViewModel() {
       ...
    
        companion object {
            fun provideFactory(
                assistedFactory: PlantDetailViewModelFactory,
                plantId: String
            ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                    return assistedFactory.create(plantId) as T
                }
            }
        }
    }
    
    @AssistedFactory
    interface PlantDetailViewModelFactory {
        fun create(plantId: String): PlantDetailViewModel
    }
    

    And consume it in the View like:

    @AndroidEntryPoint
    class PlantDetailFragment : Fragment() {
    
        private val args: PlantDetailFragmentArgs by navArgs()
    
        @Inject
        lateinit var plantDetailViewModelFactory: PlantDetailViewModelFactory
    
        private val plantDetailViewModel: PlantDetailViewModel by viewModels {
            PlantDetailViewModel.provideFactory(plantDetailViewModelFactory, args.plantId)
        }
    }
    
  5. PatilShreyas commented on Jan 18, 2021

    @PatilShreyas

    @manuelvicnt Since we're not able to add @HiltViewModel for a ViewModel having Assisted Injection, can't install modules using ViewModelComponent. So currently using ActivityRetainedComponent for a repository which is going to be injected with that ViewModel. Is it correct to use it like this?

  6. tfcporciuncula commented on Jan 18, 2021

    @tfcporciuncula

    A good improvement on top of the workaround is to define a few extensions to generalize the solution so we don't need to repeat that boilerplate in each ViewModel. Here's the example for the fragment scoped ViewModel case:

    inline fun <reified T : ViewModel> Fragment.assistedViewModel(
      crossinline viewModelProducer: (SavedStateHandle) -> T
    ) = viewModels<T> {
      object : AbstractSavedStateViewModelFactory(this, arguments) {
        override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle) =
          viewModelProducer(handle) as T
      }
    }

    And then in the fragment:

    @Inject lateinit var viewModelFactory: SomeViewModel.Factory
    
    private val viewModel by assistedViewModel { 
      viewModelFactory.create(input = args.input, savedStateHandle = it)
    }

    Other extensions can then be added to cover the other cases.

    It would definitely be better to have Hilt support this out of the box, though, and I really like the API suggested here.

  7. PatilShreyas commented on Jan 18, 2021

    @PatilShreyas

    I'm using Jetpack compose in a project and here's a single activity I'm using in it. It's using Jetpack navigation for various screens. NoteDetailViewModel is using Assisted Injection so I want to access the factory when I only need it i.e. in Composable function. So I've achieved right now like this...

    • This is how ViewModel looks
    class NoteDetailViewModel @AssistedInject constructor(
        private val notyTaskManager: NotyTaskManager,
        @LocalRepository private val noteRepository: NotyNoteRepository,
        @Assisted private val noteId: String
    ) : ViewModel() {
        // Other ViewModel logic
    
        @AssistedFactory
        interface Factory {
            fun create(noteId: String): NoteDetailViewModel
        }
    
        companion object {
            fun provideFactory(
                assistedFactory: Factory,
                noteId: String
            ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
                override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                    return assistedFactory.create(noteId) as T
                }
            }
        }
    }
    • Created EntryPoint in MainActivity
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
        @EntryPoint
        @InstallIn(ActivityComponent::class)
        interface ViewModelFactoryProvider {
            fun noteDetailViewModelFactory(): NoteDetailViewModel.AssistedFactory
        }
    }
    • Somewhere in @Composable function I wrote this and it's perfectly working fine
    @InternalCoroutinesApi
    @ExperimentalCoroutinesApi
    @Composable
    fun NoteDetailsScreen(navController: NavHostController) {
    
        val viewModelFactory = EntryPointAccessors.fromActivity(
            AmbientContext.current as Activity, 
            MainActivity.ViewModelFactoryProvider::class.java
        ).noteDetailViewModelFactory()
    
        val noteDetailViewModel: NoteDetailViewModel = viewModel(
            factory = NoteDetailViewModel.provideFactory(viewModelFactory, "noteIdHere")
        )
    
        Scaffold(
            ....
        )
    }

    So my question - Is it good to use this approach for getting ViewModel factory in Composable functions? Or is there any other workaround for getting Assisted Injection ViewModel factory in such functions?

    cc: @manuelvicnt

  8. luis-cortes commented on Jan 26, 2021

    @luis-cortes

    This is preventing me from sharing a binding between a ViewModel annotated with @HiltViewModel and one that's using @AssistedInject

    @manuelvicnt Since we're not able to add @HiltViewModel for a ViewModel having Assisted Injection, can't install modules using ViewModelComponent. So currently using ActivityRetainedComponent for a repository which is going to be injected with that ViewModel. Is it correct to use it like this?

    This seems to be the only way to share it as far as I can tell. 🙁

  9. wbervoets commented on Feb 2, 2021

    @wbervoets

    At this moment SavedStateHandle is the only @assisted injection I'm using which works fine with @ViewModelInject and no workarounds needed. @ViewModelInject is deprecated now, so please don't remove it before this feature request has been implemented :)

  10. tfcporciuncula commented on Feb 2, 2021

    @tfcporciuncula

    @wbervoets With the new @HiltViewModel you can have the SavedStateHandle as a dependency without having to annotate it with @Assisted. The SavedStateHandle is now a binding from the new ViewModelComponent, so it doesn't need to be assist-injected anymore.

  11. Chang-Eric commented on Feb 19, 2021

    @Chang-Eric
    Member

    Sorry for being late to this thread, but I should clarify some issues with the workaround described in #2287 (comment). The high-level is though that people should not use this workaround.

    The issue with the workaround is that the assisted factory is injected from the FragmentComponent (since it is injected directly into the fragment). This is a problem because it basically all but guarantees you're going to leak your activity/fragment instance into the ViewModel. This can happen simply by injecting the wrong thing into the ViewModel, but even if you are diligent about that, there's also the issue of multibinding contributions installed in the FragmentComponent. Finally, if you use fastInit mode (which is the default in Hilt), any reference to a Provider<> in the transitive deps of your ViewModel will leak the component, which will include the fragment instance (https://dagger.dev/dev-guide/compiler-options).

    The main workaround we suggest is to use the arguments bundle in your fragment which should be accessible via the SavedStateHandle injected in the ViewModel. This should handle most data types.

    For other object types, hopefully rarer, I think the options are passing them as arguments when calling methods on the ViewModel or using a setter method (though similarly, be careful of leaks in this case, especially with any function closures as those may reference the fragment).

  12. dandc87 commented on Feb 19, 2021

    @dandc87

    ... use the arguments bundle in your fragment which should be accessible via the SavedStateHandle injected in the ViewModel

    @Chang-Eric Could you point to where this is documented? When reading up on ViewModels, my impression had been that nothing would be prepopulated for "fresh" instances. This could likely cover most cases where we'd otherwise require @AssistedInject support (depending on how ViewModels are instantiated & reused for Fragment instances with different arguments).

  13. tfcporciuncula commented on Feb 19, 2021

    @tfcporciuncula

    The main workaround we suggest is to use the arguments bundle in your fragment which should be accessible via the SavedStateHandle injected in the ViewModel. This should handle most data types.

    @Chang-Eric There's a big downside with this, though: we lose type safety. We've come a long way with Navigation Safe Args, so losing that here isn't great. I understand the technical limitations, but I thought it would still be relevant to bring this up -- I would much rather have lint checks helping with my diligence when it comes to ViewModels (e.g. preventing me to inject Provider<>) than to give up on type safety.


    @dandc87 You can look directly at the code:

  14. 43 remaining items

  15. armichaud commented on Jan 19, 2024

    @armichaud

    For those who might be reading this thread looking for how in the end this works with hiltViewModel(), here's a guide.

  16. cgaisl commented on Jan 20, 2024

    @cgaisl

    I've shared my journey of trying to pass runtime arguments to a @hiltviewmodel here.

  17. carstenhag commented on Feb 26, 2024

    @carstenhag

    @kuanyingchou This ticket can also be closed, right? It is mentioned in the release notes as fixed

  18. kuanyingchou commented on Feb 27, 2024

    @kuanyingchou

    @carstenhag Yes! Assisted injection with ViewModels was added in Dagger 2.49 and overloads for functions like hiltViewModel() were added to the AndroidX part of Hilt in 1.2.0. Thanks for the reminder!

  19. myounis97 commented on Feb 29, 2024

    @myounis97

    How can I inject SavedStateHandle with the new approach I used to inject it like in the screenshot is there a way to migrate to the new approach
    Screenshot 2024-02-29 121950
    ?

  20. tfcporciuncula commented on Feb 29, 2024

    @tfcporciuncula

    @myounis97 As it's mentioned here, SavedStateHandle is a binding from ViewModelComponent so you can add it as a regular non-assisted dependency to your ViewModel if you're using Hilt.

  21. myounis97 commented on Feb 29, 2024

    @myounis97

    @myounis97 As it's mentioned here, SavedStateHandle is a binding from ViewModelComponent so you can add it as a regular non-assisted dependency to your ViewModel if you're using Hilt.

    Unfortunately it doesn't work with @hiltviewmodel(assistedFactory) it gives compile time error cannot inject savedStateHandle without @provide

  22. tfcporciuncula commented on Feb 29, 2024

    @tfcporciuncula

    This compiles fine for me:

    @HiltViewModel(assistedFactory = MyViewModel.Factory::class)
    class MyViewModel @AssistedInject constructor(
      @Assisted val runtimeArg: String,
      private val savedStateHandle: SavedStateHandle,
      private val someDependency: SomeDependency,
    ) : ViewModel() {
      @AssistedFactory interface Factory {
        fun create(runtimeArg: String): MyViewModel
      }
      ...
    }

    And I'm able to create the ViewModel with:

    private val myViewModel by viewModels<MyViewModel>(
      extrasProducer = {
        defaultViewModelCreationExtras.withCreationCallback<MyViewModel.Factory> { factory ->
          factory.create(runtimeArg = "abc")
        }
      }
    )

    As it's described in the docs.

  23. myounis97 commented on Feb 29, 2024

    @myounis97

    @tfcporciuncula My bad i didn't remove the factory from the Singleton Scoped Entry point after removing it everything worked fine thanks

    Screenshot 2024-02-29 141727

  24. Zhuinden commented on Feb 17, 2025

    @Zhuinden

    Just making sure these two issues are related via links #3523 (comment)

  25. iamanbansal commented on Jun 19, 2025

    @iamanbansal

    Facing Dagger does not support providing @AssistedFactory types. while writing Fragment/Activity test. What's the right way to provide factory ?
    @manuelvicnt @tfcporciuncula

    @HiltAndroidTest
    class FragmentTest {
    
        @RelaxedMockK
        lateinit var viewmodel:HubViewModel
    
        @BindValue
        val hubViewModelAssistedFactory = object : HubViewModelAssistedFactory {
            override fun create(flowProvider: Provider): HubViewModel {
                return viewmodel
            }
        }
    
  26. Chang-Eric commented on Jun 20, 2025

    @Chang-Eric
    Member

    @iamanbansal You can't bind over the AssistedFactory binding, even in tests, as that is something Hilt is passing to your callback. In general, we don't support providing a fake ViewModel, but rather encourage changing the bindings the ViewModel depends on in tests.

  27. iamanbansal commented on Jun 20, 2025

    @iamanbansal

    Thanks @Chang-Eric ! I can change other bindings of VM but how would I change the binding of assisted one (provider in my case) in test case? Currently real object is being passed from the Fragment.
    It would be great if there is any example of Fragment/Activity Test for such Assisted Injection

  28. Chang-Eric commented on Jun 23, 2025

    @Chang-Eric
    Member

    @iamanbansal Assisted parameters are passed from your code and not from Dagger/Hilt so there's nothing we really provide here. Those parameters are kind of outside of dependency injection so you'd have to figure out your own strategy to handle the arguments like any other argument you pass into a normal method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions