Reputation: 21
class MovieListFragment : Fragment() {
@Inject
lateinit var movieListView: MovieListViewModel
private lateinit var movieListAdapter: MovieListAdapter
private lateinit var binding: ListFragmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMovieComponent.builder().appComponent(MovieListApp.component()).fragmentModule(FragmentModule(this)).build().inject(this)
}
This is the class I'm trying to have my viewmodel injected.
@Module (includes = [FragmentModule::class])
class MovieListModule(fragment: Fragment) {
private lateinit var movieListView : MovieListViewModel
@Provides
fun getMovieListViewModel(fragment: Fragment): MovieListViewModel {
movieListView = ViewModelProvider(fragment).get(MovieListViewModel::class.java)
return movieListView
}
} This is the class that has the module and lastly,
@Singleton
@Component(modules = [MovieModule::class,MovieListModule::class], dependencies = [AppComponent::class]))
interface MovieComponent {
fun inject(movieListViewModel : MovieListViewModel)
fun inject(movieDetailViewModel: MovieDetailViewModel)
fun inject(fragment : Fragment)
}
This is my component interface.
The app crashes, saying that the lateinit viewmodel that was supposed to be injected is not initialised. Is there a way around this?
Thank you in advance.
The error message:
2022-03-30 15:41:40.749 18607-18607/com.example.polyapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.polyapp, PID: 18607
java.lang.RuntimeException: Unable to create application com.example.polyapp.MovieListApp: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7487)
at android.app.ActivityThread.access$1700(ActivityThread.java:310)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8641)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
Caused by: java.lang.IllegalStateException: com.example.polyapp.movieDatabaseFeature.di.AppComponent must be set
at dagger.internal.Preconditions.checkBuilderRequirement(Preconditions.java:95)
at com.example.polyapp.movieDatabaseFeature.di.DaggerMovieComponent$Builder.build(DaggerMovieComponent.java:101)
at com.example.polyapp.MovieListApp.onCreate(MovieListApp.kt:15)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1211)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7482)
at android.app.ActivityThread.access$1700(ActivityThread.java:310)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2283)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8641)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
Upvotes: 2
Views: 1792
Reputation: 56
ViewModel injection on Android is tricky because ViewModels are created using ViewModelProvider
to ensure they survive configuration changes. If they're created with ViewModelProvider
, then how do you create it with Dagger? Luckily they both provide API's that can mesh together to solve your problem.
Dagger has Multibindings, and ViewModelProvider
has it's ViewModelProvider.Factory
API. Multibindings allow us to more finely tune when injection occurs by looking it up on a map first. The ViewModelProvider.Factory
will tell the ViewModelProvider
how to construct your ViewModel which allows for you to specify constructor parameters.
Here are the steps and explanations:
Annotate your MovieListViewModel constructor with @Inject. This will tell Dagger to put your MovieListViewModel on its' graph provided it can satisfy the constructor parameters(we won't be injecting it directly, we just need it on the Dagger graph). If there are no parameters, Dagger will handle it just fine.
import javax.inject.Inject
class MovieListViewModel @Inject constructor() {
...
}
Create a Multibinding for your MovieListViewModel. Instead of directly injecting your MovieListViewModel into the fragment, we want to wrap it in a special Dagger feature called a Multibinding. This will allow you to put your MovieListViewModel into a Map which can be injected and more finely manipulated at runtime(remember that ViewModelProvider.Factory
I mentioned?).
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
annotation class ViewModelKey(val value: KClass<out ViewModel>)
@Module
abstract class MovieListViewModelMultiBinder
{
@Binds // Tells Dagger to use the parameter value here, which extends ViewModel
@IntoMap // Tells Dagger to put this ViewModel implementation into a map. This requires you to provide a key which is known at compile time.
@ViewModelKey(MovieListViewModel::class) // Tells Dagger the key to use for this ViewModel. This is your ViewModel class.
// The parameter is the implementation to use when we request a `ViewModel`. Since this is a multibinding, multiple ViewModels can be bound.
fun bind(viewModel: MovieListViewModel): ViewModel
}
Create a ViewModelProvider.Factory
that injects and uses the Map mentioned in step 2. This uses a special Dagger type called a Provider
. Providers wrap your injected type and do not construct it until you call Provider.get()
to retrieve your object.
class DaggerViewModelFactory @Inject constructor(
private val viewModelProviders: Map<Class<out ViewModel>, Provider<ViewModel>>
): ViewModelProvider.Factory
{
override fun <T: ViewModel?> create(modelClass: Class<T>): T
{
return viewModelProviders[modelClass]?.get() as T
}
}
Use your custom ViewModelProvider.Factory
in your Fragments and Activities.
class MyActivity: Activity() {
private lateinit var viewModel: MovieListViewModel
@Inject lateinit var factory: DaggerViewModelFactory
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(MovieListViewModel::class.java)
}
}
For the sake of simplicity, I did not throw an error if the DaggerViewModelFactory
returns null for the modelClass
, but you should add one in case you forget to bind your ViewModel into the Multibinding.
Hope this helps.
Upvotes: 2