I've started using Dagger2, so there's still a lot to learn. I'm wondering if someone could point me on the right direction.
So, I've created a module for registering the view models used by my activities. It looks like this:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
#Binds
#IntoMap
#ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
ViewModelKey is a simple helper annotation class which looks like this:
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey (val value: KClass<out ViewModel>) {
}
The ViewModelModule is loaded by my main app component (used for creating the app):
#Singleton
#Component(
modules=[
AndroidSupportInjectionModule::class,
AppModule::class,
DatabaseModule::class,
NewsServiceModule::class,
JobBindingModule::class,
ViewModelModule::class,
PreferencesModule::class,
ActivityBindingModule::class
]
)
interface AppComponent: AndroidInjector<MyApp> {
#Component.Builder
abstract class Builder: AndroidInjector.Builder<MyApp>()
}
And here's the code for the ActivityBindingModule, responsible for setting up the subcomponents (in this case, activities used by my app):
#Module
abstract class ActivityBindingModule {
#ActivityScoped
#ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Internally, each activity is instantiating the view model with code that looks like this (called from within the onCreate method):
//view model code
_viewModel = ViewModelProviders.of(this, viewModelFactory)[ShowDetailsViewModel::class.java]
And, as you'd expect, viewModelFactory is injected as field:
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
Both view models have external dependencies which are set up on the other modules referenced by the top app component.
And, for the sake of completeness, here's the code for my view model factory:
#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
This code works, but it seems like it can be improved. After reading the docs, I'm under the impression that I could refactor my ViewModeModule so that it will simply instantiate my ViewModelFactory and move each of the view model declarations into separate module (so that each of them can be injected only in the "correct" activity).
In order to test this, I've started by moving the ShowDetailsViewModel into a new module which has only one entry:
#Module
internal abstract class DetailsModule {
#Binds
#IntoMap
#ViewModelKey(ShowDetailsViewModel::class)
abstract fun bindShowDetaislViewModel(viewModel: ShowDetailsViewModel): ViewModel
}
After that, the ViewModelModule looks like this:
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainActivityViewModel::class)
internal abstract fun bindMainActivityViewModel(viewModel: MainActivityViewModel): ViewModel
}
And I've updated the ActivityBindingModule so that in looks like this:
#Module
abstract class ActivityBindingModule {
#ActivityScoped
#ContributesAndroidInjector()
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector(modules = [DetailsModule::class])
internal abstract fun showDetailsActivity(): ShowDetailsActivity
}
Notice that now I'm passing the DetailsModule (which instantiates the ShowDetailsViewModel) to the ContributeAndroidInjector annotation which is applied to the showDetailsActivity method because that view model is only used by that activity.
Now, I'm surely missing something because after doing this, I'm always getting the following exception:
java.lang.IllegalStateException: ViewModelProviders.of(th…ilsViewModel::class.java] must not be null
If I debug the app, I can see that moving the ShowDetailsViewModel into its own model does not register it on the map used by the factory (ie, the map has only one entry, which corresponds to the MainActivityViewModel that is registered in the ViewModelModule.
I thought that moving each view model the declaration into each a module used by a subcomponent should still allow it to be registered in a map injected by a module which is registered with the top component. Am I wrong? What is it that I'm missing to make this work?
Thanks.
The problem lies with ViewModelFactory being #Singleton and that it won't get any of the bindings you add in your subcomponents. Remove the scope from your factory or make it #ActivityScoped (the same scope as the ViewModel for the Activity)
The Activity (#ActivityScoped) has access to the factory (#Singleton), but the factory (#Singleton) does not have access to use or create the ViewModel from a lower scope (#ActivityScoped). So moving the factory to the same scope (#ActivityScoped) would give it access to create the viewmodel in question.
Related
In Dagger 2, Is putting All ViewModels inside AppComponent is the right place.
Because I check this android google sample, All ViewModel scoped in the app component but I think the view model should be in it is view (Activity, Fragment) scope/subcomponent?
something like this:
#ContributesAndroidInjector(modules = [LoginActivityModule::class])
abstract fun contributeLoginActivity() : LoginActivity
-
#Module
abstract class LoginActivityModule {
#Binds
#IntoMap
#ViewModelKey(LoginViewModel::class)
abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel
}
To avoid memory-leak
I need explanation.
I am trying to add dagger-android to a Kotlin project and got confused when it is requied to create a Module and when it is sufficient to just declare an Inject contructor.
Assume there is the following dependency graph:
Activity
-> ViewModel
-> Repository
-> Webservice
-> Dao
-> Database
-> Application
To provide ViewModel for the Activity we create respective modules for the activity and ViewModel factory, and then create the ViewModel in the Activity manually like so:
#Module
abstract class ActivityModule {
#ContributesAndroidInjector
abstract fun mainActivity(): MainActivity
}
// Skiping ViewModelKey and ViewModelFactory code for brevity
#Module
abstract class ViewModelModule {
#Binds
internal abstract fun bindViewModelFactory(
factory: ViewModelFactory
): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainViewModel::class)
internal abstract fun mainViewModel(viewModel: MainViewModel): ViewModel
}
class MainActivity : DaggerAppCompatActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(HomeViewModel::class.java)
}
}
To provide Repository for the ViewModel we just declare #Inject constructor like so:
class MainViewModel #Inject constructor(private val repository: Repository): ViewModel() {}
To provide Webservice and Dao for the Repository as well as Database for the Dao we create respective Modules like so:
#Module
class NetworkModule {
#Provides
#Singleton
fun provideWebservice() = Webservice.create()
}
interface Webservice {
...
companion object Factory {
fun create(): Webservice {
...
return retrofit
}
}
}
#Module
class DataModule {
#Provides
#Singleton
fun provideApplicationDatabase(app: Application) =
AppDatabase.getDatabase(app)
#Provides
#Singleton
fun provideUserDao(db: AppDatabase) = db.userDao()
}
#Database(...)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
fun getDatabase(context: Context): AppDatabase {
...
return instance
}
}
}
And the Application is provided for the Dabatabase by some magic in the AppComponent and the Application class
#Singleton
#Component(modules = [
AndroidSupportInjectionModule::class,
NetworkModule::class,
DataModule::class,
ViewModelModule::class,
ActivityModule::class
])
interface AppComponent: AndroidInjector<App> {
#Component.Builder
interface Builder {
#BindsInstance
fun create(application: Application): Builder
fun build(): AppComponent
}
}
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
DaggerAppComponent.builder().create(this).build()
}
The questions are:
How does the Database get the Application instance? Is it AndroidSupportInjectionModule which does the magic?
Why do we need to create Modules for the Webservice and the Database but not the Repository?
Is it possible to annotate the Webservice interface and the Database class themselves to skip creating separate dagger modules for them?
Question 1 How does the Database get the Application instance? Is it AndroidSupportInjectionModule which does the magic?
Answer: No it is not the work of AndroidSupportInjectionModule. AndroidSupportInjectionModule is included in Dagger Android Support which helps in
"Configures bindings to ensure the usability of dagger.android and dagger.android.support framework classes." Found Here
So, basically you just pass the Application context when you're creating Dagger Builder you just pass it from Application Class now Once you got it in the Main Component you have the Application context in your all Modules and we need context when we initialize Room Database.
Question 2 Why do we need to create Modules for the Webservice and the Database but not the Repository? Is it possible to annotate the Webservice interface and the Database class themselves to skip creating separate dagger modules for them?
Answer:
First, always try to achieve constructor Injection. The general idea of Modules is "If we don't own the class we can't #annotate its constructor so we make Modules to provide their implementation". also if we want to inject Interface we can achieve it via its implementation class by its constructor Injection.
So we don't own the initialization of WebService and Database that is why we create their Modules and provide them so we can get their instances in our repositories. We own our repository class so we can Inject them via constructor Injection.
I'm using the well-known Dagger-ViewModelFactory pattern to be able to inject a factory for all the ViewModel in all the activities.
#ActivityScope
class ViewModelFactory #Inject constructor(
private val creators: MutableMap<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")
return creator.get() as T
}
}
The problem I have is that when I inject the factory into an Activity Dagger fails because the providers of the objects for the ViewModels that I'm not going to use are not always accessible. They are not because the modules that contain the providers have not been added.
For example, I have a LogIn activity and a SignUp activity, and this is the way I add the subcomponents for them:
#ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
LogInModule::class,
BindLogInModule::class
])
#ActivityScope
internal abstract fun loginActivityInjector(): LoginActivity
#ContributesAndroidInjector(modules = [
ViewModelModule::class,
FirebaseModule::class,
SignUpModule::class,
BindSignUpModule::class
])
#ActivityScope
internal abstract fun signUpActivityInjector(): SignUpActivity
Please notice that when I create the subcomponent for SignUpActivity I do not add the Module LogInModule because I do not need the bindings in that Module.
The result is that I get the error
e: com.package.my.AppComponent.java:8: error: [Dagger/MissingBinding] com.package.my.login.domain.LogInAuthenticator cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector {
^
A binding with matching key exists in component: com.package.my.di.ActivityInjectorsModule_LoginActivityInjector$app_prodDebug.LoginActivitySubcomponent
com.package.my.login.domain.LogInAuthenticator is injected at
com.package.my.login.repository.LoginRepository(logInAuthenticator)
com.package.my.login.repository.LoginRepository is injected at
com.package.my.login.domain.LoginUseCase(loginRepository)
com.package.my.login.domain.LoginUseCase is injected at
com.package.my.login.presentation.LoginViewModel(loginUseCase)
com.package.my.login.presentation.LoginViewModel is injected at
com.package.my.di.ViewModelModule.provideLoginViewModel(viewModel)
java.util.Map,javax.inject.Provider> is injected at
com.package.my.di.ViewModelFactory(creators)
com.package.my.di.ViewModelFactory is injected at
com.package.my.di.ViewModelModule.bindViewModelFactory$app_prodDebug(factory)
androidx.lifecycle.ViewModelProvider.Factory is injected at
com.package.my.login.ui.SignUpActivity.viewModelFactory
com.package.my.login.ui.SignUpActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.package.my.di.AppComponent → com.package.my.di.ActivityInjectorsModule_SignUpActivityInjector$app_prodDebug.SignUpActivitySubcomponent]
This happens because LogInAuthenticator is provided by LogInModule.
Does this mean that the only solution is to add LogInModule even if I don't really need to create GoogleSignInClient in the SignUpActivity?
You have declared both of #ContributesAndroidInjector methods to be dependent on ViewModelModule. Inside ViewModelModule you have declared all of the ViewModels out there, which means, that at the point when Dagger wants to construct the dependency tree for SignUpActivity it will also require you to explicitly mention how LoginViewModel should be constructed. This happens, because Dagger needs to know how each of the dependency declared inside ViewModelModule should be constructed.
The solution for you case will be either include all of the modules in all of #ContributesAndroidInjector declarations (which is an ugly approach), or, alternatively, move the provider method of SignUpViewModel to SignUpModule and do not include ViewModelModule for SignUpActivity declaration.
Here's the setup that works for me.
First, I have created a BaseActivityModule, which all of feature modules should include in their dedicated #Module classes:
#Module
abstract class BaseActivityModule {
#Binds abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
Then, assuming we have 2 features: Foo and Bar:
#Module
abstract class ActivitiesModule {
#PerActivity #ContributesAndroidInjector(modules = [FooModule::class])
abstract fun contributesFooActivity(): FooActivity
#PerActivity #ContributesAndroidInjector(modules = [BarModule::class])
abstract fun contributesBarActivity(): BarActivity
}
The implementation class of ViewModelProvider.Factory should be scoped with #PerActivity because the same instance of ViewModelProvider.Factory should be provided each time that dependency is needed to be injected in the scope of particular activity:
private typealias ViewModelProvidersMap = Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
#PerActivity
class MyViewModelFactory #Inject constructor(
private val creators: ViewModelProvidersMap
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var viewModelProvider = creators[modelClass]
if (viewModelProvider == null) {
val entries = creators.entries
val mapEntry = entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
} ?: throw IllegalArgumentException("Unknown model class $modelClass")
viewModelProvider = mapEntry.value
}
try {
#Suppress("UNCHECKED_CAST")
return viewModelProvider.get() as T
} catch (e: Throwable) {
throw IllegalArgumentException("Couldn't create ViewModel with specified class $modelClass", e)
}
}
}
Where #PerActivity is declared this way:
#Scope
#Retention(AnnotationRetention.RUNTIME)
annotation class PerActivity
FooModule and BarModule are declared as such:
#Module(includes = [BaseActivityModule::class])
abstract class FooModule {
#Binds #IntoMap #ViewModelKey(FooViewModel::class)
abstract fun bindsFooViewModel(viewModel: FooViewModel): ViewModel
}
#Module(includes = [BaseActivityModule::class])
abstract class BarModule {
#Binds #IntoMap #ViewModelKey(BarViewModel::class)
abstract fun bindsBarViewModel(viewModel: BarViewModel): ViewModel
}
Then we are including ActivitiesModule in the AppComponent as such:
#Singleton
#Component(modules = [
AndroidInjectionModule::class,
ActivitiesModule::class
])
interface AppComponent {
...
}
With this approach we've moved the ViewModelProvider.Factory creation one layer down: previously it was in the topmost AppComponent and now each of subcomponents will take care of creating the ViewModelProvider.Factory.
The answer to why you need to add the LoginModule lies in your error log. The error log traces the dependencies as below:
SignUpActivity <--is injected at-- ViewModelFactory <--is injected at-- LoginViewModel <--is injected at-- LoginUseCase <--is injected at-- LoginRepository <--is injected at-- LoginAuthenticator
The map above shows that you need to add LoginModule because Dagger needs it to successfully inject your ViewModelFactory into your SignUpActivity.
EDIT
Move SignUpViewModel binding from your ViewModelModule and place it in your SignUpModule like below:
#Module(includes = [SignUpModule.BindsModule::class])
class SignUpModule {
// your other provides methods
#Module
interface BindsModule{
#Binds
#IntoMap
#ViewModelKey(SignUpViewModel::class)
fun signUpViewModel(signUpViewModel: SignUpViewModel): ViewModel
}
}
Then add subcomponents this way. Notice ViewModelModule has been excluded
#ContributesAndroidInjector(modules = [
FirebaseModule::class,
SignUpModule::class,
BindSignUpModule::class
])
#ActivityScope
internal abstract fun signUpActivityInjector(): SignUpActivity
Why can't I inject interface types in ViewModel constructors when using Dagger Android?
Here's my AppComponent
#Singleton
#Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
ActivityBuilder::class,
ViewModelModule::class
]
)
interface AppComponent {
fun inject(app: App)
}
Here's the module for my activities:
#Module
abstract class ActivityBuilder {
#ActivityScope
#ContributesAndroidInjector(modules = [UserDetailsModule::class])
abstract fun userDetailsActivity(): UserDetailsActivity
}
Here's the UserDetailsModule
#Module
abstract class UserDetailsModule {
#Binds
#ActivityScope
abstract fun providesUserRepository(repository: UserRepositoryImpl): UserRepository
}
Here's the ViewModelModule where I follow the dynamic view model factory solution.
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(UserDetailsViewModel::class)
abstract fun userDetailsViewModel(viewModel: UserDetailsViewModel): ViewModel
#Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
Here's the concrete class of UserRepository
class UserRepositoryImpl #Inject constructor(private val api: Api) : UserRepository { ... }
Here's the UserDetailsViewModel
class UserDetailsViewModel #Inject constructor(private val userRepository: UserRepository) : ViewModel() { ... }
When I compile, it will error
UserRepository cannot be provided without an #Provides-annotated method.
However, the confusing part is when I change UserDetailsViewModel's constructor to receive UserRepositoryImpl instead of type UserRepository, it compiles successfully and it works.
Anyone knows what the problem might be?
Solved the issue. Using a generic view model factory found in this Github issue:
https://github.com/google/dagger/issues/1273#issuecomment-447997439
I am using dependency injection to provide global objects (#Singleton) and non global objects for activities only (#ActivityScoped).
Now I wonder if I did it right and if it could be done better. The most interesting part of this DI implementation is the injection of the object SomeManager into 2 different activities with restricted scope
Here is the code
The main app component
#Singleton
#Component(modules = [
ApplicationModule::class,
AndroidSupportInjectionModule::class,
ActivityModule::class,
ManagerModule::class,
...
ClientModule::class])
interface AppComponent : AndroidInjector<App> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): AppComponent.Builder
fun build(): AppComponent
}
}
The appclication module
#Module
abstract class ApplicationModule {
#Binds
#Singleton
internal abstract fun bindContext(application: Application): Context
}
The module for the activities
#Module
abstract class ActivityModule {
#ActivityScoped
#ContributesAndroidInjector(modules = [MainActivityModule::class])
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector(modules = [LoginActivityModule::class])
internal abstract fun loginActivity(): LoginActivity
}
And now I want to inject a new SomeManager to the LoginActivity and a new one to the MainActivity.
The approach is having a module for each activity like you see above in the #ContributesAndroidInjector(modules... annotation. The implementations of the 2 files look like this
#Module
object MainActivityModule {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideSomeManager(activity: MainActivity, apiClient: ApiClient) =
SomeManager(activity, apiClient)
}
And
#Module
object LoginActivityModule {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideSomeManager(activity: LoginActivity, apiClient: ApiClient) =
SomeManager(activity, apiClient)
}
The Question:
1) Now the LoginActivityModule and MainActivityModule look very similar. Is there a better approach to provide SomeManager to both activities without making them #Singleton and withouth creating a module for each activity (becaues SomeManager only needs an Activity, not a special sublcass)? I had something in mind like that it only takes an Activity instead of a specific XXXActivity. But how can I tell dagger to provide the XXXActivity as Activity
2) And beside that optimizing in 1), is this a correct implementation?
Update 1
I have solved it by the following implementation. Is this the right way to do this?
Module that provides the MainActivity as Activity
#Module
object MainActivityModule {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideAsActivity(activity: MainActivity): Activity = activity
}
Module that provides the MainActivity as Activity
#Module
object LoginActivityModule {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideAsActivity(activity: LoginActivity): Activity = activity
}
A Manager module that is only #ActivityScoped
#Module
object ManagerModule2 {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideSomeManager(activity: Activity, apiClient: ApiClient) =
SomeManager(activity, apiClient)
}
And the Android injector for the activities
#Module
abstract class ActivityModule {
#ActivityScoped
#ContributesAndroidInjector(modules = [ManagerModule2::class, MainActivityModule::class])
internal abstract fun mainActivity(): MainActivity
#ActivityScoped
#ContributesAndroidInjector(modules = [ManagerModule2::class, LoginActivityModule::class])
internal abstract fun loginActivity(): LoginActivity
}
Now the LoginActivityModule and MainActivityModule look very similar. Is there a better approach to provide SomeManager to both activities without making them #Singleton? I had something in mind like that it only takes an Activity instead of a specific XXXActivity.
Yes, you could do that. You can replace the dependency on the specific Activity and replace it with Activity or Context (depending on your actual needs) and move that declaration into a separate module, which you could include in both of your ActivityModules.
#Module
object SomeManagerModule {
#JvmStatic
#Provides
#ActivityScoped
internal fun provideSomeManager(activity: Activity, apiClient: ApiClient) =
SomeManager(activity, apiClient)
}
And either include it with the module or add it to ContributesAndroidInjector.
#Module(includes = [SomeManagerModule::class])
object MainActivityModule { /* ... */ }
// or
#ActivityScoped
#ContributesAndroidInjector(modules = [LoginActivityModule::class, SomeManagerModule::class])
internal abstract fun loginActivity(): LoginActivity
And you could even remove the need for a module completely by using Constructor Injection.
#ActivityScoped
SomeManager #Inject constructor(activity: Activity, apiClient: ApiClient)
Either way you would have to bind / provide your xxxActivitys as a Activity somewhere so that Dagger can find them.
And beside that optimizing in 1), is this a correct implementation?
Looks good to me. You said you wanted a new manager per Activity, so #ActivityScoped seems the correct choice. You could possibly remove the scope completely if you don't have to ensure that there is only ever one per Activity-scoped component, but this depends on your exact usecase.