How to Bind/Provide Activity or Fragment with Hilt? - android

I'm trying to implement Hilt on an Android App, while It's pretty easy to implement and removes a lot of the boilerplate code when comparing with Dagger, there are some things I miss, Like building my own components and scoping them myself so i'll have my own hirerchy.
To the point: Example: let's say I have a simple App with a RecyclerView, Adapter, Acitivity, and a Callback nested in my Adapter that I pass into my Adapter constructor in order to detect clicks or whatever, and I'm letting my activity implement that Callback, and of course I want to inject the adapter.
class #Inject constructor (callBack: Callback): RecyclerView.Adapter...
When I let Hilt know that I want to inject my adapter I need to let Hilt know how to provide all the Adapter dependencies - the Callback.
In Dagger I was able to achieve this by just binding the Activity to the Callback in one of my modules:
#Binds fun bindCallback(activity: MyActivity): Adapter.Callback
Dagger knew how to bind the Activity(or any Activity/Fragment) and then it was linked to that Callback, but with Hilt it does'nt work.
How can I achieve this? How can I provide or Bind Activity or Fragment with Hilt?

The solution is quite simple.
So a few days ago I came back to look at my question only to see that there was still no new solution, so I tried Bartek solution and wasn't able to make it work, and even if it did work, the clean Hilt code was becoming too messy, so I did a little investigation and played a little and discovered that the solution is actually stupidly easy.
It goes like this:
App:
#HiltAndroidApp
class MyApp: Application()
Activity: (implements callback)
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), SomeClass.Callback {
#Inject
lateinit var someClass: SomeClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onWhatEver() {
// implement
}
}
SomeClass: (with inner callback)
class SomeClass #Inject constructor(
private val callback: Callback
) {
fun activateCallback(){
callback.onWhatEver()
}
interface Callback{
fun onWhatEver()
}
}
SomeModule: (providing/binding the activity to the callback)
#Module
#InstallIn(ActivityComponent::class)
object SomeModule{
#Provides
fun provideCallback(activity: Activity) =
activity as SomeClass.Callback
}
And that's all we need.
We cannot bind the activity to the callback with #Bind because it needs to be explicitly provided and cast to the callback so that the app can build.
The module is installed in ActivityComponent and is aware of a generic 'activity', if we cast it to the callback, Hilt is content and the activity is bound to the callback, and Hilt will know how to provide the callback as long as its in the specific activity scope.
Multiple Activities/Fragments
App:
#HiltAndroidApp
class MyApp: Application()
BooksActivity:
#AndroidEntryPoint
class BooksActivity : AppCompatActivity(), BooksAdapter.Callback{
#Inject
lateinit var adapter: BooksAdapter
...
override fun onItemClicked(book: Book) {...}
}
}
AuthorsActivity:
#AndroidEntryPoint
class AuthorsActivity : AppCompatActivity(), AuthorsAdapter.Callback{
#Inject
lateinit var adapter: AuthorsAdapter
...
override fun onItemClicked(author: Author) {...}
}
BooksAdapter
class BooksAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(book: Book)
}
}
AuthorsAdapter
class AuthorsAdapter #Inject constructor (
val callback: Callback
) ... {
...
interface Callback{
fun onItemClicked(auhtor: Auhtor)
}
}
AuhtorsModule
#Module
#InstallIn(ActivityComponent::class)
object AuthorsModule {
#Provides
fun provideCallback(activity: Activity) =
activity as AuthorsAdapter.Callback
}
BooksModule
#Module
#InstallIn(ActivityComponent::class)
object BooksModule {
#Provides
fun provideCallback(activity: Activity) =
activity as BooksAdapter.Callback
}
The Modules can be joined to one module with no problem, just change the names of the functions.
This is offcourse applicable for more activities and/or multiple fragments.. for all logical cases.

Hilt can provide an instance of the generic Activity (and Fragment for that matter) as a dependency within the ActivityComponent (and FragmentComponent respectively). It's just not able to provide an instance of your specific MyActivity.
You can still create your own Component in Hilt. You will just have to manage the component instance on your own. Add the MyActivity as the seed data for the component builder and you should be able to #Bind your Callback with no problem.

