Provide domain-layer UseCase classes with HILT - android

I am implementing some of the architectural designs from Google I/O's app to my own app, but I have come across something in their app that has created some confusion for me.
They have a domain layer with repository usecases, which I use myself in my apps usually. However, I do have to provide these usecases with dagger in my app. But on Google's I/O app, I cannot find any module that provides these usecases. And when used with viewmodels annotated #HiltViewModel (In my own app), it seems like it works? Somehow these get injected into my viewmodels. I do have to provide all the usecase's dependencies(repositories etc) with Hilt, but I don't have to provide any usecase via Hilt.
Here is example how it looks in my code.
Usecase:
abstract class UseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
suspend operator fun invoke(parameters: P): Resource<R> {
return try {
withContext(coroutineDispatcher) {
execute(parameters).let {
Resource.Success(it)
}
}
} catch (e: Exception) {
Timber.d(e)
Resource.Error(e.toString())
}
}
#Throws(RuntimeException::class)
protected abstract suspend fun execute(parameters: P): R
}
Concrete implementation of usecase:
class GetListUseCase #Inject constructor(
private val coroutineDispatcher: CoroutineDispatcher,
private val remoteRepository: RemoteRepository
): UseCase<ListRequest,ItemsList>(coroutineDispatcher) {
override suspend fun execute(parameters: ListRequest): ItemsList{
return remoteRepository.getList(parameters)
}
}
Viewmodel:
#HiltViewModel
class DetailViewModel #Inject constructor(
private val GetListUseCase: getListUseCase
): ViewModel() {
suspend fun getList(): Resource<ItemsList> {
getPokemonListUseCase.invoke(ListRequest(3))
}
}
Example of provided repo:
#Singleton
#Provides
fun provideRemoteRepository(
api: Api
): RemoteRepository = RemoteRepositoryImpl(api)
Remote repo:
#ActivityScoped
class RemoteRepositoryImpl #Inject constructor(
private val api: Api
): RemoteRepository {
override suspend fun getList(request: ListRequest): PokemonList {
return api.getPokemonList(request.limit, request.offset)
}
}
My Question is:
How come this works? Why don't I have to provide usecase' via Hilt? Or is my design wrong and I should provide usecases via Hilt even if this works?
EDIT:
Link to Google I/O domain layer and ui layer if it helps.

Your design is great! And it's not wrong, but yes, you can add some additional context for your use cases if you put them to a separate module and scope them per your needs. Modules exist mostly for cases when you do have your third-party dependencies (the simplest example is OkHTTPClient) or when you have interface -> impl of the interface, or you want to visibly limit/extend lifecycle/visibility of your components.
Currently you're telling Hilt how to provide instances of GetListUseCase by annotating its constructor with #Inject AND Hilt already knows what is "CoroutineDispatcher" (because it's been provided by #Provides in the coroutine module, right?) and what is RemoteRepository (because you're injecting the interface, but beneath the hood you're providing the real implementation of it in the repo module by #Provides).
So it's like saying - give me an instance of the use case class with two constructor dependencies, and both of them are known to Hilt, so it's not confusing.
And if you want to have a scoped binding/component (like explained here) or to mark your use case as a singleton, then you have to create a UseCaseModule and scope your use case components there.

Related

How can I use Hilt to inject Retrofit to Repository, which is injected to ViewModel?

