I'm new in dagger , I want to inject context and network (using retrofit) in my classes .
this is my code so far :
#Module
// Safe here as we are dealing with a Dagger 2 module
#Suppress("unused")
object NetworkModule {
#Provides
#Reusable
#JvmStatic
internal fun provideMainApi(retrofit: Retrofit): MainApi {
return retrofit.create(MainApi::class.java)
}
#Provides
#Reusable
#JvmStatic
internal fun provideRetrofitInterface(): Retrofit {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
return Retrofit.Builder()
.baseUrl(Constants.baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(client)
.build()
}
}
#Module
class AppModule(private val app: Application) {
#Provides
#Singleton
fun provideApplication() = app
}
this is my component :
#Singleton
#dagger.Component(modules = arrayOf(AppModule::class, NetworkModule::class))
interface AppComponent {
///for injecting retrofit network
fun injectMain(mainRepository: MainRepository)
#dagger.Component.Builder
interface Builder {
fun build(): AppComponent
fun networkModule(networkModule: NetworkModule): Builder
fun appModule(appModule: AppModule):Builder
}
}
I want to use it in my repository , I've a baseRepository :
open class BaseRepository {
private val injector: AppComponent = DaggerAppComponent
.builder()
.networkModule(NetworkModule)
.build()
init {
inject()
}
private fun inject() {
when (this) {
is MainRepository -> injector.injectMain(this)
}
}
}
when I run the app , I get this error "module.AppModule must be set"
I understand the error and I should provie appMOdule in my base repository but the problem is I don't have any application or context in base repository
how should I fix this ?
the second problem I've is this , I've heard that I should once make the dagger and use it in my entire app and I shouldn't make it every time , it means I should use application for that .
but how can I use injector in an application class , it doesn't make sense
If I were you I would go through this project and see there how everything is done.
https://github.com/google/iosched
Here is explained what this project is about:
https://medium.com/androiddevelopers/google-i-o-2018-app-architecture-and-testing-f546e37fc7eb
Also you can check the Dagger branch ot this projec:
https://github.com/android/architecture-samples
It was all done by Jose Alcerreca and some other guys. These are the guidelines of Google about Android and there will be the answer of most of your questions.
but how can I use injector in an application class , it doesn't make sense There is a DaggerApplication class which you can extend and inject your App class however you want.
And I am telling you all of this, because I think you have learned Dagger from old sources. I would have changed a lot of stuff in your code.
Also I am using Dagger since 1.5 years and I have never ever written such a code like the one in: BaseRepository. I use only and only #Inject annotations and that is all throuhg my classes which are not Dagger related. Creating a Component in a Repository? -> Never!
This is how your classes should end up everywhere.
class Repostory #Inject constructor(
private val dependency1: Dependency1
) {}
class Activity {
#Inject lateinit var dependency2: Dependency2
}
Related
Base app architecture for Android client-server app is:
UI (Activity + Fragment or Jetpack Compose)
ViewModel (also is part of UI actually)
Data (repositories to get data from database (Room) and API (Retrofit))
Hilt has different Components and Scopes for Android https://dagger.dev/hilt/components.html
For my base app architecture I have set DI using Hilt in the following way:
Database
#InstallIn(SingletonComponent::class)
#Module
object DatabaseModule {
#Provides
#Singleton // only this one is marked as #Singleton to get the same instance all the time (logically for Database...)
fun provideDataBase(#ApplicationContext context: Context): AppDataBase {
return AppDataBase.getInstance(context)
}
#Provides
fun provideUsersDao(dataBase: AppDataBase): UsersDao = dataBase.getUsersDao()
}
Network (Datasource)
#InstallIn(SingletonComponent::class)
#Module
object NetworkModule {
#Provides
#Singleton // also as Room instance, it should return the same instance of http client
fun providesOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
...
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(headerInterceptor)
...
.build()
#Provides
#Singleton
fun provideRetrofit(
gson: Gson,
client: OkHttpClient,
queryConverterFactory: Converter.Factory
): Retrofit = Retrofit.Builder()
...
.client(client)
.build()
//... some other stuff needed to setup OkHttp and Retrofit
#Provides
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
#Provides
fun provideUserDataSource(
userApi: UserApi,
networkErrorConverterHelper: NetworkErrorConverterHelper
): UserDataSource = UserDataSourceImpl(userApi, networkErrorConverterHelper)
}
Repository
#InstallIn(SingletonComponent::class)
#Module
interface RepositoryModule {
#Binds
fun bindUserRepositoryImpl(impl: UserRepositoryImpl): UserRepository
}
class UserRepositoryImpl #Inject constructor(
private val userDataSource: UserDataSource,
private val userDbManager: UserDbManager
) : UserRepository {
override fun observeCachedUsers(): Flow<List<User>> ...
override fun fetchUsers(): Flow<Resource<Unit>> = ...
}
ViewModel
#HiltViewModel
class UsersViewModel #Inject constructor(
private val userRepository: UserRepository,
) : ViewModel() {
...
}
Activity
#AndroidEntryPoint
class UsersActivity : AppCompatActivity() {
val userViewModel: UserViewModel by viewModels()
...
}
So basically everywhere I have used SingletonComponent for Hilt modules and #Singleton annotation for some providers so it would return the same instance (of Database, Http client)
I'm trying to understand how good (correct) all of that and in which cases I could use other components/scopes based on this sample.
When app has only one activity and uses Fragments or Jetpack Compose to implement all screens then I guess I can use ActivityRetainedComponent instead of SingletonComponent for data modules, in this case Hilt releases all instances in memory when a user presses the back button to exit the app (activity is destroyed), in case of SingletonComponent these instances live until process of the app dies, so basically difference in this case is not big but still...
I'm creating a pet project with Hilt, and perhaps I'm having this issue because I'm installing everything in SingletonComponent::class, and perhaps I should create components for each one.
The pet project has a NetworkModule, UserPrefsModule, and the problem appeared when I was trying to create an Authenticator for OkHttp3.
This is my network module
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Singleton
#Provides
fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor()
.apply {
if (BuildConfig.DEBUG) level = HttpLoggingInterceptor.Level.BODY
}
#Singleton
#Provides
fun providesErrorInterceptor(): Interceptor {
return ErrorInterceptor()
}
#Singleton
#Provides
fun providesAccessTokenAuthenticator(
accessTokenRefreshDataSource: AccessTokenRefreshDataSource,
userPrefsDataSource: UserPrefsDataSource,
): Authenticator = AccessTokenAuthenticator(
accessTokenRefreshDataSource,
userPrefsDataSource,
)
#Singleton
#Provides
fun providesOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
errorInterceptor: ErrorInterceptor,
authenticator: Authenticator,
): OkHttpClient =
OkHttpClient
.Builder()
.authenticator(authenticator)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(errorInterceptor)
.build()
}
Then my UserPrefsModule is :
#Module
#InstallIn(SingletonComponent::class)
object UserPrefsModule {
#Singleton
#Provides
fun provideSharedPreference(#ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
}
#Singleton
#Provides
fun provideUserPrefsDataSource(sharedPreferences: SharedPreferences): UserPrefsDataSource {
return UserPrefsDataSourceImpl(sharedPreferences)
}
}
Then I have an AuthenticatorModule
#Module
#InstallIn(SingletonComponent::class)
object AuthenticationModule {
private const val BASE_URL = "http://10.0.2.2:8080/"
#Singleton
#Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()
#Singleton
#Provides
fun provideApiService(retrofit: Retrofit): AuthenticationService =
retrofit.create(AuthenticationService::class.java)
#Singleton
#Provides
fun providesAccessTokenRefreshDataSource(
userPrefsDataSource: UserPrefsDataSource,
authenticationService: AuthenticationService,
): AccessTokenRefreshDataSource = AccessTokenRefreshDataSourceImpl(
authenticationService, userPrefsDataSource
)
}
The problem started to happen when I created the AccessTokenRefreshDataSourceImpl that I need the AuthenticationService and UserPrefsDataSource, and I'm getting this error :
error: [Dagger/DependencyCycle] Found a dependency cycle:
public abstract static class SingletonC implements App_GeneratedInjector,
For each feature like Login, SignIn, Verification, etc.. I was creating a new #Module as this :
#Module
#InstallIn(SingletonComponent::class)
interface SignInModule {
#Binds
fun bindIsValidPasswordUseCase(
isValidPasswordUseCaseImpl: IsValidPasswordUseCaseImpl,
): IsValidPasswordUseCase
#Binds
fun bindIsValidEmailUseCase(
isValidEmailUseCase: IsValidEmailUseCaseImpl,
): IsValidEmailUseCase
//Here in that Datasource I'm using the AuthenticationService from AuthenticationModule and it works
#Binds
fun bindSignInDataSource(
signInDataSourceImpl: SignInDataSourceImpl
): SignInDataSource
}
Constructor of AccessTokenAutenticator
class AccessTokenAuthenticator #Inject constructor(
private val accessTokenRefreshDataSource: AccessTokenRefreshDataSource,
private val userPrefsDataSource: UserPrefsDataSource,
) : Authenticator {
Constructor of AccessTokenRefreshDatasource
class AccessTokenRefreshDataSourceImpl #Inject constructor(
private val authenticationService: AuthenticationService,
private val userPrefsDataSource: UserPrefsDataSource,
) : AccessTokenRefreshDataSource {
Note I have everything in a #Module separated by features for a future be able to modularise the app.
In most programming languages, if you require an instance of B to construct A and an instance of A to construct B, then you won't be able to construct either.
Here:
AccessTokenRefreshDataSource requires AuthenticationService
AuthenticationService requires Retrofit
Retrofit requires OkHttpClient
OkHttpClient requires Authenticator
Authenticator requires AccessTokenRefreshDataSource
...and consequently, regardless of your module or component structure, Dagger can't create any of those instances first.
However, if your AccessTokenRefreshDataSourceImpl does not need its AuthenticationService instance within the constructor itself, you can replace it with Provider<AuthenticationService>: Dagger automatically lets you inject Provider<T> for any T in the graph, among other useful bindings. This allows Dagger to create your AccessTokenRefreshDataSource without first creating an AuthenticationService, with the promise that once the object graph is created your AccessTokenRefreshDataSource can receive the singleton AuthenticationService instance it needs. After you inject the provider, just call authenticationServiceProvider.get() to get the instance wherever you need it (presumably outside the constructor).
Of course, you can solve your problem with the same refactor anywhere else in your graph you control. AccessTokenAuthenticator is also a reasonable refactor point, assuming you've written it yourself and thus can modify its constructor.
Points discussed in the comments:
You can always inject a Provider<T> instead of any binding T in your graph. In addition to being valuable for breaking dependency cycles, it can also be handy if your dependency-injected class needs to instantiate an arbitrary number of that object, or if creating the object takes a lot of memory or classloading and you want to delay it until later. Of course, if the object is cheap to construct without dependency cycles and you expect to call get() exactly once, then you can skip that and directly inject T as you've done here.
Provider<T> is a single-method object. Calling get() on it is the same as calling a getter of type T on the Component itself. If the object is unscoped, you get a new one; if the object is scoped, you get the one that Dagger has stored in the Component.
Generally speaking you can just inject the Provider and call get on it directly:
class AccessTokenRefreshDataSourceImpl #Inject constructor(
private val authenticationServiceProvider:
Provider<AuthenticationService>,
private val userPrefsDataSource: UserPrefsDataSource,
) : AccessTokenRefreshDataSource {
... and then rather than using this.authenticationService.someMethod() directly, use this.authenticationServiceProvider.get().someMethod(). Ma3x pointed out in the comments that if you declare val authenticationService get() = authenticationServiceProvider.get() as a class field, Kotlin can abstract away the fact that there's a call to get() involved and you won't need to make any other changes to AccessTokenRefreshDataSourceImpl.
You will also need to change the #Provides method in your Module, but only because you're not taking full advantage of the #Inject annotation on your AccessTokenRefreshDataSourceImpl as below.
#Singleton
#Provides
fun providesAccessTokenRefreshDataSource(
userPrefsDataSource: UserPrefsDataSource,
authenticationServiceProvider: Provider<AuthenticationService>, // here
): AccessTokenRefreshDataSource = AccessTokenRefreshDataSourceImpl(
authenticationServiceProvider /* and here */, userPrefsDataSource
)
It is generally not necessary to use #Provides to refer to an #Inject-annotated constructor. #Provides is useful when you can't change the constructor to make it #Inject. #Inject can be less maintenance because then you don't need to copy #Provides method arguments to your constructor; Dagger will do that for you. Read more here.
If you do use #Inject and delete your #Provides method, you might still want to use #Binds to indicate that your AccessTokenRefreshDataSource should be bound to AccessTokenRefreshDataSourceImpl, though then you'll need to decide how to put #Binds and #Provides in the same Module. In Java 8 you can do this by making your #Provides methods static and putting them on an interface, but in Kotlin it might be easier to create a nested interface and install that using #Module(includes = ...). Read more here.
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>()
I have a class named NetworkManager. Since it is not one of the Android Components, I am using custom entry point, NetworkManagerEntryPoint with one fun that returns NetworkClient object which is what I want to inject.
Now, to inject an instance of this class using Hilt, I believe I need to use one of the Helper methods in EntryPointAccessors. But all of them requires a reference to android components. So, do I really have to pass an android component like Context to my class to inject an object using Hilt?
class NetworkManager() {
#InstallIn(SingletonComponent::class)
#EntryPoint
interface NetworkManagerEntryPoint {
fun getNetworkClient(): NetworkClient
}
var defaultNetworkClient: NetworkClient = EntryPointAccessors.fromApplication(
context, // Do I have to pass a context to this class to use Hilt?
NetworkManagerEntryPoint::class.java
).getNetworkClient()
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
Hi there maybe you can try this way i have done , i follow the mvvm pattern
My RetrofitApi
interface RetrofitApi {
#GET("endpoint")
suspend fun getApi():Response<RetrofitApiResponse>
}
My NetworkModule
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule{
#Singleton
#Provides
fun provideApi(): RetrofitApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitApi::class.java)
#Singleton
#Provides
fun provideRepository(retrofitApi:RetrofitApi) : MainRepository =
DefualtMainRepository(retrofitApi)
}
and this module gets injected in my repository
class DefualtMainRepository #Inject constructor(
val retrofitApi: RetrofitApi
):MainRepository {
override suspend fun getQuotes(): Resource<RetrofitApiResponse> {
val response = retrofitApi.getApi()
val result = response.body()
if (response.successful){
}
}
}
If you are interested i have full project in my github and even wrote a medium article explaining it, Hopefully my answer is helpful to you
https://zaidzakir.medium.com/a-simple-android-app-using-mvvm-dagger-hilt-e9f45381f1bc
Okay. #Zaid Zakir's answer showed me that I can inject objects via constructor parameters, if not field injection. So, the solution for me ended up looking like this.
#Singleton
class NetworkManager #Inject constructor(
var defaultNetworkClient: NetworkClient
) {
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
In another class named NetworkClientModule, I have this,
#Module
#InstallIn(SingletonComponent::class)
abstract class NetworkClientModule {
#Binds #Singleton
abstract fun bindDefaultNetworkClient(impl: DefaultNetworkClient): NetworkClient
}
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())