Repository navigation
[AssistedInject] Integration with @HiltViewModel #2287
Description
Activity
Trying to use
@HiltViewModelwithAssistedInjectgives aViewModel constructor should be annotated with @Inject instead of @AssistedInjectusing Dagger/Hilt 2.31Reacted by feivur, Majid Ahmadi Jebeli, Yosif, Vengatesh M, dk-3cx, Levi Melamed, Matej Hlatký, Rodrigo, Martin Mose Facondini, IsakTheHacker and 2 moreIn Dagger 2.31, it's possible to achieve the above without using
@HiltViewModeland passing everything manuallyclass 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) } }Reacted by Gabor Varadi, Bitlinker, Bryan Dela Cruz, drinkthestars, Alexander Ruhland, Dmytro Ivanov, Illia Achour, Hossain Khan, Ajay Singh Dewari, kefasjw and 22 moreReacted by KimoTeru and philip-haspaReacted by Vitaliy PtitsynReacted by NDiazRoncero and jain-ullasIn Dagger 2.31, it's possible to achieve the above without using
@HiltViewModeland passing everything manuallyclass 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
FTR, it's even easier without the
SavedStateHandledependency (although always consider usingSavedStateHandlein 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) } }Reacted by Tobenna Ezike, Mikhalchuk Grigoriy, Abdelraouf Sabri, Anton Kovalov, saied89, Euan Rochester, Mike, Farrukh Khusainov, Fikky Ardianto, Trubnikov Dima and 3 moreReacted by Gabor Varadi, glureau-betclic, Carlos Guerrero, Marco RS, Roman Tikonov and Fabio Santo@manuelvicnt Since we're not able to add
@HiltViewModelfor a ViewModel having Assisted Injection, can't install modules usingViewModelComponent. So currently usingActivityRetainedComponentfor a repository which is going to be injected with that ViewModel. Is it correct to use it like this?Reacted by Yogesh Choudhary, Tan Jun Rong, Mario Bat and Vikram SinghA 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.
Reacted by Shreyas Patil, Riccardo Ciovati, Tobenna Ezike, Nicolas Gouzy, Artur Artikov, Okonji Emmanuel , Michael Nimbs, FunkyMuse, Osip Fatkullin, Ash Davies and 36 moreI'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.
NoteDetailViewModelis 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
@Composablefunction 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
Reacted by Carlos Guerrero, Gowtham, Couzy, Shakil Karim, NPcgmrt, Kdompy Saing, Tyler, Fabio Santo and Amr MohamedReacted by drinkthestars, Alexey Gvozditskiy, Fokke Vermeulen, FFFF0h, Aidan McWilliams, Fabio Santo and Trubnikov DimaThis is preventing me from sharing a binding between a
ViewModelannotated with@HiltViewModeland one that's using@AssistedInject@manuelvicnt Since we're not able to add
@HiltViewModelfor a ViewModel having Assisted Injection, can't install modules usingViewModelComponent. So currently usingActivityRetainedComponentfor 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. 🙁
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 :)
@wbervoets With the new
@HiltViewModelyou can have theSavedStateHandleas a dependency without having to annotate it with@Assisted. TheSavedStateHandleis now a binding from the newViewModelComponent, so it doesn't need to be assist-injected anymore.Reacted by Wim Bervoets, Agung Subastian, Shohei Kawano, Reza Najafi, Norris Aboagye Boateng, Abdelraouf Sabri, s12u, tcqq, Ricardo Sousa, Ricard and 25 moreReacted by Ricard, Radoslav Backovsky, Hossain Khan, Chandan Kumar Mandal, Jeremy Walker, Martin M., santu01, ThinkDeeper, Mustafa Berkay Mutlu, IsakTheHacker and 1 moreReacted by Radoslav Backovsky, Hossain Khan, matteofabris, IsakTheHacker and furkanayazSorry 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 theFragmentComponent. Finally, if you usefastInitmode (which is the default in Hilt), any reference to aProvider<>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
SavedStateHandleinjected 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).
Reacted by Alexander Ruhland, FunkyMuse, Daniel Kim, drinkthestars, Osaigbovo Odiase, Ricardo Costeira, trietbui85, Gabor Varadi, Farrukh Khusainov, Seyyed davud hosseiny and 5 more... use the arguments bundle in your fragment which should be accessible via the
SavedStateHandleinjected 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
@AssistedInjectsupport (depending on how ViewModels are instantiated & reused for Fragment instances with different arguments).The main workaround we suggest is to use the arguments bundle in your fragment which should be accessible via the
SavedStateHandleinjected 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:
Reacted by Mike Scamell and Omer Karakose43 remaining items
For those who might be reading this thread looking for how in the end this works with
hiltViewModel(), here's a guide.Reacted by Alex Wied, Anurag Parmar, Renan Silva Moura, Benoit Letondor and Piotr PiskorskiI've shared my journey of trying to pass runtime arguments to a @hiltviewmodel here.
Reacted by Carsten HagemannReacted by Renan Silva Moura and Carsten Hagemann@kuanyingchou This ticket can also be closed, right? It is mentioned in the release notes as fixed
Reacted by Sven Jacobs@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!Reacted by Carsten Hagemann, Sven Jacobs, Karol Kamiński, Kakeru Nakabachi, Danni and Wojciech Rozwadowski@myounis97 As it's mentioned here,
SavedStateHandleis a binding fromViewModelComponentso you can add it as a regular non-assisted dependency to your ViewModel if you're using Hilt.Reacted by Mohammad Younis@myounis97 As it's mentioned here,
SavedStateHandleis a binding fromViewModelComponentso 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
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.
Reacted by Mohammad Younis, Gabor Varadi, WuLianpu and Shubham Singh@tfcporciuncula My bad i didn't remove the factory from the Singleton Scoped Entry point after removing it everything worked fine thanks
Just making sure these two issues are related via links #3523 (comment)
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 } }@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.
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@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.


It'd be nice to have Assisted Injection support for Hilt ViewModels.
A nice API would be something like the following:
And use it from the View like: