Use the same instance of view model in multiple fragments using dagger2 - android

I am using only dagger2 (not dagger-android) in my project. It's working fine to inject the ViewModel using multibinding. But there's one problem with that previously without dagger2 I was using the same instance of viewmodel used in activity in multiple fragments (using fragment-ktx method activityViewModels()), but now since dagger2 is injecting the view model it's always gives the new instance (checked with hashCode in each fragment) of the viewmodel for each fragment, that's just breaks the communication between fragment using viewmodel.
The fragment & viewmodel code is as below:
class MyFragment: Fragment() {
#Inject lateinit var chartViewModel: ChartViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
(activity?.application as MyApp).appComponent.inject(this)
}
}
//-----ChartViewModel class-----
class ChartViewModel #Inject constructor(private val repository: ChartRepository) : BaseViewModel() {
//live data code...
}
Here's the code for viewmodel dependency injection:
//-----ViewModelKey class-----
#MapKey
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
//-----ViewModelFactory class------
#Singleton
#Suppress("UNCHECKED_CAST")
class ViewModelFactory
#Inject constructor(
private val viewModelMap: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModelMap[modelClass] ?: viewModelMap.asIterable()
.firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("Unknown ViewModel class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
//-----ViewModelModule class-----
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(ChartViewModel::class)
abstract fun bindChartViewModel(chartViewModel: ChartViewModel): ViewModel
}
Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments.
Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.
One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity.
This is generated code for ChartViewModel which always create the newInstance of viewModel:
#SuppressWarnings({
"unchecked",
"rawtypes"
})
public final class ChartViewModel_Factory implements Factory<ChartViewModel> {
private final Provider<ChartRepository> repositoryProvider;
public ChartViewModel_Factory(Provider<ChartRepository> repositoryProvider) {
this.repositoryProvider = repositoryProvider;
}
#Override
public ChartViewModel get() {
return newInstance(repositoryProvider.get());
}
public static ChartViewModel_Factory create(Provider<ChartRepository> repositoryProvider) {
return new ChartViewModel_Factory(repositoryProvider);
}
public static ChartViewModel newInstance(ChartRepository repository) {
return new ChartViewModel(repository);
}
}

The problem is that when you inject the viewmodel like this
class MyFragment: Fragment() {
#Inject lateinit var chartViewModel: ChartViewModel
dagger simply creates a new viewmodel instance. There is no viewmodel-fragment-lifecycle magic going on because this viewmodel is not in the viewmodelstore of the activity/fragment and is not being provided by the viewmodelfactory you created. Here, you can think of the viewmodel as any normal class really. As an example:
class MyFragment: Fragment() {
#Inject lateinit var anything: AnyClass
}
class AnyClass #Inject constructor(private val repository: ChartRepository) {
//live data code...
}
Your viewmodel is equivalent to this AnyClass because the viewmodel is not in the viewmodelstore and not scoped to the lifecycle of the fragment/activity.
Is there any way to achieve the same instance of viewmodel for multiple fragment and also at the same time inject the view model in fragments
No. Because of the reasons listed above.
Also is there any need for the bindViewModelFactory method as it seems to have no effect on app even without this method.
It does not have any effect because (I'm assuming that) you are not using the ViewModelFactory anywhere. Since it's not referenced anywhere, this dagger code for the viewmodelfactory is useless.
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
Here's what #binds is doing: 1 2
That's why removing it has no effect on the app.
So what is the solution? You need to inject the factory into the fragment/activity and get the instance of the viewmodel using the factory
class MyFragment: Fragment() {
#Inject lateinit var viewModelFactory: ViewModelFactory
private val vm: ChartViewModel by lazy {
ViewModelProvider(X, YourViewModelFactory).get(ChartViewModel::class.java)
}
What is X here? X is ViewModelStoreOwner. A ViewModelStoreOwner is something that has viewmodels under them. ViewModelStoreOwner is implemented by activity and fragment. So you have a few ways of creating a viewmodel:
viewmodel in activity
ViewModelProvider(this, YourViewModelFactory)
viewmodel in fragment
ViewModelProvider(this, YourViewModelFactory)
viewmodel in fragment (B) scoped to a parent fragment (A) and shared across child fragments under A
ViewModelProvider(requireParentFragment(), YourViewModelFactory)
viewmodel in fragment scoped to parent activity and shared across fragments under the activity
ViewModelProvider(requireActivity(), YourViewModelFactory)
One workaround could be to make a BaseFragment for fragments which shares the common viewmodel, but that will again include the boilerplate code and also I am not a great fan of BaseFragment/BaseActivity
Yes, this is indeed a bad idea. The solution is to use requireParentFragment() and requireActivity() to get the viewmodel instance. But you'll be writing the same in every fragment/activity that has a viewmodel. To avoid that you can abstract away this ViewModelProvider(x, factory) part in a base fragment/activity class and also inject the factory in the base classes, which will simplify your child fragment/activity code like this:
class MyFragment: BaseFragment() {
private val vm: ChartViewModel by bindViewModel() // or bindParentFragmentViewModel() or bindActivityViewModel()

You can share ViewModel between fragments when instantiating if the fragments has the same parent activity
FragmentOne
class FragmentOne: Fragment() {
private lateinit var viewmodel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewmodel= activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} : throw Exception("Invalid Activity")
}
}
FragmentTwo
class FragmentTwo: Fragment() {
private lateinit var viewmodel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewmodel= activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
}

Add your ViewModel as PostListViewModel inside ViewModelModule:
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(PostListViewModel::class)
internal abstract fun postListViewModel(viewModel: PostListViewModel): ViewModel
//Add more ViewModels here
}
To end with, our activity will have ViewModelProvider.Factory injected and it will be passed to theprivate val viewModel: PostListViewModel by viewModels { viewModelFactory }
class PostListActivity : AppCompatActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel: PostListViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_list)
getAppInjector().inject(this)
viewModel.posts.observe(this, Observer(::updatePosts))
}
//...
}
For more check this post:Inject ViewModel with Dagger2 And Check github

Related

Bind ViewModel from main flavor to overridden one in another flavor with Dagger

What I have
A project with one flavor except the main one.
In the main flavor I have MyFragment and MyViewModel.
MyFragment takes the half of the screen and is displayed in another fragment which is just a container.
View model injection happens the following way:
class Fragment: Fragment() {
#Inject lateinit var viewModelFactory: ViewModelFactory
private val viewModel by viewModels<MyViewModel> { viewModelFactory }
}
class ViewModelFactory #Inject constructor(
private val viewModels: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}
In the second flavor I have MyFlavoredFragment which is displayed under MyFragment, in the same container fragment, and takes the other half of the screen.
In this fragment I need the already existed view model from MyFragment, so I try to get it by the following way:
class FlavoredFragment: Fragment() {
#Inject lateinit var viewModelFactory: ViewModelFactory
private val viewModel: MyFlavoredViewModel by viewModels(
ownerProducer = {
requireParentFragment()
.childFragmentManager
.findFragmentByTag(MyFragment.TAG)!!
},
factoryProducer = {
viewModelFactory
}
)
}
What I want
Not changing anything in MainFragment I want it starts using MyFlavoredViewModel over declared MyViewModel.
As well, I want the injected view model in MyFlavoredFragment be the exact same instance already created in MyFragment.
But it does not work as expected. When MyFlavoredFragment gets displayed, a new instance of MyFlavoredViewModel gets created.
In MyFragment the correct type MyFlavoredViewModel gets created over declared MyViewModel, though.
My dagger setup is:
#Singleton
#Component(
modules = [
MyModule::class
]
)
interface AppComponent {
fun inject(myFragment: MyFragment)
}
#Singleton
#Component(
modules = [
MyFlavoredModule::class
]
)
interface FlavoredAppComponent : AppComponent {
override fun inject(myFragment: MyFragment)
fun inject(flavoredFragment: FlavoredFragment)
}
#Module
abstract class MyModule {
#Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
abstract fun myViewModel(myViewModel: MyViewModel): ViewModel
}
#Module
abstract class MyFlavoredModule {
#Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
abstract fun myViewModel(myFlavoredViewModel: MyFlavoredViewModel): ViewModel
#Binds
#IntoMap
#ViewModelKey(MyFlavoredViewModel::class)
abstract fun myFlavoredViewModel(myFlavoredViewModel: MyFlavoredViewModel): ViewModel
}
I have also tried a solution with #Repeatable ViewModelKey, that didn't work either.
Is this even possible what I want to achieve?

Hilt - How to inject ViewModel interface?

Based on the Hilt tutorial, ViewModels needs to be inject the following way:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
However, in my case, I want to use an interface:
interface ExampleViewModel()
#HiltViewModel
class ExampleViewModelImp #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ExampleViewModel, ViewModel() {
...
}
Then I want to inject it via the interface
#AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
How to make this work?
viewModels requires child of ViewModel class
val viewModel: ExampleViewModel by viewModels<ExampleViewModelImp>()
Had a similar problem where I wanted to Inject the ViewModel via interface, primarily because to switch it with a fake implementation while testing. We are migrating from Dagger Android to Hilt, and we had UI tests that used fake view models. Adding my findings here so that it could help someone whose facing a similar problem.
Both by viewModels() and ViewModelProviders.of(...) expects a type that extends ViewModel(). So interface won't be possible, but we can still use an abstract class that extends ViewModel()
I don't think there is a way to use #HiltViewModel for this purpose, since there was no way to switch the implementation.
So instead, try to inject the ViewModelFactory in the Fragment. You can switch the factory during testing and thereby switch the ViewModel.
#AndroidEntryPoint
class ListFragment : Fragment() {
#ListFragmentQualifier
#Inject
lateinit var factory: AbstractSavedStateViewModelFactory
private val viewModel: ListViewModel by viewModels(
factoryProducer = { factory }
)
}
abstract class ListViewModel : ViewModel() {
abstract fun load()
abstract val title: LiveData<String>
}
class ListViewModelImpl(
private val savedStateHandle: SavedStateHandle
) : ListViewModel() {
override val title: MutableLiveData<String> = MutableLiveData()
override fun load() {
title.value = "Actual Implementation"
}
}
class ListViewModelFactory(
owner: SavedStateRegistryOwner,
args: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, args) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return ListViewModelImpl(handle) as T
}
}
#Module
#InstallIn(FragmentComponent::class)
object ListDI {
#ListFragmentQualifier
#Provides
fun provideFactory(fragment: Fragment): AbstractSavedStateViewModelFactory {
return ListViewModelFactory(fragment, fragment.arguments)
}
}
#Qualifier
annotation class ListFragmentQualifier
Here, ListViewModel is the abstract class and ListViewModelImpl is the actual implementation. You can switch the ListDI module while testing using TestInstallIn. For more information on this, and a working project refer to this article
Found a solution using HiltViewModel as a proxy to the actual class I wish to inject. It is simple and works like a charm ;)
Module
#Module
#InstallIn(ViewModelComponent::class)
object MyClassModule{
#Provides
fun provideMyClas(): MyClass = MyClassImp()
}
class MyClassImp : MyClass {
// your magic goes here
}
Fragment
#HiltViewModel
class Proxy #Inject constructor(val ref: MyClass) : ViewModel()
#AndroidEntryPoint
class MyFragment : Fragment() {
private val myClass by lazy {
val viewModel by viewModels<Proxy>()
viewModel.ref
}
}
Now you got myClass of the type MyClass interface bounded to viewModels<Proxy>() lifeCycle
It's so simple to inject an interface, you pass an interface but the injection injects an Impl.
#InstallIn(ViewModelComponent::class)
#Module
class DIModule {
#Provides
fun providesRepository(): YourRepository = YourRepositoryImpl()
}

How to use AssistedInject to pass dynamic value as a parameter to ViewModel when using Dagger2 in Android

I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}

Dagger 2: multi-module project, inject dependency but get "lateinit property repository has not been initialize" error at runtime

