Single instance for different scopes with dagger 2 - android

Problem
I am building an app with a dynamic feature.
To provide all the dependencies to the main module and the feature module I am using dagger 2. The feature component is depending on the main component and because of that, the feature component is having a different scope than the main component scope (#Singleton in that case)
One of the interface injected in the main module are implemented on the feature module and provided by reflection in the main module. The implementation is also used in the feature module.
The problem that I have is that the instance provided in the main module is different from the one in the feature module (because of the scopes) but I would like to have just one instance provided with dagger.
Code
Here some code and you can find the whole example project in github
Dagger configuration for the main module:
TestModule.kt
#Module
class TestModule {
#Provides
#Singleton
fun provideTestA() : TestA = TestAImplementation()
private var testCProvider: TestC?= null
#Provides
#Singleton
fun provideTestC(testComponent: TestComponent) : TestC {
if(testCProvider != null) return testCProvider as TestC
val provider = Class.forName("com.example.feature.services.TestCImplementation\$Provider").kotlin.objectInstance as TestC.Provider
return provider
.get(testComponent)
.also { testCProvider = it }
}
}
TestComponent.kt
#Singleton
#Component(modules = [TestModule::class])
interface TestComponent {
fun inject(activity: MainActivity)
fun provideTestA() : TestA
}
Dagger configuration for the feature module:
TestDependencyModule.kt
#Module
class TestDependencyModule {
#Provides
#TestScope
fun provideTestB(): TestB = TestBImplementation()
#Provides
#TestScope
fun provideTestC(testB: TestB): TestC = TestCImplementation(testB)
}
TestDependencyComponent.kt
#TestScope
#Component(
modules = [TestDependencyModule::class],
dependencies = [TestComponent::class]
)
interface TestDependencyComponent {
fun inject(receiver: TestBroadcastReceiver)
fun testC(): TestC
}
Expected result
The interfaces TestC and TestA are injected in the MainActivity
The interfaces TestB and TestA are injected in the TestBroadcastReceiver
As expected the instance of the TestA implementation is unique but for the implementation of the TestB is not that way. As TestC depends on TestB the one injected in TestC is different from the one injected in the TestBroadcastReceiver with the #TestScope annotation.
So running the example project that you can find here I get the following log output
Instances injected in the MainActivity
D/TestB: instance 40525431
D/TestC: instance 119319268
D/TestA: instance 60713805
Instances injected in the TestBroadcastReceiver
D/TestB: instance 219966227
D/TestA: instance 60713805
I would like to share the same instance of TestB in both modules.
Any suggestion?
Thanks in advance!

TestDependencyComponent can not access TestC from the component dependency on TestComponent because TestComponent does not expose TestC on its public API. If you add fun testC(): TestC on TestComponent I expect you will then get a duplicate binding exception when processing TestDependencyComponent. From there you will need to determine the right way to provide the TestC instance.

I was building two instances of the DaggerTestDependencyComponent one in the Injector and another different one when you are implementing TestC
The solution that I found was the following:
Create an object where I can instantiate the TestDependencyComponent that will be shared with the Injector and the TestCImplementation
object FeatureInjector {
val testDependencyComponent: TestDependencyComponent by lazy {
DaggerTestDependencyComponent.builder()
.testComponent(com.example.daggertest.dagger.Injector.testComponent)
.build()
}
}
Now I modified my feature Injector like that:
object Injector {
lateinit var testDependencyComponent: TestDependencyComponent
#JvmStatic
internal fun getTestDependencyComponent(): TestDependencyComponent {
if (!::testDependencyComponent.isInitialized) {
testDependencyComponent = FeatureInjector.testDependencyComponent
}
return testDependencyComponent
}
}
And the TestCImplementation as follow:
class TestCImplementation #Inject constructor(
private val testB: TestB
) : TestC {
override fun testCFun() {
testB.testBFun()
Log.d("TestC", "instance ${System.identityHashCode(this)}")
}
companion object Provider : TestC.Provider {
override fun get(testComponent: TestComponent): TestC {
return FeatureInjector.testDependencyComponent.testC()
}
}
}
Running the code now I am getting the same instance of the TestB

Related

Hilt not finding service