I have just learnt manual dependency injection, but I am trying out Hilt to handle these dependency injections.
I want to inject a ViewModel into a Fragment. The fragment is contained within an Activity. Right now, I have added the annotations to Application, Activity, and Fragment.
#HiltAndroidApp
class MovieCatalogueApplication : Application()
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}
#AndroidEntryPoint
class HomeFragment : Fragment() {
private lateinit var binding: FragHomeBinding
private val viewmodel: HomeViewModel by viewModels()
...
As can be seen, my HomeFragment depends on HomeViewModel. I have added a ViewModel injection as described here like so.
class HomeViewModel #ViewModelInject constructor(
private val movieRepository: MovieRepository,
private val showRepository: ShowRepository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
...
}
However, the ViewModel requires two repositories. Right now, my MovieRepository is like so.
class MovieRepository (private val movieApi: MovieService) {
...
}
In the above code, MovieService will be created by Retrofit using the Retrofit.create(class) method. The interface used to create MovieService is like so.
interface MovieService {
...
}
To get my Retrofit instance, I am using the following code.
object RetrofitService {
...
private var _retrofit: Retrofit? = null
val retrofit: Retrofit
get() {
return when (_retrofit) {
null -> {
_retrofit = Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
_retrofit!!
}
else -> _retrofit!!
}
}
}
I am not too sure how I can inject the Retrofit into the Repository to be used by my ViewModel later on. Could someone give me some pointers or step-by-step instructions on how to do this?
Apparently, it is not as hard as it seems.
You have to first define the binding information to Hilt. Binding information tells Hilt how to provide the instances of the dependency specified. Because MovieService is created using a Retrofit (which is a 3rd-party class not created by yourself) using the builder pattern, you can't use the constructor injection and you have to instead use Hilt modules and the annotation #Provides to tell Hilt about this binding information.
As described in the doc, the annotated function in the Hilt module you have created will supply the following information to Hilt so that Hilt can provide the instances of the dependency.
• The function return type tells Hilt what type the function provides instances of.
• The function parameters tell Hilt the dependencies of the corresponding type.
• The function body tells Hilt how to provide an instance of the corresponding type. Hilt executes the function body every time it needs to provide an instance of that type.
In the end, you only need to modify the MovieRepository class, add a module for each repository, and annotate the function that tells Hilt how to provide the service instance created with Retrofit with #Provides.
Code.
class MovieRepository #Inject constructor(
private val movieApi: MovieService
) {
...
}
interface MovieService {
...
}
#Module
#InstallIn(ActivityRetainedComponent::class)
object MovieModule {
#Provides
fun provideMovieService(): MovieService
= RetrofitService.retrofit.create(MovieService::class.java)
}
As you can see, the ActivityRetainedComponent is referred in the #InstallIn annotation because the Repository is to be injected to a ViewModel. Each Android component is associated to different Hilt components.

best way for injecting ViewModels?

