How to repleace dependency in hilt with dynamic feature module in instrumentTest? - android

I am working on instrument test in android studio.
My app is Dynamic Feature Module use both dagger and hilt to provide dependency.
I am following this doc
https://developer.android.com/training/dependency-injection/hilt-testing#replace-binding
As it said I need to Replace a binding but when on build I got error
com.xxxxxxx.CartRepository cannot be provided without an #Provides-annotated method.
public abstract static class SingletonC implements CheckoutFragmentTest_GeneratedInjector,
This is my dagger hilt version
"com.google.dagger:hilt-android:2.38.1"
"com.google.dagger:hilt-compiler:2.38.1"
"com.google.dagger:hilt-android-testing:2.38.1"
"com.google.dagger:hilt-android-compiler:2.38.1"
"androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02"
"androidx.hilt:hilt-compiler:1.0.0-alpha02"
"androidx.hilt:hilt-common:1.0.0-alpha02"
Here is how I provide dependency in production code is feature module
Inject.kt file in feature cart module
//Inject.kt file in feature cart module
internal fun inject(fragment: CheckoutFragment) {
DaggerCartComponent
.factory()
.create(
fragment,
coreComponent(fragment),
appComponent(fragment)
)
.inject(fragment)
}
private fun appComponent(fragment: Fragment): AppComponent =
EntryPointAccessors.fromActivity(
fragment.requireActivity(),
AppComponent::class.java
)
private fun coreComponent(fragment: Fragment): CoreComponent =
EntryPointAccessors.fromApplication(
fragment.requireActivity().applicationContext,
CoreComponent::class.java
)
feature cart module component
//feature cart module component
#Component(
dependencies = [CoreComponent::class, AppComponent::class],
modules = [
CartViewModelModule::class,
CartDataModule::class
]
)
interface CartComponent {
fun inject(fragment: CartFragment)
fun inject(fragment: CheckoutFragment)
fun inject(fragment: CartNavigationFragment)
fun inject(fragment: PickupInformationFragment)
#Component.Factory
interface Factory {
fun create(
#BindsInstance fragment: Fragment,
coreComponent: CoreComponent,
appComponent: AppComponent
): CartComponent
}
}
CartDataModule
#[Module InstallIn(FragmentComponent::class)]
abstract class CartDataModule {
#Binds
abstract fun provideCartRepository(cartRepositoryImpl: CartRepositoryImpl) : CartRepository
#Binds
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: CheckoutRepositoryImpl) : CheckoutRepository
#Binds
abstract fun provideMainRepository(mainRepositoryImpl: MainRepositoryImpl): MainRepository
}
I use this as a factory to create my viewmodel that hold my repository
class DFMSavedStateViewModelFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle?,
private val delegateFactory: SavedStateViewModelFactory,
private val viewModelFactories: #JvmSuppressWildcards Map<String, Provider<ViewModelAssistedFactory<out ViewModel>>>,
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
#SuppressLint("RestrictedApi")
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
val factoryProvider = viewModelFactories[modelClass.name]
?: return delegateFactory.create("$KEY_PREFIX:$key", modelClass)
#Suppress("UNCHECKED_CAST")
return factoryProvider.get().create(handle) as T
}
companion object {
private const val KEY_PREFIX = "androidx.hilt.lifecycle.HiltViewModelFactory"
}
}
And Here is my code under androidTest folder
CheckoutFragmentTest.kt
#ExperimentalCoroutinesApi
#UninstallModules(
CartDataModule::class,
)
#HiltAndroidTest
class CheckoutFragmentTest {
#Module
#InstallIn(FragmentComponent::class)
abstract class TestModule1 {
#Binds
#Singleton
abstract fun provideCartRepository(cartRepositoryImpl: FakeCartRepositoryImpl): CartRepository
#Binds
#Singleton
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: FakeCheckoutRepositoryImpl): CheckoutRepository
}
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
var activityScenarioRule = activityScenarioRule<MainActivity>()
#Before
fun init() {
hiltRule.inject()
}
#Test
fun testCheckoutUICase1() {
launchFragmentInHiltContainer<CheckoutFragment> {
}
}
}
And this FakeCartDataModule that assume to replace CartDataModule in production code
#Module
#TestInstallIn(
components = [FragmentComponent::class],
replaces = [CartDataModule::class]
)
abstract class FakeCartDataModule {
#Binds
abstract fun provideCartRepository(cartRepositoryImpl: FakeCartRepositoryImpl): CartRepository
#Binds
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: FakeCheckoutRepositoryImpl): CheckoutRepository
}
Does anyone know why I cannot provide FakeCartDataModule to run my androidTest?
Despite of I already following along the doc