I'm trying to understand HILT and I'm creating a sample project, I'm used to creating modules and components, but now with a hilt, there's the InstallIn() and perhaps I have to play with them to do the same as before. For instance, I'm used to creating a module per feature as :
LoginModule (contains: ViewModel, activity/fragment, data source, use case, etc...)
ProductModule (contains: ViewModel, activity/fragment, data source, use case, etc...)
From now I've created a NetworkModule with hilt :
#Module
#InstallIn(ApplicationComponent::class)
object NetworkModule {
#Provides
fun provideRetrofit(httpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(httpClient)
.build()
#Provides
fun provideOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
#ErrorInterceptorOkHttpClient errorInterceptor: Interceptor,
): OkHttpClient = OkHttpClient()
.newBuilder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(errorInterceptor)
.build()
#ErrorInterceptorOkHttpClient
#Provides
fun provideErrorInterceptor(): Interceptor = ErrorInterceptor()
#Provides
fun provideHttpLogginInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = when {
BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.NONE
}
return loggingInterceptor
}
#Provides
fun provideMyService(retrofit: Retrofit): MyService =
retrofit.create(MyService::class.java)
}
And then I have the feature or let's say part of the app that is the home where I want to show some things from now a List, and I've created this :
#Module
#InstallIn(FragmentComponent::class)
object MainActivityModule {
#Provides
fun proviceDataSource(
myService: MyService,
myMapper: MyMapper,
): MyDatasource = MyDatasourceImpl(myService, myMapper)
#Provides
fun provicesMyMapper(): MyMapper = MapperImpl()
}
And in my Activity, I've added the
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
And in Fragment, I don't have anything maybe this is the thing that I'm doing wrong?
And when to use the ApplicationComponent SingletonComponent , etc?
Since I was using dagger (not dagger-android) I had a ComponentFactory where I had all of this Components as (LoginComponent, GoogleMapComponent, ReferalCodeComponent) all of those were a feature (talking about multi-module project) and there I was creating the factory of these components, how's the way of doing this now with Hilt?
When using multiple modules, if I follow my idea, I will define multiple modules in a library and provide them to the interface module for use.
Your question is how to use the factory method you defined in the interface module? I don't understand your question very well on this point.
If you use the MVVM architecture, you can inject these components into the ViewModel . The Flow provided by these components (such as LoginComponent) can be accessed in the ViewModel. Of course, the parameters injected through the ViewModel can also easily access the corresponding methods of the components you provide.
Below is the ViewModel in my Demo:
#HiltViewModel
class NickNameViewModel #Inject constructor(
private val repo: DataRepo,
application: Application,
): AndroidViewModel(application) {
fun updateUserNickName(
userId: String,
userKey: String,
userNickName: String,
resultCallBack: (Boolean, String) -> Unit = {_, _ -> },
) {
viewModelScope.launch {
repo.updateUserInfo(
userId = userId,
userKey = userKey,
userNickName = userNickName,
).collect {
//.....
}
}
}
}
}
In the above ViewModel, the DetaRepo component is injected into the ViewModel through construction injection.
The method provided in DataRepo can be easily accessed in the method updateUserNickName.
Below is the code of ViewModel in Fragment:
#AndroidEntryPoint
class NickNameModifyFragment: Fragment(R.layout.nick_name_modify_fragment) {
companion object {
const val nickNickNameResult = "nickNickNameResult"
}
private var _binding: NickNameModifyFragmentBinding? = null
private val viewModel by viewModels<NickNameViewModel>()
}
It should be noted that if Hilt is in Fragment's ViewModel, you still need to add the #AndroidEntryPoint annotation to the attach Activity.
Hope to bring you good luck.
If your using SingletonComponent then with the #Provides annotation you should also required to use #singleton annotation
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton #Provides
fun provideServiceGenerator(
context: Context
) = ServiceGenerator(context)
}
similarly For FragmentComponent you need annotate with
#Provides #FragmentScoped
as I read you mentioned UseCase and other stuff u must check out hilt viewModel because it will auto generate a lot boilerplate code for you and handles dependency according to activity or fragment lifecycle
#Module
#InstallIn(ViewModelComponent::class)
object ViewModelModule {
//repo
#Provides
#ViewModelScoped
fun providesTicketsRepo(
checkOutApi: CheckOutApi
)= TicketsRepo(checkOutApi)
}
#HiltViewModel
class TicketsAndCouponViewModel
#Inject constructor(
val ticketsRepo: TicketsRepo
) : ViewModel() { /****/}
inside fragment and activity
val viewModel by viewModels<TicketsAndCouponViewModel>()