Related

Dagger2 Can't provide dependency of activity to dagger

MainActivity cannot be provided without an #Inject constructor or an
#Provides-annotated method. This type supports members injection but
cannot be implicitly provided.
I'm using dagger-android, I injected MainActivity through AndroidInjection.inject(this), but it's still unavailable in Module. I prepared sample project: https://github.com/deepsorrow/test_daggerIssu.git, files listed below:
FactoryVmModule:
#Module
class FactoryVmModule {
#Provides
#Named("TestVM")
fun provideTestVM(
activity: MainActivity, // <--- dagger can't inject this
viewModelFactory: ViewModelFactory
): TestVM =
ViewModelProvider(activity, viewModelFactory)[TestVM::class.java]
}
MainActivityModule:
#Module
abstract class MainActivityModule {
#ContributesAndroidInjector
abstract fun injectMainActivity(): MainActivity
}
MainActivity (using DaggerAppCompatActivity):
class MainActivity : DaggerAppCompatActivity() {
#Named("TestVM")
#Inject
lateinit var testVM: TestVM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
TestApplication:
class TestApplication : Application(), HasAndroidInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate() {
super.onCreate()
DaggerAppComponent.create().inject(this)
}
override fun androidInjector() = dispatchingAndroidInjector
}
AppComponent:
#Component(modules = [AndroidInjectionModule::class, MainActivityModule::class, ViewModelModule::class, FactoryVmModule::class])
interface AppComponent {
fun inject(application: TestApplication)
}
dagger.android does do this automatically: See the explicit version of the binding that #ContributesAndroidInjector generates for you, where the generated AndroidInjector.Factory contains a #BindsInstance binding of the type you request here.
This isn't working for you because you are injecting MainActivity in a binding that is installed on your top-level component. This is a problem because AppComponent will exist before the Activity does, and will also be replaced as Android recreates the Activity: Passing an instance through #Component.Builder is not a way around this problem.
Instead, move your FactoryVmModule::class to within the subcomponent that #ContributesAndroidInjector generates, which you can do by including it in the modules attribute on #ContributesAndroidInjector. Dagger will create a different subcomponent instance per Activity instance, so your FactoryVmModule will always have a fresh binding to MainActivity.
#Module
abstract class MainActivityModule {
#ContributesAndroidInjector(
modules = [ViewModelModule::class, FactoryVmModule::class]
)
abstract fun injectMainActivity(): MainActivity
}
I moved your ViewModelModule class there as well; though it's possible you could leave it in your top-level Component if it doesn't depend on anything belonging to the Activity, you might want to keep them together. Bindings in subcomponents inherit from the application, so you can inject AppComponent-level bindings from within your Activity's subcomponent, but not the other way around. This means you won't be able to inject VM instances (here, TestVM) outside your Activity, but if they depend on the Activity, you wouldn't want to anyway: Those instances might go stale and keep the garbage collector from reclaiming your finished Activity instances.

Hilt injection into activity before super.onCreate()

I defined my own LayoutInflater.Factory2 class in a separate module. I want to inject it into each activity in my App, but the point is that I have to set this factory before activity's super.onCreate() method.
When I using Hilt it makes an injection right after super.onCreate(). So I have an UninitializedPropertyAccessException.
Is there any opportunity to have an injection before super.onCreate with Hilt?
Below is my example of module's di.
#Module
#InstallIn(SingletonComponent::class)
object DynamicThemeModule {
#FlowPreview
#Singleton
#Provides
fun provideDynamicThemeConfigurator(
repository: AttrRepository
): DynamicTheme<AttrInfo> {
return DynamicThemeConfigurator(repository)
}
}
You can inject the class before onCreate by using Entry Points like this.
#AndroidEntryPoint
class MainActivity: AppCompatActivity() {
#EntryPoint
#InstallIn(SingletonComponent::class)
interface DynamicThemeFactory {
fun getDynamicTheme() : DynamicTheme<AttrInfo>
}
override fun onCreate(savedInstanceState: Bundle?) {
val factory = EntryPointAccessors.fromApplication(this, DynamicThemeFactory::class.java)
val dynamicTheme = factory.getDynamicTheme()
super.onCreate(savedInstanceState)
}
}
If you need something like this a lot Id recommend creating an instance of it in the companion object of your Application class when your application starts (onCreate). That is before any of your views are created. So you don´t need to jump threw those hoops all the time, but can just access the instance that already exists. This code above won´t be available in attachBaseContext, when you need it there you have to create it in your application class I think.

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

