Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AssistedInject] Integration with @HiltViewModel #2287

Open
manuelvicnt opened this issue Jan 17, 2021 · 59 comments
Open

[AssistedInject] Integration with @HiltViewModel #2287

manuelvicnt opened this issue Jan 17, 2021 · 59 comments

Comments

@manuelvicnt
Copy link

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)
    }
}
@manuelvicnt
Copy link
Author

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

@manuelvicnt
Copy link
Author

manuelvicnt commented Jan 17, 2021

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)
    }
}

@Ezike
Copy link

Ezike commented Jan 17, 2021

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

@manuelvicnt
Copy link
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)
    }
}

@PatilShreyas
Copy link

@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?

@tfcporciuncula
Copy link

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.

@PatilShreyas
Copy link

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

@luis-cortes
Copy link

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. 🙁

@wbervoets
Copy link

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 :)

@tfcporciuncula
Copy link

@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.

@Chang-Eric
Copy link
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).

@dandc87
Copy link

dandc87 commented Feb 19, 2021

... 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).

@tfcporciuncula
Copy link

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:

@danysantiago
Copy link
Member

Regarding Safe Args, an upcoming release of it will support parsing args from a SavedStateHandle. That should help with regards to arguments type safety.

@FunkyMuse
Copy link

FunkyMuse commented Feb 27, 2021

How would this be done in the compose world? @manuelvicnt

@mhernand40
Copy link

Isn't the example in the issue summary already possible with the latest version of Dagger and Hilt?
Please see https://proandroiddev.com/whats-new-in-hilt-and-dagger-2-31-c46b7abbc64a#33b4

@Chang-Eric
Copy link
Member

@mhernand40 I think the difference is that example you linked does not use @HiltViewModel which means it does not work with the ViewModelComponent. I believe it is the same as the workaround mentioned in #2287 (comment) which I have advised people not to use in #2287 (comment) due to leaking the fragment/activity instance.

@mhernand40
Copy link

@Chang-Eric ah I see. I missed that subtle, yet significant detail. Thanks for bringing it to my attention. 🙂

@FunkyMuse
Copy link

@mhernand40 I think the difference is that example you linked does not use @HiltViewModel which means it does not work with the ViewModelComponent. I believe it is the same as the workaround mentioned in #2287 (comment) which I have advised people not to use in #2287 (comment) due to leaking the fragment/activity instance.

I've read the explanation of the issue, using simple stuff like passing simple strings, ints or anything not related to a fragment/activity should be fine with the workaround suggested in the OP?

@Chang-Eric
Copy link
Member

@FunkyMuse No, you still risk leaking the fragment/activity (even by just having a Provider<> somewhere in a transitive dep), which is not based on what you assist the factory with if you use the workaround where you inject the assisted factory into the fragment.

Passing simple strings, ints and things are okay if you pass those through the Bundle/SavedStateHandle though.

@mhernand40
Copy link

@mhernand40 I think the difference is that example you linked does not use @HiltViewModel which means it does not work with the ViewModelComponent. I believe it is the same as the workaround mentioned in #2287 (comment) which I have advised people not to use in #2287 (comment) due to leaking the fragment/activity instance.

I've read the explanation of the issue, using simple stuff like passing simple strings, ints or anything not related to a fragment/activity should be fine with the workaround suggested in the OP?

Agreed. Sometimes there are cases where an Intent argument is first passed to an Activity that needs to be propagated through to the ViewModel. During a configuration change, the Intent arguments should remain intact, hence there is probably no need to re-bind the argument(s) to the ViewModel?

@FunkyMuse
Copy link

FunkyMuse commented Mar 12, 2021

@FunkyMuse No, you still risk leaking the fragment/activity (even by just having a Provider<> somewhere in a transitive dep), which is not based on what you assist the factory with if you use the workaround where you inject the assisted factory into the fragment.

Passing simple strings, ints and things are okay if you pass those through the Bundle/SavedStateHandle though.

That works for fragment/activity, would the same work for the compose world?

Since there you'd still have arguments to pass but usually they're passed inside a compose function and then you create the view model

Currently this is how I'm doing it

@PublishedApi
internal inline fun <reified T : ViewModel> createAssistedViewModel(
    arguments: Bundle? = null,
    owner: SavedStateRegistryOwner,
    crossinline viewModelProducer: (SavedStateHandle) -> T,
) =
    object : AbstractSavedStateViewModelFactory(owner, arguments) {
        override fun <T : ViewModel> create(
            key: String,
            modelClass: Class<T>,
            handle: SavedStateHandle
        ) =
            viewModelProducer(handle) as T
    }