Hilt - How can I inject dependencies into adapter?

I am providing dependency in module:
#Provides
#Singleton
fun provideImageUtil(#ImageUrl imageUrl: String): ImageUtil = GlideImageUtil(imageUrl)
I am trying to inject it into RecyclerView adapter:
class MainAdapter(private val goods: ArrayList<GoodItem>) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {
#Inject
lateinit private var imageUtil: ImageUtil
I used to inject this way using Dagger:
object Injector {
lateinit var appComponent: AppComponent
fun initAppComponent(context: Context){
if(context is Activity){
throw IllegalStateException("pass an Application as an argument to avoid memory leaks")
}
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(context))
.build()
}
}
In adapter:
init {
Injector.appComponent.inject(this)
}
How can I inject dependency into the adapter using Hilt? As I understood now "appComponent" is generated by Hilt. How can I access it?
First create EntryPoint in your custom class
#EntryPoint
#InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun getImageUtil(): ImageUtil
}
It is simple interface with #EntryPoint annotation. Since your dependency (ImageUtil) is singleton you should use #InstallIn(SingletonComponent::class) annotation to declare component. Finally declare a method to get your dependency fun getImageUtil(): ImageUtil
You can get your dependency in init block of your Adapter
init {
val myEntryPoint = EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
imageUtil = myEntryPoint.getImageUtil()
}
Full code
class MainAdapter(
context: Context,
private val goods: ArrayList<GoodItem>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {
var imageUtil: ImageUtil
#EntryPoint
#InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun getImageUtil(): ImageUtil
}
init {
val myEntryPoint = EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
imageUtil = myEntryPoint.getImageUtil()
}
}
See also https://developer.android.com/training/dependency-injection/hilt-android#not-supported
You can take a look at Assisted Injection with Dagger 2 which allows you to pass in types that might not be available initially and needs to be passed later. https://dagger.dev/dev-guide/assisted-injection.html
You'll have to add an #AndroidEntryPoint and likely will have to inject the constructor,because the imageUrl needs to come from somewhere; for example from Gradle:
javaCompileOptions {
annotationProcessorOptions {
arguments += ["imageUrl": "..."]
}
}
And I think #Singleton #Provides / #Singleton #Binds annotations do require a scope.

I can't inject Dao to a Repository Class, with HILT, in Android