Dagger version is 2.25.2.
I have two Android project modules: core module & app module.
In core module, I defined for dagger CoreComponent ,
In app module I have AppComponent for dagger.
CoreComponet in core project module:
#Component(modules = [MyModule::class])
#CoreScope
interface CoreComponent {
fun getMyRepository(): MyRepository
}
In core project module, I have a repository class, it doesn't belong to any dagger module but I use #Inject annotation next to its constructor:
class MyRepository #Inject constructor() {
...
}
My app component:
#Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
#featureScope
interface AppComponent {
fun inject(activity: MainActivity)
}
In MainActivity:
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val coreComponent = DaggerCoreComponent.builder().build()
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
}
}
My project is MVVM architecture, In general:
MainActivity hosts MyFragment
MyFragment has a reference to MyViewModel
MyViewModel has dependency MyRepository (as mentioned above MyRepository is in core module)
Here is MyViewModel :
class MyViewModel : ViewModel() {
// Runtime error: lateinit property repository has not been initialize
#Inject
lateinit var repository: MyRepository
val data = repository.getData()
}
MyViewModel is initialized in MyFragment:
class MyFragment : Fragment() {
lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
...
}
}
When I run my app, it crashes with runtime error:
kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialize
The error tells me dagger dependency injection does't work with my setup. So, what do I miss? How to get rid of this error?
==== update =====
I tried :
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
Now when I run the app, I get new error:
Caused by: java.lang.InstantiationException: class foo.bar.MyViewModel has no zero argument constructor
====== update 2 =====
Now, I created MyViewModelFactory:
class MyViewModelFactory #Inject constructor(private val creators: Map<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
try {
#Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
I updated MyFragment to be :
class MyFragment : Fragment() {
lateinit var viewModel: MyViewModel
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onAttach(context: Context) {
// inject app component in MyFragment
super.onAttach(context)
(context.applicationContext as MyApplication).appComponent.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// I pass `viewModelFactory` instance here, new error here at runtime, complaining viewModelFactory has not been initialized
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
...
}
}
Now I run my app, I get new error:
kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized
What's still missing?
In order to inject dependencies Dagger must be either:
responsible for creating the object, or
ask to perform an injection, just like in the activities or fragments, which are instantiated by the system:
DaggerAppComponent
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)
In your first approach none of the above is true, a new MyViewModel instance is created outside Dagger's control:
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
therefore the dependency doesn't even get initialized. Additionally, even if you'd perform the injection more manually, like in the activity, the code still would fail, because you are trying to reference the repository property during the initialization process of the object val data = repository.getData(), before the lateinit var gets a chance to be set. In such cases the lazy delegate comes handy:
class MyViewModel : ViewModel() {
#Inject
lateinit var repository: MyRepository
val data by lazy { repository.getData() }
...
}
However, the field injection isn't the most desirable way to perform a DI, especially when the injectable objects needs to know about it. You can inject your dependencies into ViewModels using the construction injection, but it requires some additional setup.
The problem lies in the way view models are created and managed by the Android SDK. They are created using a ViewModelProvider.Factory and the default one requires the view model to have non-argument constructor. So what you need to do to perform the constructor injection is mainly to provide your custom ViewModelProvider.Factory:
// injects the view model's `Provider` which is provided by Dagger, so the dependencies in the view model can be set
class MyViewModelFactory<VM : ViewModel> #Inject constructor(
private val viewModelProvider: #JvmSuppressWildcards Provider<VM>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModelProvider.get() as T
}
(There are 2 approaches to implementing a custom ViewModelProvider.Factory, the first one uses a singleton factory which gets a map of all the view models' Providers, the latter (the one above) creates a single factory for each view model. I prefer the second one as it doesn't require additional boilerplate and binding every view model in Dagger's modules.)
Use the constructor injection in your view model:
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
And then inject the factory into your activities or fragments and use it to create the view model:
#Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
#featureScope
interface AppComponent {
fun inject(activity: MainActivity)
fun inject(fragment: MyFragment)
}
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: MyViewModelFactory<MyViewModel>
lateinit var viewModel: MyViewModel
override fun onAttach(context: Context) {
// you should create a `DaggerAppComponent` instance once, e.g. in a custom `Application` class and use it throughout all activities and fragments
(context.applicationContext as MyApp).appComponent.inject(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)[MyViewModel::class.java]
...
}
}
A few steps you'll need to use Dagger with the AAC ViewModel classes:
You need to use constructor injection in your ViewModel class (as you're doing in the updated question)
You will need a ViewModelFactory to tell the ViewModelProvider how to instantiate your ViewModel
Finally, you will need to tell Dagger how to create your ViewModelFactory
For the first step, pass the repository in the ViewModel constructor and annotate your view model class with #Inject:
class MyViewModel #Inject constructor(private val repository: MyRepository): ViewModel() {
val data = repository.getData()
}
For the second and third steps, one easy way to create a generic ViewModelFactory for any ViewModels that you will have in your project, and also tell Dagger how to use it you can:
Create a Singleton generic ViewModelFactory:
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
viewModels[modelClass]?.get() as T
}
Create a custom annotation to identify your ViewModels and let Dagger know that it needs to provide them:
#Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Create a new module for your ViewModels:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindsViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
// Add any other ViewModel that you may have
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
internal abstract fun bindsMyViewModel(viewModel: MyViewModel): ViewModel
}
Don't forget to declare the new module in your dagger component
And use the view model in your activity, instantiating it with the help of the ViewModelFactory:
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
...
}
}

How to inject a LifecycleOwner in Android using Dagger2?

I happen to have an Android lifecycle aware component with the following interface:
class MyLifecycleAwareComponent #Inject constructor(
private val: DependencyOne,
private val: DependencyTwo
) {
fun bindToLifecycleOwner(lifecycleOwner: LifecycleOwner) {
...
}
...
}
All Dagger specific components and modules are configured correctly and have been working great so far.
In each activity when I need to use the component I do the following:
class MyActivity: AppCompatActivity() {
#Inject
lateinit var component: MyLifecycleAwareComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
component.bindToLifecycleOwner(this)
...
}
}
Now I want to get rid of bindLifecycleOwner and denote my component like this:
class MyLifecycleAwareComponent #Inject constructor(
private val: DependencyOne,
private val: DependencyTwo,
private val: LifecycleOwner
) {
...
}
And provide the lifecycleOwner within the scope of individual activities (which implement the interface by extending AppCompatActivity).
Is there any way to do it with Dagger?
You may bind your Activity to LifecycleOwner from your ActivityModule:
#Module
abstract class ActivityModule {
...
#Binds
#ActivityScope
abstract fun bindLifecycleOwner(activity: AppCompatActivity): LifecycleOwner
...
}

Categories

Resources