@Composable
inline fun <reified T : ViewModel> assistedViewModel(
    arguments: Bundle? = null,
    crossinline viewModelProducer: (SavedStateHandle) -> T,
): T =
    viewModel(factory = createAssistedViewModel(
        arguments = arguments,
        owner = LocalSavedStateRegistryOwner.current
    ) {
        viewModelProducer(it)
    })

Since the factory has to be injected in the activity, i assume activity has a chance of leaking?

Any alternative solution?

@drinkthestars
Copy link

drinkthestars commented Mar 14, 2021

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).

No, you still risk leaking the fragment/activity (even by just having a Provider<> somewhere in a transitive dep), which is not based on what you assist the factory with if you use the workaround where you inject the assisted factory into the fragment.

@Chang-Eric Trying to get a deeper understanding where the potential memory leak comes from with OP's workaround.
Going through your comments, the Activity/Fragment could be leaked when:

  1. "injecting the wrong thing into the ViewModel"
  2. "having a Provider<> somewhere in a transitive dep"

Even if those points are handled, you mention there's also the issue of multibinding contributions installed in the FragmentComponent, implying there's something inherently wrong with the very idea of "injecting assisted factory from the FragmentComponent". Could you elaborate? 🙏🏼

@ReginFell
Copy link

parsing args from a SavedStateHandle

Do you have any ideas when it will be shipped?

@FunkyMuse
Copy link

parsing args from a SavedStateHandle

Do you have any ideas when it will be shipped?

It's already there, you pass the State handle with @HiltViewModel
And you have access to the

SavedStateHandle
And
Application

@Chang-Eric
Copy link
Member

@drinkthestars I think the issue is just that even if you address 1 and 2 at the time you first introduce the ViewModel injection, it just isn't going to be durable as code changes without a lot of care and effort. Generally, a binding in the FragmentComponent should be free to inject the Fragment as a dependency, but if you inject your ViewModels using the workaround from the FragmentComponent, then every time you change a FragmentComponent dependency you will need to check all of the users up the chain.

