I'm trying to write a UI test using Espresso, I have a view model that I need to mock. But inside this mocked viewmodel there're several #Inject fields that are null, which causing all sorts of trouble.
Here's the view model itself (Names changed and stuff for simplicity but the code is exactly the same)
public class MyViewModel extends ViewModel {
private final MyFieldRepository myFieldRepository;
#Inject
public MyViewModel(MyFieldRepository myField) {
this.myFieldRepository = myField; //remains null
}
public void start() { //NPE here
myFieldRepository.startAPI() //... async call code
}
}
And the view model Dagger Module
#Module
public class MyViewModelModule {
#Provides
#IntoMap
#ViewModelKey(MyViewModel.class)
public ViewModel providesMyViewModel() {
MyViewModel myViewModel = Mockito.mock(MyViewModel.class);
return pinPadViewModel;
}
}
And finally the field module (that I wanted to be injected)
#Module
public class MyFieldRepositoryModule {
#Provides
#AppScoped
public MyFieldRepository providesMyField() {
MyFieldRepository myField = Mockito.mock(MyFieldRepository.class);
return myField;
}
}
And here's the component code:
#AppScoped
#Component(modules = {
MyViewModelModule.class,
MyFieldModule.class,
})
public interface TestAppComponent extends AndroidInjector<TestApp> {
#Component.Builder
interface Builder {
#BindsInstance
Builder application(Application application);
TestAppComponent build();
}
}
I want to find a way for myField to be injected by Dagger using MyFieldModule.
You have to Use ViewModelProvider.Factory to provide dependency
#Singleton
class ViewModelFactory #Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
}
Related
I receive following error message from dagger at compile time. It finds a dependency cycle in dagger :
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.sample.android.storytel.StorytelApplication> {
^
androidx.lifecycle.ViewModelProvider.Factory is injected at
com.sample.android.storytel.ui.DetailFragment.factory
com.sample.android.storytel.ui.DetailFragment is injected at
com.sample.android.storytel.di.DetailModule.providePost$app_debug(fragment)
com.sample.android.storytel.domain.Post is injected at
com.sample.android.storytel.viewmodels.DetailViewModel.Factory(…, post)
com.sample.android.storytel.viewmodels.DetailViewModel.Factory is injected at
com.sample.android.storytel.di.DetailModule.bindViewModelFactory$app_debug(factory)
androidx.lifecycle.ViewModelProvider.Factory is injected at
com.sample.android.storytel.ui.DetailFragment.factory
com.sample.android.storytel.ui.DetailFragment is injected at
dagger.android.AndroidInjector.inject(T) [com.sample.android.storytel.di.AppComponent → com.sample.android.storytel.di.ActivityBindingModule_MainActivity$app_debug.MainActivitySubcomponent → com.sample.android.storytel.di.DetailModule_DetailFragment$app_debug.DetailFragmentSubcomponent]
Here is my DetailModule :
#Module
abstract class DetailModule {
#ContributesAndroidInjector
internal abstract fun detailFragment(): DetailFragment
#Binds
internal abstract fun bindViewModelFactory(factory: DetailViewModel.Factory): ViewModelProvider.Factory
#Module
companion object {
#Provides
#JvmStatic
internal fun providePost(fragment: DetailFragment): Post =
DetailFragmentArgs.fromBundle(fragment.arguments!!).post
}
}
Here is AppComponent :
#Singleton
#Component(
modules = [ActivityBindingModule::class,
AndroidSupportInjectionModule::class,
Network::class,
BaseModule::class]
)
interface AppComponent : AndroidInjector<StorytelApplication> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
}
Here is ActivityBindingModule :
#Module
abstract class ActivityBindingModule {
#ContributesAndroidInjector(
modules = [MainModule::class,
DetailModule::class]
)
internal abstract fun mainActivity(): MainActivity
}
And this is my DetailViewModel Factory :
/**
* Factory for constructing DetailViewModel with parameter
*/
class Factory #Inject constructor(
private val useCase: DetailUseCase,
val post: Post
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DetailViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return DetailViewModel(useCase, post) as T
}
throw IllegalArgumentException("Unable to construct viewmodel")
}
}
I have injected factory at my DetailFragment :
class DetailFragment #Inject
constructor() // Required empty public constructor
: DaggerFragment() {
#Inject
lateinit var factory: DetailViewModel.Factory
private val viewModel: DetailViewModel by lazy {
ViewModelProviders.of(this, factory)
.get(DetailViewModel::class.java)
}
}
I change my providePost method by passing MainActivity as parameter instead of DetailFragment, and problem resolved :
#Provides
#JvmStatic
internal fun providePost(activity: MainActivity): Post {
val navHostFragment = activity.supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment)
val fragment = navHostFragment?.childFragmentManager?.fragments?.get(0);
return DetailFragmentArgs.fromBundle(fragment?.arguments!!).post
}
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
I'm creating an application in Kotlin using the MVP pattern.
I would need to inject a Repository into my Presenter for this purpose. Except that for this, my Repository requires a Retrofit interface as a parameter of its constructuor.
I'm a beginner in the use of Dagger2, and the answers found on the internet are far too complicated for such a basic case like mine.
Here's the repository i want to be injected :
class RepositoryInventory(private val api: Service): IRepositoryInventory {
override fun getInventoryItemByNum(itemnum: String): Observable<Response<Item>> {
return api.getInventoryItemByNum(itemnum)
.toObservable()
}
override fun getAllInventoryItems(): Single<Response<Item>> {
return api.getAllInventoryItems()
}
}
My Component
#Singleton
#Component(modules = arrayOf(ActivityModule::class))
interface ActivityComponent {
fun inject(loginActivity: LoginActivity)
fun inject(itemDetailActivity: ItemDetailActivity)
}
My module :
#Module
class ActivityModule(private var activity: Activity) {
#Provides
fun provideActivity(): Activity {
return activity
}
#Provides
fun provideLoginPresenter(): LoginPresenter {
return LoginPresenter()
}
#Provides
fun provideItemDetailPresenter(): ItemDetailPresenter {
return ItemDetailPresenter()
}
}
In my activity, my module is injected with this method :
private fun injectDependency() {
val activityComponent = DaggerActivityComponent.builder()
.activityModule(ActivityModule(this))
.build()
activityComponent.inject(this)
}
I have 2 components and 2 modules: one designed to inject into a fragment and the other into an activity.
Except in my case, I want to inject into a Presenter that is not a Fragment or an Activity but a class
Ok, my guess is you want to inject RepositoryInventory into LoginPresenter. If so, you can make use of #ContributesAndroidInjector and Binds
First, create a LoginActivityModule
#Module
abstract class LoginActivityModule {
#Binds
abstract fun loginPresenter(loginPresenter: LoginPresenter): LoginPresenter
}
Then, create a module called ActivityBindingModule
#Module
abstract class ActivityBindingModule {
#ContributesAndroidInjector(modules = [LoginActivityModule::class])
abstract fun loginActivity(): LoginActivity
#ContributesAndroidInjector()
abstract fun itemDetailActivity(): ItemDetailActivity
}
And change your ActivityComponent like this
#Singleton
#Component(modules = arrayOf(ActivityModule::class, ActivityBindingModule::class))
interface ActivityComponent {
}
And in your LoginPresenter:
class LoginPresenter #Inject constructor(private val repositoryInventory: IRepositoryInventory) {
...
}
Remember to remove this in ActivityModule:
#Provides
fun provideLoginPresenter(): LoginPresenter {
return LoginPresenter()
}
I try understanding ViewModel. I create ViewModel:
public class UsersViewModel extends ViewModel {
private final UsersRepository usersRepository;
public UsersViewModel(UsersRepository usersRepository) {
this.usersRepository = usersRepository;
}
public LiveData<List<User>> loadAll() {
return usersRepository.getAll();
}
}
But I not understand 2 things:
How can I inject UsersRepository to this VievModel? When I used presenter I can create it with dagger 2 like this:
#Module
public class PresentersModule {
#Singleton
#Provides
UsersPresenter provideUsersPresenter(UsersRepository usersRepository) {
return new UsersPresenter(usersRepository);
}
}
but how can I do it with ViewModel? Like this?
#Module
public class ViewModelsModule {
#Singleton
#Provides
UsersViewModel provideUsersViewModel(UsersRepository usersRepository) {
return new UsersViewModel(usersRepository);
}
}
How can I get this ViewModel in fragment? With presenter I can this:
presenter = MyApplication.get().getAppComponent().getUsersPresenter();
ViewModel is created via ViewModelProvider that uses a ViewModelFactory to create the instances. You can't inject ViewModels directly, instead you should use a custom factory like below
#Singleton
public class DaggerViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
#Inject
public DaggerViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
#SuppressWarnings("unchecked")
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Then you need a module for the dagger that creates the view model factory and view models itself.
#Module
abstract class ViewModelModule {
#Binds
abstract ViewModelProvider.Factory bindViewModelFactory(DaggerViewModelFactory factory);
#Binds
#IntoMap
#ViewModelKey(VideoListViewModel.class)
abstract ViewModel provideVideoListViewModel(VideoListViewModel videoListViewModel);
#Binds
#IntoMap
#ViewModelKey(PlayerViewModel.class)
abstract ViewModel providePlayerViewModel(PlayerViewModel playerViewModel);
#Binds
#IntoMap
#ViewModelKey(PlaylistViewModel.class)
abstract ViewModel providePlaylistViewModel(PlaylistViewModel playlistViewModel);
#Binds
#IntoMap
#ViewModelKey(PlaylistDetailViewModel.class)
abstract ViewModel providePlaylistDetailViewModel(PlaylistDetailViewModel playlistDetailViewModel);
}
ViewModelKey file is like this
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#MapKey
#interface ViewModelKey {
Class<? extends ViewModel> value();
}
Now to get your view model in the activity or fragment simply inject the view model factory and then use that factory to create the view model instances
public class PlayerActivity extends BaseActivity {
#Inject DaggerViewModelFactory viewModelFactory;
PlayerViewModel viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
viewModel = ViewModelProviders.of(this,viewModelFactory).get(PlayerViewModel.class);
}
To inject anything to your ViewModel like the repository simply use Constructor Injection.
public class PlayerViewModel extends ViewModel {
private VideoRepository videoRepository;
private AudioManager audioManager;
#Inject
public PlayerViewModel(VideoRepository videoRepository, AudioManager audioManager) {
this.videoRepository = videoRepository;
this.audioManager = audioManager;
}
}
Check out the fully working example from here https://github.com/alzahm/VideoPlayer, Also I learned many of the dagger stuff from google samples, you can check them out as well.
Dagger2 required you to create ViewModelModule and do the binding to your ViewModels
I know you are asking about Dagger2, but if you are starting a new project, maybe you can check out Koin which provides a lightweight injection.
I've used it in some of my production apps and it works fine with lesser lines of code.
You can just declare in your module like
viewModel { MyViewModel(get()) }
Then in your activities / fragments / classes (you need to extend KoinComponent), just write
val myViewModel : MyViewModel by viewModel()
It will handle the creation itself.
For more information can refer
https://insert-koin.io/
https://start.insert-koin.io/#/getting-started/koin-for-android?id=android-viewmodel
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
})
}
}
This solution allows you to inject dependencies into ViewModels and Workers without factories. Instead, it uses static methods.
Injecting the Graph into a class
This line can be used in the init block or the onCreate method (Example ViewModel at the bottom)
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
.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
}
}
That's it!
Request injection like usual in your ApplicationComponent
ApplicationComponent
Singleton
#Component(modules = [add your modules])
interface ApplicationComponent {
fun inject(someViewModel: SomeViewModel)
}
ViewModel
class SomeViewModel : ViewModel(){
#Inject
lateinit var someClass: SomeClass //In your case, userRepository: UserRepository
init{
Injector.getComponent().inject(this)
}
}
The error I have is following:
Caused by: java.lang.IllegalArgumentException: No injector factory
bound for Class. Injector factories were bound for
supertypes of MyActivity_: [MyActivity]. Did you mean to bind an
injector factory for the subtype?
As I understand it happens because I am using an AndroidAnnotations library.
AppComponent.class :
#Singleton
#Component(modules = {
AndroidInjectionModule.class,
AppModule.class,
ActivityBindingModule.class })
public interface AppComponent extends AndroidInjector<DaggerApplication> {
#Component.Builder
interface Builder {
#BindsInstance Builder application(Application application);
AppComponent build();
}
void inject(Application application);
#Override
void inject(DaggerApplication instance);
}
AppModule.class :
#Module
public abstract class AppModule {
#Binds
abstract Context provideContext(Application application);
#Provides
#Singleton
static SharedPreferencesManager providesPreferences(Application application){
return SharedPreferencesManager_.getInstance_(application);
}
}
ActivityBindingModule.class :
#Module
public abstract class ActivityBindingModule {
#ContributesAndroidInjector(modules = LoginActivityModule.class)
#LoginActivityScope
abstract LoginActivity bindLoginActivity();
}
Application.class :
#EApplication
public class Application extends DaggerApplication {
#Inject
DispatchingAndroidInjector<Activity> activityDispatchingAndroidInjector;
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
AppComponent appComponent = DaggerAppComponent.builder()
.application(this)
.build();
appComponent.inject(this);
return appComponent;
}
}
LoginActivityModule.class
#Module
public class LoginActivityModule {
#Provides
#LoginActivityScope
#ActivityContext
public Context providesContext(LoginActivity loginActivity){
return loginActivity;
}
#Provides
#LoginActivityScope
public LoginViewModel providesLoginViewModel(TelephonyManager telephonyManager,
LoginModel loginModel,
SharedPreferencesManager sharedPreferencesManager,
LoginRemoteRepository loginRemoteRepository){
return new LoginViewModel(telephonyManager, loginModel, sharedPreferencesManager, loginRemoteRepository,
new CompositeSubscription());
}
#Provides
#LoginActivityScope
public LoginRemoteRepository providesRemoteRepository(#ActivityContext Context context,
MainApi mainApi,
SharedPreferencesManager sharedPreferencesManager){
return new LoginRemoteRepository(mainApi, sharedPreferencesManager, context.getContentResolver());
}
#Provides
#LoginActivityScope
public LoginModel provideLoginModel(){
return new LoginModel();
}
#Provides
#LoginActivityScope
public TelephonyManager provideTelephonyManager(Context context){
return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
}
}
LoginActivity.class :
#EActivity(R.layout.activity_login)
public class LoginActivity {
#Inject
LoginViewModel loginViewModel;
#AfterViews
void afterViews(){
AndroidInjection.inject(this);
}
}
How to deal with Dagger 2 error:
"Injector factories were bound for supertypes of ... Did you mean to bind an injector factory for the subtype?"
Suppose we do have some BaseActivity:
open class BaseActivity : MvpAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
}
We do also have an ActivityBuilder Module (which provides the BaseActivity itself) and we have a Module which provides
the dependencies needed for the successful running of the BaseActivity.
#Module
abstract class ActivityBuilder {
#ContributesAndroidInjector(modules = [BaseModule::class])
abstract fun bindBaseActivity(): BaseActivity
}
If we will add a ChildActivity like that
class ChildActivity : BaseActivity() {
}
thinking: "Hey, Dagger 2 will satisfy the dependencies for the BaseActivity and since we are extending
it, we will get a ChildActivity up and running, right?". Wrong. We will get an exception "Injector factories were bound for supertypes of ... Did you mean to bind an injector factory for the subtype?"
What we should do? We should make Dagger 2 know explicitly about our ChildActivity.
a) Add AndroidInjection.inject(this) to the ChildActivity onCreate() method:
class ChildActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
}
b) add all subclasses of the BaseActivity to the ActivityBuilder module (which provides activities).
Note, that the BaseModule, satisfying dependencies for the BaseActivity, should also be included
to the #ContributesAndroidInjector of the child class (ChildActivity)
#Module
abstract class ActivityBuilder {
#ContributesAndroidInjector(modules = [BaseModule::class])
abstract fun bindBaseActivity(): BaseActivity
//Adding subclasses
#ContributesAndroidInjector(modules = [BaseModule::class])
abstract fun bindChildActivity(): ChildActivity
}
Just an informed guess: It probably happens because your Class is actually called different when you use AndroidAnnotations (they add the underscore somewhere). Then you have to define the binding also like so (not sure where the underscore goes):
#ContributesAndroidInjector(modules = LoginActivityModule_.class)
#LoginActivityScope
abstract LoginActivity bindLoginActivity();