Dagger2 creating a map of concrete instances fails with "cannot be provided without an #Provides-annotate"

I am playing around with Dagger collection, in particular with map.
I want to use Dagger2 to inject a map whose key is an enum and the values a concrete
implementation of an interface. The map
is injected in a presenter. An unique component instantiates the presenter, an
activity uses the instantiation to display the strings produced by the value
of the map.
I get the following error:
e: ../DaggerMap/app/build/tmp/kapt3/stubs/debug/com/aklal/briquedagger2/MainComponent.java:7: error: [Dagger/MissingBinding] java.util.Map<com.aklal.briquedagger2.Lapse.TIME,? extends com.aklal.briquedagger2.Lapse.ChristmasTime> cannot be provided without an #Provides-annotated method.
public abstract interface MainComponent {
^
java.util.Map<com.aklal.briquedagger2.Lapse.TIME,? extends com.aklal.briquedagger2.Lapse.ChristmasTime> is injected at
com.aklal.briquedagger2.MainPresenter(mapOfLapse)
com.aklal.briquedagger2.MainPresenter is injected at
com.aklal.briquedagger2.MainModule.getMainPresenter(connection)
com.aklal.briquedagger2.Presenter is provided at
com.aklal.briquedagger2.MainComponent.presenter()
FAILURE: Build failed with an exception.
The "project" can be found here
Implementation
I have an interface ChristmasTime which is implemented by two classes:
TimeUntilNextChristmas
TimeSinceLastChristmas
These implementation are similarly simply defined as follow:
class TimeSinceLastChristmas #Inject constructor(): ChristmasTime {
override fun getLapseOfTime() = "SINCE TEST"
}
I want to let Dagger2 create a map with
an enum value as key
ChristmasTime as type value
The key value is defined as follow:
#MapKey
annotation class TimeKey(val value: TIME)
enum class TIME {
UNTIL,
SINCE
}
I created a module to provide concrete implementations of type ChristmasTime:
#Module
interface LapseOfTimeModule {
#Binds
#IntoMap
#TimeKey(TIME.UNTIL)
fun provideLapsesOfTimeUntil(t: TimeUntilNextChristmas): ChristmasTime
#Binds
#IntoMap
#TimeKey(TIME.SINCE)
fun provideLapsesOfTimeSince(t: TimeSinceLastChristmas): ChristmasTime
}
I want to displayed the string returned by the concrete implementations on the
screen. To do so, a presenter communicates with an activity the strings contained
in the map (that has been injected in the presenter):
class MainPresenter #Inject constructor(
private val mapOfLapse: Map<TIME, ChristmasTime>
) : Presenter {
override fun getDateUntil(): String = mapOfLapse[TIME.UNTIL]?.getLapseOfTime() ?: "UNTIL FAILED"
}
The component to instantiate the presenter in the MainActivity takes the module that defines the map (LapseOfTimeModule) and a MainModule
#Component(modules = [MainModule::class, LapseOfTimeModule::class])
interface MainComponent {
fun presenter(): Presenter
}
MainModule is:
#Module
interface MainModule {
#Binds
fun getMainPresenter(connection: MainPresenter): Presenter
}
And the MainActivity is:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var presenter: Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter = DaggerMainComponent.create().presenter()
bttDisplayNewDate.setOnClickListener{
displayDate(
presenter.getDateSince(),
presenter.getDateUntil()
)
}
}
fun displayDate(since: String, until: String) {
tvSince.text = since
tvUntil.text = until
}
}
Does anyone know how to fix that ?
Here are some threads that I read but they did not help much:
Kotlin dagger 2 Android ViewModel injection error
Dagger2 Inherited subcomponent multibindings
Thanks in advance!!
ps: The version of dagger used is 2.24 and kotlin version is 1.3.41
It's a little bit tricky how it works. In Java it will work. Kotlin decompile Map<K,V> just to Map, and Dagger can't find a Map without types of K and V. To fix it please use just java.util.Map for autogenerated daggers class.
class MainPresenter #Inject constructor(
private val mapOfLapse: java.util.Map<TIME, ChristmasTime>
) : Presenter {
of course, then you need to map it into kotlin map to have all extension functions.

Inject Adapter class to Fragment using Dagger2

I have followed Android Architecture Blueprints Dagger2 for dependency injection: URL
Now I want to inject Adapter to my Fragment class:
#ActivityScoped
class MainFragment #Inject
constructor(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
Main Module class:
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
MainFragmentModule:
#Module
class MainFragmentModule {
#Provides
fun provideArtistAdapter() = ArtistAdapter()
}
And this is my adapter class:
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)
When I build the project I get following Kotlin compiler error:
error: [Dagger/DependencyCycle] Found a dependency cycle:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.sample.android.lastfm.LastFmApp> {
^
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainModule.bindArtistClickCallback$app_debug(mainFragment)
com.sample.android.lastfm.ArtistClickCallback is injected at
com.sample.android.lastfm.ui.main.ArtistAdapter.artistClickCallback
com.sample.android.lastfm.ui.main.ArtistAdapter is injected at
com.sample.android.lastfm.ui.main.MainFragment.adapter
com.sample.android.lastfm.ui.main.MainFragment is injected at
com.sample.android.lastfm.ui.main.MainActivity.mainFragment
com.sample.android.lastfm.ui.main.MainActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.sample.android.lastfm.di.AppComponent → com.sample.android.lastfm.di.ActivityBindingModule_MainActivity$app_debug.MainActivitySubcomponent]
Can you suggest me how to solve this problem?
Codes can be found at URL
Your fragment should probably not have #ActivityScoped as a scope. Further do not use constructor injection with fragments (or any other framework type)! The Android framework will create those objects in some cases, and you will end up with the wrong reference in your classes. Add the fragment to the corresponding component via its builder.
Also you're using a provides annotated method as well as constructor injection (#Inject constructor()). Pick one. Since you also use field injection within the ArtistAdapter the next "error" you would encounter would be a null callback because you don't inject the adapter anywhere. You just create the object.
Constructor injection should usually be favored, which will also inject fields. Remove the following completely, keep the annotation on the construcor:
#Provides
fun provideArtistAdapter() = ArtistAdapter()
Moving on, your error originates in MainActivitySubcomponent (last line) and seems to be because your MainFragment is bound as an ArtistClickCallback, but requires a ArtistAdapter which requires a ArtistClickCallback...hence your dependency cycle.
This issue should resolve itself once you fix the problems mentioned (#Inject constructor on the fragment in this case) above, since it originates through the MainFragment being constructed by Dagger within the MainActivitySubcomponent, which is the wrong place anyways since your fragment should have a lower scope than the Activity.
Further you need to move your binding (#Binds fun bindArtistClickCallback) into the MainFragmentModule, since there is no fragment to bind in the Activity component (where you add the binding currently)
When you fix all those issues, you will bind your fragment to the correct FragmentSubcomponent, where you will bind it as a Callback, with which you can then create the Adapter and it should work.
I recommend you have a more thorough look on Dagger and make sure to understand all the issues / fixes pointed out.
This is how it should look
#FragmentScoped
class MainFragment(): DaggerFragment(), ArtistClickCallback {
#Inject lateinit var adapter : ArtistAdapter
}
#Module
abstract class MainModule {
#FragmentScoped
#ContributesAndroidInjector(modules = [MainFragmentModule::class])
internal abstract fun mainFragment(): MainFragment
}
#Module
class MainFragmentModule {
#Binds
internal abstract fun bindArtistClickCallback(mainFragment: MainFragment) : ArtistClickCallback
}
class ArtistAdapter #Inject constructor(
private val artistClickCallback : ArtistClickCallback
) : PagedListAdapter<LastFmArtist, RecyclerView.ViewHolder>(POST_COMPARATOR)

Categories

Resources