My app uses the Sugar ORM instead of Room DB at the moment, however, I still would like to use some of the potentials of MVVM.
I set up the Dagger-Hilt in my app and made all connections needed. The app working correctly, however, the Scabbard dependency graph shows that I am missing binding between View Model and Repository.
My goal here is to fix that if need it, and understand why that happened.
Scabbard Dependency Graph
Hilt Module:
#Module
#InstallIn(SingletonComponent::class)
object TestRepositoryHiltModule {
#Provides
#Singleton
fun provideTestRepository(): TestRepository =
TestRepositoryImpl( provideDaoOne() , provideDaoTwo ())
#Provides
#Singleton
fun provideDaoOne(): TestDaoOne = TestDaoOneImpl()
#Provides
#Singleton
fun provideDaoTwo(): TestDaoTwo = TestDaoTwoImpl()
}
DAO:
class TestDaoOneImpl: TestDaoOne {
override fun getBreedsDaoOne(): List<User> {
TODO("Not yet implemented")
}
}
Repository:
class TestRepositoryImpl #Inject constructor(
private val testDaoOne: TestDaoOne,
private val testDaoTwo: TestDaoTwo
) : TestRepository {
override fun getBreeds(): List<User> {
testDaoOne.getBreedsDaoOne()
TODO("Not yet implemented")
}
}
Application:
#HiltAndroidApp
class AppApplication : Application() {
}
Activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), RecyclerViewAdapter.OnItemClickListener {
private val viewModel: MainActivityViewModule by viewModels()
...
Related
I'm getting this error error: [Dagger / MissingBinding] com.eduramza.domain.repositories.RemoteRepository cannot be provided without an # Provides-annotated method. when implementing my repository interface with android hilt.
That's because my useCase implements my repository interface. What may be wrong with my implementation, below is the code:
app.Viewmodel:
#HiltViewModel
class RemoteListViewModel #Inject constructor(
private val useCase: GetTickersUseCase
): ViewModel() {
}
domain.usecase:
class GetTickersUseCase #Inject constructor(
private val remoteRepository: RemoteRepository)
: SingleUseCase<MainCoins> {
override suspend fun executeCall(): Flow<Result<MainCoins>> = remoteRepository.readAllTickers()
}
domain.repository:
interface RemoteRepository {
suspend fun readAllTickers(): Flow<Result<MainCoins>>
}
core.repositoryImpl:
class RemoteRepositoryImpl #Inject constructor(
private val apiService: BraziliexService,
private val tickersMapper: TickersMapper
) : RemoteRepository{
override suspend fun readAllTickers(): Flow<Result<MainCoins>> {
TODO("Not yet implemented")
}
}
core.module:
#Module
#InstallIn(ActivityComponent::class)
abstract class RemoteModule {
#Binds
abstract fun bindRemoteRepository(
remoteRepositoryImpl: RemoteRepositoryImpl
): RemoteRepository
}
My multimodule app in this structure
where core implement domain, and app implement both.
why is the bind method not being initialized?
You using the ActivityComponent but the RemoteRepository is the indirect dependency of ViewModel so it should be tied with the ViewModel Lifecycle
so instead of ActivityComponent
#Module
#InstallIn(ActivityComponent::class)
abstract class RemoteModule {
#Binds
abstract fun bindRemoteRepository(
remoteRepositoryImpl: RemoteRepositoryImpl
): RemoteRepository
}
Use this ViewModelComponent
#Module
#InstallIn(ViewModelComponent::class)
abstract class RemoteModule {
#Binds
abstract fun bindRemoteRepository(
remoteRepositoryImpl: RemoteRepositoryImpl
): RemoteRepository
}
I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}
I want to implement authentication in my application, so I want to get the current logged user in the application class, I am following GithubBrowserSample to develop my application, but I am very new to dagger injection and I am a little lost in how to inject my authRepository to my application class.
class App : Application(), HasActivityInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
AppInjector.init(this)
}
override fun activityInjector() = dispatchingAndroidInjector
}
I tried adding something like
#Inject
lateinit var authRepository: AuthRepository
in the application class and in the AppModule
//I already have functions that provide everything the repository needs
#Singleton
#Provides
fun provideAuthRepository(
appExecutors: AppExecutors,
db: AppDatabase,
trainersService: TrainersService,
sessionDao: SessionDao,
tokenInterceptor: TokenInterceptor
): AuthRepository {
return AuthRepository(
appExecutors,
db,
trainersService,
sessionDao,
tokenInterceptor
)
}
but it throws Uninitialized Property Access Exception
Hope you can help me to achive this following the github sample approach.
I'm getting this error with Dagger 2
C:\Users\Anon\AndroidStudioProjects\BarreChat1082\app\build\tmp\kapt3\stubs\debu
g\com\example\barrechat108\Dependencies\RepositoryComponent.java:8: error:
[Dagger/MissingBinding] com.example.barrechat108.MainActivity cannot be
provided without an #Inject constructor or an #Provides-annotated method.
public abstract interface RepositoryComponent {
^
com.example.barrechat108.MainActivity is injected at
com.example.barrechat108.Dependencies.RepositoryComponent.inject(com.example.barrechat108.MainActivity)
I thought I had set up the injection correctly, My modules are
#Module
class RepositoryModule {
//Provide singleton instance of our database for use by entire app.
//Set up in application's onCreate method
#Provides
#Singleton
fun providesDatabase(applicationContext: Application) : AppDatabase {
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "AppDatabase"
).build()
return db
}
#Provides
#Singleton
fun providesRepository(database: AppDatabase) : Repository {
val repos = Repository(database)
return repos
}
}
and
#Module
class AppModule (application: Application) {
val mApplication = application
#Provides
#Singleton
fun providesApplication() : Application {
return mApplication
}
}
The component used for injection to the MainActivity is
#Singleton
#Component(modules=[AppModule::class, RepositoryModule::class])
interface RepositoryComponent{
fun inject(activity: MainActivity)
}
Which is where the error is contained.
To build the instance of Dagger2, I extended from the Application class and in the manifest I told the app to use this code,
class BarreApp : Application() {
private lateinit var mRepositoryComponent: RepositoryComponent
override fun onCreate() {
super.onCreate()
mRepositoryComponent = DaggerRepositoryComponent.builder()
.appModule(AppModule(this))
.repositoryModule(RepositoryModule())
.build()
}
fun getRepositoryComponent(): RepositoryComponent {
return mRepositoryComponent
}
}
Finally, the activity I use the data injection in is the MainActivity. In this Main Activity, I create an instance of the Repository and send it to the view model from this start activity. I get rid of re-assignment by checking whether or not the repository was already set in the viewmodel. So each time the MainActiviy goes through on create, it opens the viewmodel and checks whether it has a repository and if it doesn't it pulls the repository that was injected on it and puts it into the ViewModel.
class MainActivity : AppCompatActivity() {
private lateinit var mPager: ViewPager
#Inject
private lateinit var mRepository: Repository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.barreBar))
//Get Repository Component using Dagger2
val mApp = application as BarreApp
mApp.getRepositoryComponent().inject(this)
//Get the main ViewModel
val barreViewModel = ViewModelProviders.of(this).get(BarreViewModel :: class.java)
//Add repository to ViewModel
barreViewModel.setRepository(mRepository)
}
During testing, my view models are still using production services, and I want them to use test services. I'm using robolectric for testing, and still haven't found a solution to my problem.
What I thought would be most promising would be using a viewModelFactory generator in my test fragment, like ViewModelUtil.createFor(mockedViewModel) is used in the GitHubBrowserSample. But that's not working for me, my test services created in the mocked viewModel will then get replaced by production services when running startFragment as shown below.
My main application extends DaggerApplication() and my production component uses a #Component.Builder so I'm not able to specify a module in my building process.
Additionally, I've tried having TestAppComponent extend from my AppComponent but that doesn't seem to do the trick either.
I've also tried setting a builder as a static object in MyApplication.kt like so:
companion object {
var builder: AndroidInjector.Factory<MyApplication> =
DaggerAppComponent.builder()
}
...
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return builder.create(this)
}
And in my FragmentTest.kt init doing something like:
private fun init() {
bagFragment = BagFragment.newInstance()
MyApplication.builder = DaggerTestAppComponent.builder()
SupportFragmentTestUtil.startFragment(bagFragment, AppCompatActivity::class.java)
}
But that doesn't work because of a type mismatches. If there was some solution where extending from DaggerApplication() was still a possibility I would be very interested in that.
Here's my setup:
AppComponent.kt
#Singleton
#Component(modules = [
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
AppModule::class,
ActivityModule::class,
FragmentModule::class,
ProductionModule::class])
interface AppComponent : AndroidInjector<MyApplication>{
#Component.Builder
abstract class Builder : AndroidInjector.Builder<MyApplication>()
}
ProductionModule.kt
#Module
open class ProductionModule {
#Provides
fun browseCategoriesViewModel(authenticationService: AuthenticationService): BrowseCategoriesFragment.BrowseCategoriesViewModel{
return BrowseCategoriesFragment.BrowseCategoriesViewModel(authenticationService)
}
// Provides multiple things
...
ViewModelModule.kt
#Module
abstract class ViewModelModule {
#Binds
abstract fun bindViewModelFactory(factor: ViewModelFactory): ViewModelProvider.Factory
/*** Fragments ***/
#Binds
#IntoMap
#ViewModelKey(BagFragment.BagViewModel::class)
abstract fun bindBagViewModel(viewModel: BagFragment.BagViewModel): ViewModel
MyApplication.kt
class MyApplication: DaggerApplication(), HasActivityInjector, HasSupportFragmentInjector {
#Inject
lateinit var sharedPreferencesService: SharedPreferencesService
#Inject
lateinit var activityInjector: DispatchingAndroidInjector<Activity>
#Inject
lateinit var supportFragmentInjector: DispatchingAndroidInjector<Fragment>
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}
override fun activityInjector() = activityInjector
override fun supportFragmentInjector() = supportFragmentInjector
}
The AppModule, FragmentModule, and ActivityModule are set up according to documentation, and haven't given me any issues.
Here is my test setup:
TestAppComponent.kt
#Singleton
#Component(modules =[
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
TestModule::class])
interface TestAppComponent: AndroidInjector<MyApplication>{
#Component.Builder
abstract class Builder: AndroidInjector.Builder<MyApplication>()
}
TestModule.kt
#Module
internal class TestModule {
#Provides
fun bagViewModel(dataService: FakeDataService,
authenticationService: FakeAuthenticationService,
favoritesService: FakeFavoritesService,
cartService: FakeCartService): BagFragment.BagViewModel {
return BagFragment.BagViewModel(dataService, authenticationService, favoritesService, cartService)
}
#Provides
#Singleton
fun dataService(): FakeDataService = FakeDataService()
#Provides
#Singleton
fun authenticationService(): FakeAuthenticationService = FakeAuthenticationService()
// provides many other things
My current test fragment init method looks like this:
private fun init() {
bagFragment = BagFragment.newInstance()
SupportFragmentTestUtil.startFragment(bagFragment, AppCompatActivity::class.java)
}
Last but not least, an example production fragment:
BagFragment.kt
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel: BagViewModel
override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(BagViewModel::class.java)
}
When I call SupportFragmentTestUtil.startFragment() i'm assuming that AndroidInjection.inject(this) will replace my fake test module with the production module. I've also tried putting AndroidInjection.inject(this) in an init method as opposed to onCreate but that gives me problems else where and is contrary to documentation. Please help!