epic
epic

Reputation: 1443

android ViewModelFactory with hilt

I am first trying android ViewModel and Hilt DI

As i understand from below link, to initialize ViewModel with a value on run-time i should use ViewModelFactory

Use a ViewModelFactory

//ViewModel
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}

//ViewModelFactory
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}


//Fragment
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)

And to use ViewModel with hilt i should use @ViewModelInject as explained in below link

Hilt and Jetpack integrations

//ViewModel
class ExampleViewModel @ViewModelInject constructor(
  private val repository: ExampleRepository,
  @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  ...
}

//Activity / Fragment
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}

But how to use Hilt with ViewModelFactory?

It seems the answer is in the @Assisted but i can't figure out how

How to tell hilt i like it to inject repository interfaces to ViewModel while still allowing ViewModelFactory to initialize the ViewModel with parameters on run-time?

Upvotes: 34

Views: 27807

Answers (3)

In Jilt 2.44, at least, there is no @ViewModelInject. To inject into the constructor of a ViewModel the ViewMolde needs to be annotated with @HiltViewModel and for the constructor you can just use @Inject.

@HiltViewModel
class MyViewModel @Inject constructor(
    private val navigationHelper: NavHelper
) : ViewModel() 

Upvotes: 2

epic
epic

Reputation: 1443

courtesy of @Elye, next articles helped a lot. I recommend a read.

Passing Activity Intent Data to ViewModel through Injection

Injecting ViewModel with Dagger Hilt

It seems that mostly Factory is not needed since mostly viewmodel initial parameters are taken from previous fragment and can be accessed via SavedStateHandle which is automatically injected if marked as @Assisted

To set-up hilt i used the following code-labs tutorial

Using Hilt in your Android app

Then, viewModel injection is done automatically using only the next code

Note that as noted by fabioCollini here, it seems savedStateHandle can also get values from safe args by simply placing argument name as the key. In fact, that is what i did in the following example. ps: In an attempt to make the safe args be more "safe", i did try to replace the SavedStateHandle with ItemsFragmentArgs hoping it will work but the app did not compile. I do hope it will be implemented in the future (and if already, please let me know)

//ItemFragment file
@AndroidEntryPoint
class ItemsFragment : Fragment() {

    private val viewModel: ItemsViewModel by viewModels()

    //use viewModel as you would. No need to initialize.
}

//Module file - if you have any repository, remember to bind it 
//or provide the exact implementation as noted in code-labs
@InstallIn(ApplicationComponent::class)
@Module
abstract class DatabaseModuleBinder {

    @Binds
    abstract fun bindGlistRepository(impl: FirestoreGlistRepository): GlistRepository

}


//ItemsViewModel file - lastly, anotate as follows and take your arguments 
//from savedStateHandle (for safe args, use variable name as key)
class ItemsViewModel @ViewModelInject constructor(private val glistRepo: GlistRepository,
                     @Assisted private val savedStateHandle: SavedStateHandle) : ViewModel() {

    private val glistLiveDate = glistRepo.getGlistLiveData(
        savedStateHandle.get<String>("listId")!!
    )

..
}

Hope it helps anyone and if any mistake, please let me know

Upvotes: 30

Ravi
Ravi

Reputation: 181

Pass your ScoreViewModelFactory into built in ktx-extension of viewModel. Also you can consume Activity/Fragment arguments by using SavedStateHandle itself with defaultViewModelProviderFactory.

/*
Gradle Dependencies
def lifecycle_version = "2.2.0"
def hiltLifeVersion = "1.0.0-alpha01"
def hiltVersion = "2.28.1-alpha"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "com.google.dagger:hilt-android:$hiltVersion"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
implementation "androidx.hilt:hilt-work:$hiltLifeVersion"
implementation "androidx.hilt:hilt-common:1.0.0-alpha01"
kapt "com.google.dagger:hilt-android-compiler:$hiltVersion"
kapt "androidx.hilt:hilt-compiler:$hiltLifeVersion"
*/

import androidx.fragment.app.viewModels

@AndroidEntryPoint
class ExampleFragment : Fragment(R.layout.example_fragment) {

    //internally using defaultViewModelProviderFactory 
    private val viewModel : ExampleViewModel by viewModels()

    //or you own viewmodal factory instance --> scoreViewModelFactory
    private val viewModel : ExampleViewModel by viewModels { scoreViewModelFactory }

}

class ExampleViewModel @ViewModelInject constructor(
    private val repository: ExampleRepository,
    @Assisted override val savedStateHandle: SavedStateHandle
) : ViewModel() {

    //bundle args -> String, Int, Parcelable etc.. 
    private val arg1LiveData: MutableLiveData<String> = 
                         savedStateHandle.getLiveData("arg1", "")

}

In built ktx-extension of Fragment viewmodel

@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

Upvotes: 7

Related Questions