This is what Dagger components actually do for you. It is why it is nice to split say the ApplicationComponent from the ActivityComponent so that if you use the Activity from an @Singleton binding, you get a missing binding error instead of a memory leak. In Hilt, the components are also set up to prevent this for ViewModels (in https://dagger.dev/hilt/components, you can see how the ViewModelComponent branches off before the ActivityComponent and FragmentComponent). If you use the workaround, then you'll subvert this.

@drinkthestars
Copy link

drinkthestars commented Mar 15, 2021

Gotcha, thank you for clarifying @Chang-Eric. So the case seems to be:

@HiltViewModel's ensures that there is an associated with ViewModelComponent which reflects the lifetime of the single ViewModel. If ViewModels are injected using the AssistedFactory workaround from the FragmentComponent, you end up associating a ViewModel related thing with a FragmentComponent which has a shorter lifecycle than a ViewModel/ViewModelComponent. The workaround is mixing two hierarchies of components where one should outlive the other, and hence it is not recommended.

@danielesegato
Copy link

danielesegato commented Feb 2, 2022

We plan to look into the API again after https://issuetracker.google.com/188541057 is resolved as that will solve some of the API problems we're running into.

Hi @Chang-Eric do you have any update? the issue has been marked fixed in October of 2021 and this github issue is linked in the official doc on hilt on how to "officially" handle assisted parameters in viewmodels: https://developer.android.com/training/dependency-injection/hilt-jetpack

This is lower priority because there are workarounds like using the SavedStateHandle (which is probably a best practice over AssistedInject because it cannot leak the activity/fragment) and for those things that cannot be put in the bundle, you can use a setter method (obviously not ideal from a code cleanliness perspective, but this case should be rarer).

I still think I should be able to use @HiltViewModel with @AssistedInject.

the @AssistedFactory could than be used with by viewModelFactory(Factory::class).viewModel(...parameters...) or by viewModelFactory(Factory::class).activityViewModel(...parameters...)`

there should a lambda somewhere there to avoid re-executing parameter creation if not needed

Thanks.

@Chang-Eric
Copy link
Member

Sorry, I don't have any real update at this time. We're focusing on KSP migrations for Dagger so that is taking a lot of our time right now so this is still a lower priority. Also, I think that necessary fix is still only in alpha, so we can't depend on it yet since Hilt is stable.

Also, there's still some uncertainty about the direction we want to take the API. ViewModels do not compose together well, so there are some questions about if use cases would be better served with more things like ActivityRetainedComponent but for other lifetimes since then Dagger objects in there can compose better and don't have the same leaking risks that @AssistedInject into ViewModels presents. That's not to commit one way or another, but just to let you know that even if all the immediate blockers are resolved, this isn't a clear cut case where we just need to find the time to put some code together.

Hope that makes sense, and sorry this probably isn't the update you were looking for.

@danielesegato
Copy link

I see what you mean, I don't really like that you can't inject a ViewModel into another viewModel, makes it harder to create separation of concerns.

But Hilt can't really fix that: something has to be done from the Androidx side with ViewModel replacement / upgrade.

@syt0r
Copy link

syt0r commented Jun 21, 2022

For the first time I've decided to use Hilt instead of Koin and it's very disappointing that support of already existing feature takes so long...

@EllenMady
Copy link

@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.

Man, you saved my life. I'm using compose navigation and I send runtime parameters throw SavedSateHandle like you said. Nothing more is need. Thanks a lot!

@sebaslogen
Copy link

sebaslogen commented Aug 26, 2022

Now that we can inject multiple ViewModel instances of the same type (#2328) inside the same Composable nav-destination/screen (for example for a View-Pager) I really miss the Assisted Injection to provide unique ids to each ViewModel.

Unfortunately, SavedStateHandle solution can't help in this situation because the navArgs only work at the Composable nav-destination/screen level, and in this use case the multiple ViewModel ids are only available after the Composable nav-destination/screen is loaded (probably through a screen level ViewModel that loads the ids of the View-Pager ViewModels).

@mkalitis
Copy link

@manuelvicnt This is getting a little confusing given the length of the thread, lets see if I have this correctly.

We are trying to 'inject' the parameters passed during navigation directly to the view model whilst still having the goodness provided to us with the @HiltViewModel annotation yes?

If so then we do not need the hassle of creating a factory etc nor do we need the @Assisted for the injection. Further we also do not need to pass in the parameter to the screen/fragment which then passes it to the view model. Hilt already gives us this functionality.

I think I might be missing something here??

Thanks,

Martin

@svenjacobs
Copy link

I'm also looking forward to assisted injection for ViewModels. Given the stance "favour composition over inheritance", I would like to inject some "behavioural" pattern into the constructor of a VM which is only known at instantiation time. SavedStateHandle doesn't help here because this class cannot be put into a Bundle.

@Tgo1014
Copy link

Tgo1014 commented Jan 27, 2023

Really missing this feature, I tried to use the new ViewModelLifecycleScope {} with hiltViewModel() but unfortunately it crashes. Would help a lot for having different instances in a ViewPager with the same screen working independently.

@mecoFarid
Copy link

Is there any plan to support AssistedInject in ViewModel?

We've looked into it, but it isn't trivial and so far the API wouldn't be that great. This doesn't mean it is off the table, but it also isn't a high priority compared to other things so I wouldn't expect it anytime in the near future.

You mean like for the next 15 years?

@JohannesPtaszyk
Copy link

Are there any updates/plans? :)
Would be really handy to get this!

@Chang-Eric
Copy link
Member

Just updating this. We have a plan for supporting this now, however, it is competing with other higher priority projects for development time like the migration to KSP, so there's no real timeline here yet.

FWIW, now that CreationExtras is supported, you can use the DEFAULT_ARGS_KEY key for setting the default args passed to a ViewModel's SavedStateHandle, so you don't have to only pass things through the Fragment arguments. So something like:

val myVM by viewModels<MyViewModel>(extrasProducer = {
  MutableCreationExtras(defaultViewModelCreationExtras).apply {
    set(DEFAULT_ARGS_KEY, bundleOf("someId" to someId))
  }
})

will let you pass a value to be retrieved in the ViewModel through the SavedStateHandle.

@mtwalli
Copy link

mtwalli commented Mar 10, 2023

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.

How possibly this solution could be working in unit tests?

@mkalitis
Copy link

A similar thing can already be done using annotation classes which use the navigation parameters to "assist" the additional parameters into the viewmodel constructor when using HiltViewModel() This gives the same behaviour as doing an @assisted on the view model. This works for something like compose navigation though could be extended to other screen creation/fragment creation.

More info here: https://medium.com/@i.write.code/android-compose-navigation-view-models-and-hilt-c824541bd8e
Example code of doing this with compose navigation: https://github.com/mkalitis/HiltNavigationInjection

Just an thought

@nvkleban
Copy link

nvkleban commented Nov 9, 2023

Dagger documentation shows currently not released APIs like HiltViewModelExtensions.withCreationCallback is this the solution we were waiting for?

@ubuntudroid
Copy link

Dagger documentation shows currently not released APIs like HiltViewModelExtensions.withCreationCallback is this the solution we were wainitng for?

Exciting! Hopefully this will also work with hiltViewModel() in Compose.

@kuanyingchou
Copy link
Collaborator

@nvkleban @ubuntudroid This should be out in the next release, and hiltViewModel() will be updated next. In the meantime you can give HEAD-SNAPSHOT a try to see if it works for you!

@armichaud
Copy link

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

@cgaisl
Copy link

cgaisl commented Jan 20, 2024

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

@carstenhag
Copy link

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

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

No branches or pull requests