Related

Android Hilt cannot be provided without an #Provides-annotated method. in repository Interface Class

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
}

How to use AssistedInject to pass dynamic value as a parameter to ViewModel when using Dagger2 in Android

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
}
}

ViewModel cannot be provided without an #Inject constructor or an #Provides-annotated

Question EDITED
I am injecting ViewModelProvider.Factory to BaseActivity like below
open class BaseActivity : DaggerAppCompatActivity() {
#Inject
lateinit var factories: ViewModelProvider.Factory
inline fun <reified T : ViewModel> getViewModel(): T {
return ViewModelProvider(this, factories).get(T::class.java)
}
}
viewModel only works when we inject then like below.
class MainViewModel #Inject constructor( private val alertStore: AlertStore)
: BaseViewModel(){
fun showDialog(){
viewModelScope.launch {
delay(4000)
alertStore.showToast("Alert after 4 seconds.")
}
}
}
Why this #Inject constructor is necessary in my current implementation
class MainActivity : BaseActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = getViewModel()
viewModel.showDialog()
}
}
App.kt
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().addContext(this).build()
}
}
AppComponent.kt
#Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
ActivityBuilder::class,
ViewModelInjector::class
]
)
#Singleton
interface AppComponent : AndroidInjector<App> {
#Component.Builder
interface Builder {
fun addContext(#BindsInstance context: Context): Builder
fun build(): AppComponent
}
}
AppModule.kt
#Module
class AppModule {
#Provides
fun provideViewModelFactories(viewModels: Map<Class<out ViewModel>,
#JvmSuppressWildcards Provider<ViewModel>>):
ViewModelProvider.Factory {
return object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val factory = viewModels[modelClass]?.get() ?: error(
"No factory provided against ${modelClass.name}"
)
#Suppress("UNCHECKED_CAST")
return factory as T
}
}
}
}
ActivityBuilder.kt
#Module
abstract class ActivityBuilder {
//#Scope("")
#ContributesAndroidInjector ///(modules = {MainModelFactory.class})
public abstract MainActivity bindMainActivity();
}
ViewModelInjector.kt
#Module
public abstract class ViewModelInjector {
#Binds
#IntoMap
#ViewModelKey(MainViewModel.class)
public abstract ViewModel providesMainViewModel(MainViewModel model);
}
ViewModelKey.kt
#MapKey
#Retention(AnnotationRetention.SOURCE)
annotation class ViewModelKey(
val value: KClass<out ViewModel>
)
Why do I have to append #Inject constructor to each ViewModel and kindly explain a little why we need #Binds #IntoMap and with ViewModel
When you use dagger android you should make your activities and fragments as extensions of DaggerActivity (and DaggerFragment for fragments).
class MainActivity : DaggerActivity() {
#Inject
lateinit var viewModel: MainViewModel
}
Next you should prepare infrastructure for injection:
Create injectors for each your activity:
// All your injectors can be defined in this module
#Module(includes = [AndroidInjectionModule::class])
interface AppInjectorModule {
#ContributesAndroidInjector(modules = [MainActivityVmModule::class, /*other dependecies*/])
fun getMainActivityInjector(): MainActivity
}
Create modules to provide view models (can be multiple for one activity) and factory
#Module(includes = [VmFactoryModule::class])
abstract class MainActivityVmModule {
// bind implementation of ViewModel into map for ViewModelFactory
#Binds
#IntoMap
#ClassKey(MainViewModelImpl::class)
abstract fun bindMainVm(impl: MainViewModelImpl): ViewModel
#Module
companion object {
#Provides
#JvmStatic
fun getMainVm(activity: MainActivity, factory: ViewModelProvider.Factory): MainViewModel {
// create MainViewModelImpl in scope of MainActivity and inject dependecies by ViewModelFactory
return ViewModelProviders.of(activity, factory)[MainViewModelImpl::class.java]
}
}
}
Factory can be provided by different module to avoid duplication
#Module
interface VmFactoryModule {
#Binds
// bind your implementation of factory
fun bindVmFactory(impl: ViewModelFactory): ViewModelProvider.Factory
}
Add activities injectors to AppComponent graph
#Component(
modules = [
AppInjectorModule::class
]
)
#Singleton
interface AppComponent : AndroidInjector<App>
Additional info: Dagger & Android

