Closed philips77 closed 3 years ago
It is hard to say without the error message, but I think what is happening is you are injecting the ProjectComponent.Builder
outside of the UserComponent
.
the ProjectComponent needs to know the instance of UserComponent to be initialized. How can it be provided?
All DefineComponent
types use subcomponents, so the way the ProjectComponent
gets the UserComponent
instance is you have to get the ProjectComponent.Builder
from an instance of UserComponent
, either by injecting it in an object that came from the UserComponent
(most common) or using an entry point with an instance of the UserComponent
(less common).
Thank you for the response. Is there any example demonstrating this?
So far we were trying to get ProjectComponent.Builder
injected, like we do with UserComponent
, but of course this does not work.
@Singleton
class UserManager @Inject constructor(
// Since UserManager will be in charge of managing the UserComponent's
// lifecycle, it needs to know how to create instances of it. We use the
// provider (i.e. factory) Dagger generates for us to create instances of UserComponent.
private val userComponentProvider: Provider<UserComponent.Builder>,
) {
/**
* UserComponent is specific to a logged in user. Holds an instance of
* UserComponent. This determines if the user is logged in or not, when the
* user logs in, a new Component will be created.
* When the user logs out, this will be null.
*/
var userComponent: UserComponent? = null
private set
fun userLoggedIn(user: User, token: String) {
// When the user logs in, we create a new instance of UserComponent
userComponent = userComponentProvider.get().setUser(user).setToken(token).build()
}
fun logout() {
// When the user logs out, we remove the instance of UserComponent from memory
userComponent = null
}
}
So far we were trying to get ProjectComponent.Builder injected, like we do with UserComponent, but of course this does not work.
Can you post the entire error message? The error messages will contain a trace from the component entry point to the missing binding and other information that may help us identify what your issue is.
Can you also show an example of the code used to get the ProjectComponent.Builder
? It should look a lot like what you posted with the UserComponent
, but there are some easy mistakes to make like if you copied @Singleton
over instead of making your manager class @LoggedUserScope
then you'll get an error. Similarly, even if your manager class is unscoped, if you inject it from an @Singleton
type it will cause issues because the parent is still the SingletonComponent
and not the UserComponent
.
@bcorso Thanks for getting back
Here's the entire error as requested.
public abstract static class SingletonC implements HiltWrapper_ActivityRetainedComponentManager_ActivityRetainedComponentBuilderEntryPoint,
^
A binding with matching key exists in component: xx.xxx.android.ei.di.MainApplication_HiltComponents.UserC
javax.inject.Provider<xx.xxx.android.ei.di.ProjectComponent.Builder> is injected at
xx.xxx.android.ei.di.ProjectManager(component)
xx.xxx.android.ei.di.ProjectManager is injected at
xx.xxx.android.ei.viewmodels.DashboardViewModel(�, projectManager, �)
xx.xxx.android.ei.viewmodels.DashboardViewModel is injected at
xx.xxx.android.ei.viewmodels.DashboardViewModel_HiltModules.BindsModule.binds(vm)
@dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.String,javax.inject.Provider<androidx.lifecycle.ViewModel>> is requested at
dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [xx.xxx.android.ei.di.MainApplication_HiltComponents.SingletonC ? xx.xxx.android.ei.di.MainApplication_HiltComponents.ActivityRetainedC ? xx.xxx.android.ei.di.MainApplication_HiltComponents.ViewModelC]``
@Chang-Eric Here's the manager class for the ProjectComponent and it's basically a duplicate of the UserManager
class without being marked as Singleton
. As @philips77 has stated above, the idea is that once a user is logged in, they can select a project etc.
class ProjectManager @Inject constructor(
private val component: Provider<ProjectComponent.Builder>
) {
/**
* ProjectComponent is specific to a logged in user's projects. Holds an instance of
* ProjectComponent.
*/
var projectComponent: ProjectComponent? = null
private set
fun projectSelected(project: Project, keys: DevelopmentKeys) {
// we can call this to select a project when the user is logged in
projectComponent = component.get().setProject(project).setKeys(keys).build()
}
}
Edit: Added the two component class as well to make it easier for you.
@ProjectScope
@DefineComponent(parent = UserComponent::class)
interface ProjectComponent {
@DefineComponent.Builder
interface Builder {
fun setKeys(@BindsInstance keys: DevelopmentKeys): Builder
fun setProject(@BindsInstance project: Project): Builder
fun build(): ProjectComponent
}
}
@LoggedUserScope
@DefineComponent(parent = SingletonComponent::class)
interface UserComponent {
@DefineComponent.Builder
interface Builder {
fun setUser(@BindsInstance user: User): Builder
fun setToken(@BindsInstance string: String): Builder
fun build(): UserComponent
}
}
Thanks, this is helpful. So what is going on is that you are trying to inject the ProjectComponent
from a ViewModel, but the standard HiltViewModels don't come from your UserComponent
(since it is a custom component). (You can see the component path of the ViewModel at the very end of that error:
[xx.xxx.android.ei.di.MainApplication_HiltComponents.SingletonC ? xx.xxx.android.ei.di.MainApplication_HiltComponents.ActivityRetainedC ? xx.xxx.android.ei.di.MainApplication_HiltComponents.ViewModelC]
)
You'll need to inject the ProjectComponent.Builder
from a type that was injected from the UserComponent
.
Does this also mean that a manager class is not required for the ProjectComponent.Builder
at all in this case? We thought ProjectComponent.Builder
can be injected the same way as the UserComponent
is injected from a ViewModel
because it's also the parent of the ProjectComponent
. We'd be really grateful if you could share an example explaining this.
because it's also the parent of the ProjectComponent
Not sure I followed that, but the Builder
isn't the parent of the ProjectComponent
? Or do you mean that because UserComponent
is the parent, the child ProjectComponent
should be able to be injected the same as the parent?
In any case, you would need something like the following:
// This doesn't really have to be scoped, but it probably is best for it to be, especially if you
// presumably will be storing the ProjectComponent instances in this class.
@LoggedUserScope
public final class UserFoo {
@Inject UserFoo(ProjectComponent.Builder builder) { ... }
}
// Somewhere where you have your UserManager
UserFoo userFoo = userManager.userComponent().getUserFoo();
userFoo.buildProjectComponent();
The requirement is that the code that injects the ProjectComponent.Builder
has to be injected from the parent UserComponent
. If it is injected from somewhere else, like the Hilt ViewModelComponent
, then it won't work. The reason injecting the UserComponent.Builder
works is because the parent is the Hilt SingletonComponent
, so the parent is accessible from the ViewModelComponent
since ViewModelComponent
is a child of SingletonComponent
. ViewModelComponent
is not a child of UserComponent
however so that is why it does not work for ProjectComponent.Builder
.
Not sure I followed that, but the Builder isn't the parent of the ProjectComponent? Or do you mean that because UserComponent >is the parent, the child ProjectComponent should be able to be injected the same as the parent?`
What I meant was UserComponent
being the parent of the ProjectComponent
, but your sample is very helpful understanding the whole picture.
I still have one more thing to clarifiy here, you also did mention about using an EntryPoint with an instance of a UserComponent
(the less common version). How does that look like?
Really appreciate your support on this!
Thanks for your help, so we managed to solve our issue by injecting the ProjectManager
using the UserComponentEntryPoint
. Refer the source below, we decided to share it here as somebody else might benefit from our discussion.
@LoggedUserScope
@DefineComponent(parent = SingletonComponent::class)
interface UserComponent {
@DefineComponent.Builder
interface Builder {
fun setUser(@BindsInstance user: User): Builder
fun setToken(@BindsInstance string: String): Builder
fun build(): UserComponent
}
}
@ProjectScope
@DefineComponent(parent = UserComponent::class)
interface ProjectComponent {
@DefineComponent.Builder
interface Builder {
//fun setKeys(@BindsInstance keys: DevelopmentKeys): Builder
fun setProject(@BindsInstance project: Project): Builder
fun build(): ProjectComponent
}
}
@LoggedUserScope
class ProjectManager @Inject constructor(private val builder: ProjectComponent.Builder) {
/**
* ProjectComponent is specific to a logged in user's projects. Holds an instance of
* ProjectComponent.
*/
var projectComponent: ProjectComponent? = null
private set
fun projectSelected(project: Project) {
// a logged in user can call this to select a project.
//TODO include development keys in the builder
projectComponent = builder.setProject(project).build()
}
}
@EntryPoint
@InstallIn(UserComponent::class)
interface UserComponentEntryPoint {
fun userDataRepository(): UserDataRepository
fun getProjectManager(): ProjectManager
}
@ProjectScope
class ProjectDataRepository @Inject constructor(
val project: Project
)
There is one last thing I noticed that, ProjectDataRepository
did not needed to be scoped with @ProjectScope
(but still scoped it and seems like it had no effect of not being scoped?). Does this mean that @ProjectScope
is not a child scope of @LoggedUserScope
as I expected this to be automagically!
There is one last thing I noticed that, ProjectDataRepository did not needed to be scoped with @ProjectScope (but still scoped it and seems like it had no effect of not being scoped?).
A scope annotation just tells you whether or not the object should be cached. If you don't scope it, every time you ask for a ProjectDataRepository
you'll get a new instance. This doesn't affect the parent/child relationship of UserComponent
and ProjectComponent
. (Note that it is really the components that have the relationship. I think it gets confusing because colloquially people will often use the scope and component names interchangeably to mean the same thing, but most of the time it is technically the components you should be referring to as the scope annotation really just controls caching within that component).
Anyway, closing this now since it seems everything has been solved.
Hey @roshanrajaratnam,
I'm curious how you used the instance of ProjectComponent
to get an instance of ProjectDataRepository
?
Did you create an @EntryPoint
with @InstallIn(ProjectComponent::class)
?
@stavfx here's the link to the project where I used this. We ended up creating an entry point to the ProjectComponent class.
We're using Hilt in version 2.34.1-beta.
We have 2 components:
The
UserComponent
, withoutProjectComponent
works fine. We are able to get the instance usingEntryPoint
. Also, when theProjectComponent
has a parent set toSingletonComponent
, everything is OK. But we'd like to have them nested. A logged user can choose a project, but after logging out projects are not available.With the above code the compiler screams that we need
@Provide-annotated
method for theProjectComponent.Builder
. As I understand it correctly, theProjectComponent
needs to know the instance ofUserComponent
to be initialized. How can it be provided? All samples we found are using a parent component without any parameters, so the compiler can instantiate it automagically.CC: @roshanrajaratnam