I have used koin in the past and injecting viewModel with koin is a single liner. I need to know how to do that without it!
should We use a big switch/case in ViewModelFactory for different viewmodels?
class ViewModelFactory: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when(modelClass) {
is FirstViewModel -> FirstViewModel()
is SecondViewModel -> SecondViewModel()
...
}
}
}
but then I have to inject all the dependencies of all viewmodels to the factory. and that's really messy code. even without that the switch/case by itself is messy! I don't think you should do that specially in big projects. so what are the alternative ways of doing this?
how can dagger help with this?
there are actually quite good alternatives to that.
first: the first one which people tend to forget about is having a ViewModelfactory per ViewModel it's a lot better than having a massive factory. that is what we call the Single Responsibility Principle
class FirstViewModelFactory(val firstDependency: SomeClass, val secondDependency: SomeOtherClass): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
FirsViewModel(firstDependency, secondDependency) as T
}
}
and in the activity:
val viewModel: FirstViewModel = ViewModelProvider(this, FirstViewModelFactory(first, second))[FirstViewModel::class.java]
Second: if you want to have only one Factory for all your ViewModels you can define a generic factory with that takes a lambda:
class ViewModelFactory<VM: ViewModel>(val provider: () -> VM): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return provider() as T
}
}
and use it like this in the activity or fragment:
val ViewModel: FirstViewModel = ViewModelProvider(this, ViewModelFactory<FirstViewModel>{
FirstViewModel(first, second)
})[FirstViewModel::class.java]
this actually makes your life a lot easier. but it can still be improved
Third: you can tell dagger how to provide you FirstViewModel and its dependencies.
if you don't know how to use dagger, you try to learn it then read this part.
you need to tell dagger that you want to get an instance of FirstViewModle. the place for that is in the AppComponent.
#Component(modules = [AppModule::class])
interface AppComponent {
val applicationContext: Context
val firstViewModel: FirstViewModel
...
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): AppComponent
}
}
and in your appModule, you need to tell dagger how to provide dependencies of FirstViewModel with #Provides annotation. then you need to instantiate your dagger component in application class, there are lots of ways to do this, I just use the factory interface:
class MyApplication: Application() {
val component: AppComponent by lazy {
DaggerAppComponent.factory().create(applicationContext)
}
}
don't forget to add MyApplication in the manifest.
then in your activity, you can inject the viewModel without ever being worry about the dependencies:
val appComponent = (application as MyApplication).appComponent
val viewModel: FirstViewModel = ViewModelProvider(this, ViewModelFactory<FirstViewModel>{
appComponent.firstViewModel
})[FirstViewModel::class.java]
you can make it more beautiful and readable using lazy and extension functions and you can eventually have something like this:
private val viewModel: FirstViewModel by injectVmWith { appInjector.firstViewModel }
Fourth: you can always use dagger's multibinding. with this, you inject ViewModelFactory to activity or fragment and you retrieve the viewModel from that. with this, you are telling dagger to put all your viewModels in a map and inject it to the ViewModelFactory. you help dagger to find viewModels by annotating them. and you also tell dagger what their key is using another annotation. you do all that in another module and for every viewmodel you need a function in your viewModel module. then instead of switch/case in your massive factory, you tell dagger to get the required viewmodel from the map based on its type. it's a service locator (anti?)pattern. it's another topic by itself and this answer is already too long. refer to this or this
Summary : I think if you are using dagger the third one is definitely the winner. multibinding is also good but every time you add a viewmodel you need to remember to add a function in the viewmodelModule too(just like Koin). and if you are not using dagger, the second way in my opinion is the best but you should decide what is best for you.
Koin is also great!

Dagger2, adding a binding for ViewModelProvider.Factory in a dependant component

