Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass runtime parameters to a ViewModel's constructor when using Hilt for dependency injection?

I'm wondering how to pass runtime parameters to a ViewModel's constructor while using Hilt for DI? Prior to using Hilt, I have a ViewModel that looks like this:

class ItemViewModel(private val itemId: Long) : ViewModel() {
    private val repo = ItemRepository(itemId) 
}

class ItemViewModelFactory(private val itemId: Long) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(ItemViewModel::class.java)) {
        return ItemViewModel(itemId) as T
    }
    throw IllegalArgumentException("Unknown ViewModel class")
}

I create the above ViewModel in my fragment like this:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val args: ItemScreenFragmentArgs by navArgs()
    val itemId = args.itemId

    //Create the view model factory
    val viewModelFactory = ItemViewModelFactory(application, itemId)

    // Get a reference to the ViewModel associated with this fragment.
    val itemViewModel = ViewModelProvider(this, viewModelFactory).get(ItemViewModel::class.java)
}

If my ItemViewModel constructor didn't have the itemId parameter, my ViewModel and Fragment using Hilt would look like this:

class ItemViewModel
@ViewModelInject
constructor(private val repo: ItemRepository) : ViewModel() { }

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val itemViewModel: ItemViewModel by viewModels ()
}

I'm trying to figure out how to pass the itemId that I get from the ItemFragment's NavArgs to the ItemViewModel's constructor? Is there a way to do this with Hilt?

like image 940
Redek Avatar asked Dec 13 '20 20:12

Redek


3 Answers

Assisted injection is now supported by Dagger and InflationInjection got its own repo. The syntax would now be like :

import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
...




class ItemViewModel
@AssistedInject
constructor(private val repo: ItemRepository, @Assisted private val itemId: Long) : ViewModel() {
    init {
        repo.itemId = itemId
    }

    //[email protected]
    @AssistedFactory
    interface AssistedFactory {
        fun create(@Named("item_id") itemId: Long): ItemViewModel
    }

    companion object {
        fun provideFactory(
            assistedFactory: AssistedFactory,
            itemId: Long
    ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
         override fun <T : ViewModel?> create(modelClass: Class<T>): T {
             return assistedFactory.create(itemId) as T
         }
       }
    }
}

@InstallIn(FragmentComponent::class)
//-@AssistedModule
@Module
interface AssistedInjectModule {}

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val args: ItemScreenFragmentArgs by navArgs()      
    @Inject lateinit var itemViewModelAssistedFactory:ItemViewModel.AssistedFactory        
    private val itemViewModel: ItemViewModel by viewModels {
        ItemViewModel.provideFactory(itemViewModelAssistedFactory, args.itemId)
    }    
}

Base on Redek answer. More on that here

like image 61
Leuss Avatar answered Sep 29 '22 20:09

Leuss


For anyone else looking to pass runtime parameters to a ViewModel while using Dagger Hilt, this is how I did it:

I followed the code from this example which uses the AssistedInject library.

My code now looks as follows:

class ItemViewModel
@AssistedInject
constructor(private val repo: ItemRepository, @Assisted private val itemId: Long) : ViewModel() {
    init {
        repo.itemId = itemId
    }

    @AssistedInject.Factory
    interface AssistedFactory {
        fun create(itemId: Long): ItemViewModel
    }

    companion object {
        fun provideFactory(
            assistedFactory: AssistedFactory,
            itemId: Long
        ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return assistedFactory.create(itemId) as T
            }
        }
    }
}

@InstallIn(FragmentComponent::class)
@AssistedModule
@Module
interface AssistedInjectModule {}

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val args: ItemScreenFragmentArgs by navArgs()      
    @Inject lateinit var itemViewModelAssistedFactory: ItemViewModel.AssistedFactory        
    private val itemViewModel: ItemViewModel by viewModels {
            ItemViewModel.provideFactory(itemViewModelAssistedFactory, args.itemId)
    }    
}
like image 21
Redek Avatar answered Nov 17 '22 19:11

Redek


There is a better solution without AssistedInject, just use the SavedStateHandle.

if the args in your fragment contains userId:

val args: UserFragmentArgs by navArgs()
...
args.userId

Then simply in your savedStateHandle it will be available without additional work.

class UserViewModel @Inject constructor (
    private val state: SavedStateHandle
) : ViewModel() {

    //Get the value of the userId here
    val userId = state.get<String>("userId")


   //Also you can do this using safe args
   val args = UserFragmentArgs.fromSavedStateHandle(state)
   val userId = args.userId
}

Full Implementation details: https://mattrobertson.dev/passing-safe-args-to-your-viewmodel-with-hilt-366762ff3f57

like image 7
Salam El-Banna Avatar answered Nov 17 '22 18:11

Salam El-Banna