i follow this tutorial Room Dependency Injection - MVVM To-Do List App with Flow and Architecture Components #4
Now I want to convert, a hole app that I have with Room, to this Clean Architecture.
In the tutorial Florian uses DI, to inject TaskDao into TaskViewModel, but I have a Repositories clases.
So I get to a point where the app is build without errors.
and this is my Repository:
class AnioRepository constructor(
private val agrotrackerApi: AgrotrackerApi
) {
val TAG = "AnioRepository"
//val anioDao: AnioDao
fun downloadAnios(): AniosResponse? {
GlobalScope.launch(Dispatchers.IO) {
val result = agrotrackerApi.getAnios()
if (result.isSuccessful) {
for (anio in result.body()!!){
Log.d(TAG, anio.toString())
}
}
}
return null
}
fun getAnios() {
//anioDao.getListAnios()
}
}
and this is my RepositoryModule:
#Module
#InstallIn(ApplicationComponent::class)
object RepositoryModule {
#Singleton
#Provides
fun providesAnioRepository( agrotrackerApi: AgrotrackerApi) : AnioRepository {
return AnioRepository(agrotrackerApi)
}
}
So I'm trying to add Dao class to the Repository Class, like this:
class AnioRepository constructor(
private val anioDao: AnioDao,
private val agrotrackerApi: AgrotrackerApi
) {
val TAG = "AnioRepository"
...
and then, change RepositoryModule, to match constructor...
...
fun providesAnioRepository( anioDao: AnioDao, agrotrackerApi: AgrotrackerApi) : AnioRepository
= AnioRepository(anioDao, agrotrackerApi)
...
but when I press Ctrl-F9, i get this error:
public abstract class ATMDatabase extends androidx.room.RoomDatabase {
^C:\pryectos\AndroidStudioProjects\ATMobileXKt\app\build\generated\source\kapt\debug\com\cse\atm\ATMApplication_HiltComponents.java:155:
error: [Dagger/MissingBinding] #com.cse.atm.di.ApplicationScope
kotlinx.coroutines.CoroutineScope cannot be provided without an
#Provides-annotated method. public abstract static class SingletonC
implements ATMApplication_GeneratedInjector,
^
#com.cse.atm.di.ApplicationScope kotlinx.coroutines.CoroutineScope is injected at
com.cse.atm.database.ATMDatabase.Callback(�, applicationScope)
com.cse.atm.database.ATMDatabase.Callback is injected at
com.cse.atm.di.AppModule.providesDatabase(�, callback)
com.cse.atm.database.ATMDatabase is injected at
com.cse.atm.di.AppModule.providesAnioDao(db)
com.cse.atm.database.AnioDao is injected at
com.cse.atm.di.RepositoryModule.providesAnioRepository(anioDao, �)
javax.inject.Provider<com.cse.atm.data.AnioRepository> is injected at
What means this error? What I'm missing ?
#ApplicationScope annotation is in another AppModule.kt, I dont' where is the problem.
Any help wil be appreciated!!
Best Regards
Your ATMDatabase.Callback is requesting a CoroutineScope with custom qualifier #ApplicationScope, but you are not providing such a CoroutineScope in any of your modules.
To provide a coroutine scope, the tutorial you linked adds this code around the 28-minute mark:
#Provides
#Singleton
fun provideApplicationScope() = CoroutineScope(SupervisorJob())
Since you are using the #ApplicationScope qualifier when requesting a coroutine scope, you will also need to add the qualifier to this #Provides method:
#Provides
#Singleton
#ApplicationScope
fun provideApplicationScope() = CoroutineScope(SupervisorJob())

How do I use HiltViewModelFactory in non-Android classes?

How can one go about injecting HiltViewModelFactory into non-Android components like Conductor's Controller.. etc. Currently I have this code but it doesn't work, saying HiltViewModelFactory cannot be provided.
#EntryPoint
#InstallIn(ActivityComponent::class)
interface MyClassInterface {
fun viewModelFactory(): HiltViewModelFactory
}
private val viewModelStore = ViewModelStore()
private val viewModelFactory by lazy {
EntryPoints.get(requireContext(), MyClassInterface::class.java).viewModelFactory()
}
You could probably do this:
#Module
#InstallIn(ActivityComponent::class)
class HiltViewModelModule {
#Provides fun viewModelFactory(activity: Activity): HiltViewModelProviderFactory =
(activity as AppCompatActivity).getDefaultViewModelProviderFactory() as HiltViewModelFactory
}
This should work correctly in any Activity that is annotated with #AndroidEntryPoint.

Is there a way to use injectors in a non-(Activity,Service,Fragment, Application) class

We're using Dagger2 in our application. I am trying to do a room database and I am writing the repository code, but I would like to inject application context and the DAO for the class.
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
Here's what I have:
class DownloadsDataRepositoryImpl : IDownloadsDataRepository, HasAndroidInjector {
#Inject
lateinit var androidInjector : DispatchingAndroidInjector<Any>
#Inject
lateinit var downloadsDao: DownloadsDao
override fun androidInjector(): AndroidInjector<Any> = androidInjector
init {
androidInjector()
}
}
But I'm sure it's not going to work. Is there a way to do it?
As stated, dagger-android is just a tool to help injecting specific framework classes that you can't have control on it's creation.
The proper approach is to use simple construction injection.
To be more direct on how you should expose it on your #Component, I would need more code, specifically on what you have on your activity/fragment, but here is a crude example (that I did not tested, if there are minor errors, you can fix them following the compiler error messages):
First, you will have some object that exposes your DAO. Probably it's room?
#Entity(tableName = "download_table")
data class DownloadEntity(
#PrimaryKey
val key: String
)
#Dao
interface DownloadsDao {
#Query("SELECT * FROM download_table")
fun load(): List<DownloadEntity>
}
#Database(
entities = [DownloadEntity::class], version = 1
)
abstract class DownloadRoomDatabase : RoomDatabase() {
abstract val downloadsDao: DownloadsDao
}
Now we will create a crude repository that is build with #Inject annotation. Dagger will take care of building this object for us. Notice that I am not using dagger-android for it:
interface IDownloadsDataRepository
class DownloadsDataRepositoryImpl #Inject constructor(
val downloadsDao: DownloadsDao
) : IDownloadsDataRepository
How to expose it to your activity/fragment/service requires more details on your implementation. For example, if it's inside a ViewModel or a Presenter that is annotated with #Inject or you are accessing directly on your activity will result in different implementations. Without more details, I will suppose that you are accessing the repository directly on your activity:
class DownloadActivity : FragmentActivity() {
#Inject
lateinit val repo: IDownloadsDataRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerDownloadComponent.factory().create(this).inject(this)
}
}
Now we need to instruct Dagger on how to:
Bind your concrete DownloadsDataRepositoryImpl to the IDownloadsDataRepository interface that the activity requires
How to provide the dependencies to build DownloadsDataRepositoryImpl
For this we will need a module:
#Module
abstract class RepositoryModule {
//We will bind our actual implementation to the IDownloadsDataRepository
#Binds
abstract fun bindRepo(repo: DownloadsDataRepositoryImpl): IDownloadsDataRepository
#Module
companion object {
//We need the database to get access to the DAO
#Provides
#JvmStatic
fun provideDataBase(context: Context): DownloadRoomDatabase =
Room.databaseBuilder(
context,
DownloadRoomDatabase::class.java,
"download_database.db"
).build()
//With the database, we can provide the DAO:
#Provides
#JvmStatic
fun provideDao(db: DownloadRoomDatabase): DownloadsDao = db.downloadsDao
}
}
With this, we can finish the last part of our puzzle, creating the #Component:
#Component(
modules = [
RepositoryModule::class
]
)
interface DownloadComponent {
fun inject(activity: DownloadActivity)
#Component.Factory
interface Factory {
fun create(context: Context): DownloadComponent
}
}
Notice that I did not use any dagger-android code, I don't think it's useful and causes more confusion than necessary. Stick with basic dagger2 constructs and you are fine. You can implement 99.9% of your app only understanding how those constructs works:
#Module, #Component and #Subcomponent
Edit: As stated in the comments, probably you will need to properly manage the scope of your repository, specially the DB creation if you are actually using Room.
Not sure how you implemented dagger, but here is an example how you can provide context to non activity class.
Suppose you have AppModule class, so there you can add provideContext() method:
#Module
class AppModule(app: App) {
private var application: Application = app
#Provides
fun provideContext(): Context {
return application
}
}
and here is non activity class written in Kotlin:
class Utils #inject constructor(private val context: Context) {
..
}
And that's it, just rebuild j
I have a feeling that you can only do Dagger injection in Fragments, Activities, Services, Applications, etc.
You were correct to assume that before Dagger-Android 2.20, but not after 2.20+.
Now you can create a #ContributesAndroidInjector for any class, which will generate an AndroidInjector<T> for that T for which you added #ContributesAndroidInjector.
This means that there is a multi-binding that allows you to get an AndroidInjector<T> for a T, and this is what HasAndroidInjector does for you.
So the following worked for me in a different scenario (for member-injecting Workers in work-manager, instead of creating a multi-binding and a factory):
#Keep
class SyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
init {
val injector = context.applicationContext as HasAndroidInjector
injector.androidInjector().inject(this)
}
#Inject
lateinit var apiService: ApiService
and
#ContributesAndroidInjector
abstract fun syncWorker(): SyncWorker
HOWEVER in your particular case, none of this is required.
Dagger-Android is for member-injecting classes using an auto-generated subcomponent, that you typically need only if your injected type is inside a different module, and therefore you can't directly add fun inject(T t) into your AppComponent, OR you don't see your AppComponent.
In your case, simple constructor injection is enough, as you own your own class.
#Singleton
class DownloadsDataRepositoryImpl #Inject constructor(
private val downloadsDao: DownloadsDao
): IDownloadsDataRepository {}
Which you can bind via a module
#Module
abstract class DownloadsModule {
#Binds
abstract fun dataRepository(impl: DownloadsDataRepositoryImpl): IDownloadsDataRepository
}
And otherwise you just create your component instance inside Application.onCreate()
#Component(modules = [DownloadsModule::class])
#Singleton
interface AppComponent {
fun dataRepository(): DownloadsDataRepository
#Component.Factory
interface Factory {
fun create(#BindsInstance appContext: Context): AppComponent
}
}
And
class CustomApplication: Application() {
lateinit var component: AppComponent
private set
override fun onCreate() {
super.onCreate()
component = DaggerAppComponent.factory().create(this)
}
}
Then you can get it as
val component = (context.applicationContext as CustomApplication).component
Though technically you may as well create an extension function
val Context.appComponent: AppComponent
get() = (applicationContext as CustomApplication).component
val component = context.appComponent

Categories

Resources