The Problem
When attempting to add a ViewModel bind into the multibinding for an inherited ViewModelFactory (created with no scope) within a lower scope (#FragmentScope), I keep running into this error:
java.lang.IllegalArgumentException: unknown model class com.example.app.MyFragmentVM
What I've read or tried
(note: the below is not by any means an exhaustive list, but are two good examples of resources and the kinds of advice I've perused)
[1] dagger2 and android: load module which injects viewmodel on a map (and other variants / similar Q and As)
[2] https://medium.com/tompee/dagger-2-scopes-and-subcomponents-d54d58511781
I'm relatively new to working with Dagger so I had to do a lot of Googling to try and understand what has been going on, but I've reached a point where, to my understanding, something should be working(?)...
From sources similar to [1], I removed the #Singleton scope on ViewModelFactory, but I still get the aforementioned crash saying there is no model class found in the mapping.
From sources similar to [2] I tried to reinforce my understanding of how dependencies worked and how items are exposed to dependant components. I know and understand how ViewModelProvider.Factory is available to my MyFragmentComponent and it's related Modules.
However I do not understand why the #Binds #IntoMap isn't working for the MyFragmentVM.
The Code
Let me first go through the setup of the stuff that already exists in the application -- almost none of it was scoped for specific cases
// AppComponent
#Component(modules=[AppModule::class, ViewModelModule::class])
interface AppComponent {
fun viewModelFactory(): ViewModelProvider.Factory
fun inject(activity: MainActivity)
// ... and other injections
}
// AppModule
#Module
class AppModule {
#Provides
#Singleton
fun providesSomething(): Something
// a bunch of other providers for the various injection sites, all #Singleton scoped
}
// ViewModelModule
#Module
abstract class ViewModelModule {
#Binds
abstract fun bindsViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
#Binds
#IntoMap
#ViewModelKey(MainActivityVM::class)
abstract fun bindsMainActivityVM(vm: MainActivityVM): ViewModel
}
// VMFactory
class ViewModelFactory #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)
}
}
}
And the following is how I am trying to add and utilize my #FragmentScope:
// MyFragmentComponent
#FragmentScope
#Component(
dependencies = [AppComponent::class],
modules = [MyFragmentModule::class, MyFragmentVMModule::class]
)
interface MyFragmentComponent {
fun inject(fragment: MyFragment)
}
// MyFragmentModule
#Module
class MyFragmentModule {
#Provides
#FragmentScope
fun providesVMDependency(): VMDependency {
// ...
}
}
// MyFragmentVMModule
#Module
abstract class MyFragmentVMModule {
#Binds
#IntoMap
#ViewModelKey(MyFragmentVM::class)
abstract fun bindsMyFragmentVM(vm: MyFragmentVM): ViewModel
}
// MyFragment
class MyFragment : Fragment() {
#set:Inject
internal lateinit var vmFactory: ViewModelProvider.Factory
private lateinit var viewModel: MyFragmentVM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMyFragmentComponent.builder()
.appComponent(MyApplication.instance.component)
.build()
.inject(this)
viewModel = ViewModelProvider(this, vmFactory).get(MyFragmentVM::class.java)
}
}
What's interesting here to note is that MyFragmentModule itself does NOT end up providing any unique injections for MyFragment (those all come from AppComponent as it is right now). It DOES however, provide unique injections for the ViewModel that MyFragment uses.
The root of this problem is the difference between subcomponents and component dependencies.
Subcomponents
When working with subcomponents, the parent component knows everything about its subcomponents. As such, when a subcomponent requests a multibinding, the parent component can combine its contributions with those of the subcomponent. This even works transitively: if the subcomponent requests an unscoped ViewModelProvider.Factory, the injected map will include bindings from the subcomponent. (The same is true of a #Reusable binding, but not a #Singleton.)
If you change your components with dependencies into subcomponents, everything will just work. However, this might not fit your desired architecture. In particular, this is impossible if MyFragmentComponent is in an Instant App module.
Component dependencies
When working with component dependencies, the main component merely exposes objects through provision methods, and it does not know about any components that might depend on it. This time, when asked for a ViewModelProvider.Factory, the main component does not have access to any #ViewModelKey bindings except its own, and so the Factory it returns will not include the MyFragmentVM binding.
If MyFragmentComponent does not require any ViewModel bindings from AppComponent, you can extract bindsViewModelFactory into its own module and include it in both components. That way, both components can create their own Factory independently.
If you do need some ViewModel bindings from AppComponent, hopefully you can add those binding modules to MyFragmentComponent as well. If not, you would have to expose the map in AppComponent, and then somehow combine those entries with your new bindings. Dagger does not provide a good way to do this.

Dagger module depends on another module

I'm new to DI and Dagger.
I have this dependency graph in the Android project:
#Module(includes=[Module1, Module2, Module3]) ClassAModule
#Module(includes=[classAModule, Module4]) ClassBModule
#Module(includes=[ClassBModule]) ClassCModule
#Module(includes=[ClassBModule]) ClassDModule
Here's how Module3 looks like
#Module
class Module3 {
#Provides
fun provideUrl(): Url{
return ...
}
}
Module3's Url is required by ClassAModule,
But I want ClassCModule and ClassDModule to be able to provide different Url to ClassAModule
how should I approach this?
To get specific url for any module, you need to define annotation on provider method.
Example
#Provides
#Room
fun provideRoomWordDataSource(): WordDataSource {
return RoomWordDataSource()
}
#Provides
#Firestore
fun provideFirestoreWordDataSource(): WordDataSource {
return FirestoreWordDataSource()
}
#Singleton
class WordRepository
#Inject constructor(
#Room private val room: WordDataSource,
#Firestore private val firestore: WordDataSource
) : Repository<String, Word>(rx, rm), WordDataSource {
}
First two provider method has define WordDataSource instance of two difference class and define with two different annotation #Room and #Firestore.
To get two different WordDataSource in WordRepository, have just used #Room and #Firestore annotation in its constructor.
Enjoy the annotation power in Dagger. :)
Please feel free, if you need more details from me.

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