How to have view models implement test services, while dagger 2 setup extends DaggerApplication, and uses a Component.Builder

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!

Inject ViewModel using Dagger 2 + Kotlin + ViewModel

class SlideshowViewModel : ViewModel() {
#Inject lateinit var mediaItemRepository : MediaItemRepository
fun init() {
What goes here?
}
So I'm trying to learn Dagger2 so I can make my apps more testable. Problem is, I've already integrated Kotlin and am working on the Android Architectural components. I understand that constructor injection is preferable but this isn't possible with ViewModel. Instead, I can use lateinit in order to inject but I'm at a loss to figure out how to inject.
Do I need to create a Component for SlideshowViewModel, then inject it? Or do I use the Application component?
gradle:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
kapt {
generateStubs = true
}
dependencies {
compile "com.google.dagger:dagger:2.8"
annotationProcessor "com.google.dagger:dagger-compiler:2.8"
provided 'javax.annotation:jsr250-api:1.0'
compile 'javax.inject:javax.inject:1'
}
Application Component
#ApplicationScope
#Component (modules = PersistenceModule.class)
public interface ApplicationComponent {
void injectBaseApplication(BaseApplication baseApplication);
}
BaseApplication
private static ApplicationComponent component;
#Override
public void onCreate() {
super.onCreate();
component = DaggerApplicationComponent
.builder()
.contextModule(new ContextModule(this))
.build();
component.injectBaseApplication(this);
}
public static ApplicationComponent getComponent() {
return component;
}
You can enable constructor injection for your ViewModels. You can check out Google samples to see how to do it in Java. (Update: looks like they converted the project to Kotlin so this URL no longer works)
Here is how to do a similar thing in Kotlin:
Add ViewModelKey annotation:
import android.arch.lifecycle.ViewModel
import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import dagger.MapKey
import kotlin.reflect.KClass
#Suppress("DEPRECATED_JAVA_ANNOTATION")
#Documented
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Add ViewModelFactory:
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
#Singleton
class ViewModelFactory #Inject constructor(
private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("unknown model class " + modelClass)
}
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Add ViewModelModule:
import dagger.Module
import android.arch.lifecycle.ViewModel
import dagger.multibindings.IntoMap
import dagger.Binds
import android.arch.lifecycle.ViewModelProvider
import com.bubelov.coins.ui.viewmodel.EditPlaceViewModel
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(EditPlaceViewModel::class) // PROVIDE YOUR OWN MODELS HERE
internal abstract fun bindEditPlaceViewModel(editPlaceViewModel: EditPlaceViewModel): ViewModel
#Binds
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
Register your ViewModelModule in your component
Inject ViewModelProvider.Factory in your activity:
#Inject lateinit var modelFactory: ViewModelProvider.Factory
private lateinit var model: EditPlaceViewModel
Pass your modelFactory to each ViewModelProviders.of method:
model = ViewModelProviders.of(this, modelFactory)[EditPlaceViewModel::class.java]
Here is the sample commit which contains all of the required changes: Support constructor injection for view models
Assuming you have a Repository class that can be injected by Dagger and a MyViewModel class that has a dependency on Repository defined as such:
class Repository #Inject constructor() {
...
}
class MyViewModel #Inject constructor(private val repository: Repository) : ViewModel() {
...
}
Now you can create your ViewModelProvider.Factory implementation:
class MyViewModelFactory #Inject constructor(private val myViewModelProvider: Provider<MyViewModel>) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return myViewModelProvider.get() as T
}
}
Dagger setup does not look too complicated:
#Component(modules = [MyModule::class])
interface MyComponent {
fun inject(activity: MainActivity)
}
#Module
abstract class MyModule {
#Binds
abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
}
Here's the activity class (might be fragment as well), where the actual injection takes place:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var factory: ViewModelProvider.Factory
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// retrieve the component from application class
val component = MyApplication.getComponent()
component.inject(this)
viewModel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
}
}
No. You create a component where you are declaring (using) your viewModel. It is normally an activity/fragment. The viewModel has dependencies (mediaitemrepository), so you need a factory. Something like this:
class MainViewModelFactory (
val repository: IExerciseRepository): ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(p0: Class<T>?): T {
return MainViewModel(repository) as T
}
}
Then the dagger part (activity module)
#Provides
#ActivityScope
fun providesViewModelFactory(
exerciseRepos: IExerciseRepository
) = MainViewModelFactory(exerciseRepos)
#Provides
#ActivityScope
fun provideViewModel(
viewModelFactory: MainViewModelFactory
): MainViewModel {
return ViewModelProviders
.of(act, viewModelFactory)
.get(MainViewModel::class.java)
}
Refer to a repo I created when I was learning dagger+kotlin
Essentially you need a ViewModelFactory instance to the UI layer, you use that to create a viewmodel.
#AppScope
class ViewModelFactory
#Inject
constructor(private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>)
: ViewModelProvider.Factory {
#SuppressWarnings("Unchecked")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator = creators[modelClass]
if (creator == null) {
for (entry in creators) {
if (modelClass.isAssignableFrom(entry.key)) {
creator = entry.value
break
}
}
}
if (creator == null) throw IllegalArgumentException("Unknown model class" + modelClass)
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
Your ViewModelModule should look like (this is where you store all viewmodels).
#Module
abstract class ViewModelModule {
#AppScope
#Binds
#IntoMap
#ViewModelKey(YourViewModel::class)
abstract fun bindsYourViewModel(yourViewModel: YourViewModel): ViewModel
// Factory
#AppScope
#Binds abstract fun bindViewModelFactory(vmFactory: ViewModelFactory): ViewModelProvider.Factory
}
Then create a dagger map key
#Documented
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
Then on your UI layer, inject the factory and instantiate your viewmodel using ViewModelProviders
class YourActivity : BaseActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var yourViewModel: YourViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
...
(application as App).component.inject(this)
}
override fun onStart() {
super.onStart()
yourViewModel = ViewModelProviders.of(this, viewModelFactory).get(YourViewModel::class.java)
// you can now use your viewmodels properties and methods
yourViewModel.methodName()
yourViewModel.list.observe(this, { ... })
}
you expose the ViewModel on your component:
#Singleton
#Component(modules={...})
public interface SingletonComponent {
BrandsViewModel brandsViewModel();
}
And now you can access this method on the component inside the ViewModelFactory:
// #Inject
BrandsViewModel brandsViewModel;
...
brandsViewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {
#Override
public <T extends ViewModel> create(Class<T> modelClazz) {
if(modelClazz == BrandsViewModel.class) {
return singletonComponent.brandsViewModel();
}
throw new IllegalArgumentException("Unexpected class: [" + modelClazz + "]");
}).get(BrandsViewModel.class);
All this can be simplified and hidden with Kotlin:
inline fun <reified T: ViewModel> AppCompatActivity.createViewModel(crossinline factory: () -> T): T = T::class.java.let { clazz ->
ViewModelProvider(this, object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass == clazz) {
#Suppress("UNCHECKED_CAST")
return factory() as T
}
throw IllegalArgumentException("Unexpected argument: $modelClass")
}
}).get(clazz)
}
which now lets you do
brandsViewModel = createViewModel { singletonComponent.brandsViewModel() }
Where now BrandsViewModel can receive its parameters from Dagger:
class BrandsViewModel #Inject constructor(
private val appContext: Context,
/* other deps */
): ViewModel() {
...
}
Though the intent might be cleaner if a Provider<BrandsViewModel> is exposed from Dagger instead
interface SingletonComponent {
fun brandsViewModel(): Provider<BrandsViewModel>
}
brandsViewModel = createViewModel { singletonComponent.brandsViewModel().get() }
Try with below code :
#Provides
#Singleton
fun provideRepository(): Repository {
return Repository(DataSource())
}
I wrote a library that should make this more straightforward and way cleaner, no multibindings or factory boilerplate needed, while also giving the ability to further parametrise the ViewModel at runtime:
https://github.com/radutopor/ViewModelFactory
#ViewModelFactory
class UserViewModel(#Provided repository: Repository, userId: Int) : ViewModel() {
val greeting = MutableLiveData<String>()
init {
val user = repository.getUser(userId)
greeting.value = "Hello, $user.name"
}
}
In the view:
class UserActivity : AppCompatActivity() {
#Inject
lateinit var userViewModelFactory2: UserViewModelFactory2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
appComponent.inject(this)
val userId = intent.getIntExtra("USER_ID", -1)
val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
.get(UserViewModel::class.java)
viewModel.greeting.observe(this, Observer { greetingText ->
greetingTextView.text = greetingText
})
}
}
Here is my solution using reflection.
Let's say for simplicity you have AppComponent
#AppScope
#Component(modules = [AppModule::class])
interface AppComponent {
fun getAppContext(): Context
fun getRepository(): Repository
fun inject(someViewModel: SomeViewModel)
class App : Application() {
companion object {
lateinit var appComponent: AppComponent
private set
}
...
}
fun appComponent() = App.appComponent
And you need inject SomeViewModel class
class SomeViewModel: ViewModel() {
#Inject
lateinit var repository: Repository
}
Create custom lazy property delegate
inline fun <reified T: ViewModel> Fragment.viewModel(component: Any?) = lazy {
val vm = ViewModelProvider(this).get(T::class.java)
component?.let {
val m = component.javaClass.getMethod("inject", T::class.java)
m.invoke(component, vm)
}
vm
}
And use it
class SomeFragment: Fragment() {
private val vm: SomeViewModel by viewModel(appComponent())
...
}
With the solution below, I found I can use injection anywhere I want by including this line in the init or onCreate methods (No factories needed, so it works with ViewModel and WorkManager)
Injector.getComponent().inject(this)
BaseApplication
class BaseApplication : Application() {
lateinit var applicationComponent: ApplicationComponent
override fun onCreate() {
super.onCreate()
INSTANCE = this
applicationComponent = DaggerApplicationComponent
.builder()
//Add your modules like you did in your question above
.build()
}
companion object {
private var INSTANCE: BaseApplication? = null
#JvmStatic
fun get(): BaseApplication= INSTANCE!!
}
}
Injector
class Injector private constructor() {
companion object {
#JvmStatic
fun getComponent(): ApplicationComponent = BaseApplication.get().applicationComponent
}
}
Essentially, you access applicationComponent with a static method. With that, you should be able to inject any class you've made an inject method for in your component with this line:
Injector.getComponent().inject(this)
in your case
init{
Injector.getComponent().inject(this)
}

Categories

Resources