msamhoury
msamhoury

Reputation: 331

How to make MVRX viewmodels work with dagger2?

Edit

Adding the @ViewModelKey and making sure all the viewmodels have the @Inject annotation did the trick

Injecting ViewModels using Dagger2 Di library and ViewModelFactory causing missing binding build error.

The error I am getting is the following:

 AppComponent.java:12: error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an @Provides-annotated method. public abstract interface AppComponent extends dagger.android.AndroidInjector<com.honing.daggerexploration.DaggerExplorationApplication> {
                ^
      java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
          com.honing.daggerexploration.di.DaggerViewModelFactory(creators)
      com.honing.daggerexploration.di.DaggerViewModelFactory is injected at
          com.honing.daggerexploration.features.MainActivity.viewModelFactory
      com.honing.daggerexploration.features.MainActivity is injected at
          dagger.android.AndroidInjector.inject(T) [com.honing.daggerexploration.di.AppComponent → com.honing.daggerexploration.di.modules.ActivityModule_BindActivityMain.MainActivitySubcomponent]

I have searched other stackoverflow questions but non of them solved the issue for me.

Dagger version I am using is the latest, 2.22.1

I bet this error is not related to MVRX as I was able to reproduce it in small library without involving mvrx view model class, however my intentions is to eventually use dagger2 with mvrx framework and be able to inject dependencies to it.

Some code related to this:

DaggerExplorationApplication

class DaggerExplorationApplication : DaggerApplication() {
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.builder().create(this)
    }

}

DaggerViewModelFactory:

    /**
     * ViewModelFactory which uses Dagger to create the instances.
     */
    class DaggerViewModelFactory @Inject constructor(
        private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
    ) : ViewModelProvider.Factory {
        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 {
                @Suppress("UNCHECKED_CAST")
                return creator.get() as T
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        }
    }

ViewModelFactoryModule

@Module
abstract class ViewModelFactoryModule {

    @Binds
    abstract fun bindViewModelFactory(viewModelFactory: DaggerViewModelFactory): ViewModelProvider.Factory
}

ActivityModule

@Module
abstract class ActivityModule {


    @ContributesAndroidInjector(modules = [ViewModelFactoryModule::class])
    abstract fun bindActivityMain(): MainActivity
}

In regards my efforts to implement mvrx with dagger, according to this I need to use AssistedInject library by square, I watched the video and fairly understand the reason behind this. Yet I am failed in making the project build because of the error described above. An interesting thread by chrisbanes also about this thing is on this link

MVRX ViewModels with dagger2 has been implemented successfully using this project(Tivi) by chrisbanes, I tried following what they did, but I failed too. The issue described at the top of the post is blocking me. Ready to provide any missing code, more info if needed to be able to resolve this issue.

Upvotes: 1

Views: 683

Answers (2)

EpicPandaForce
EpicPandaForce

Reputation: 81539

You are missing the configuration for the map multi-binding.

Tivi has the @ViewModelKey:

/*
 * Copyright 2017 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package app.tivi.inject

import androidx.lifecycle.ViewModel
import dagger.MapKey
import kotlin.reflect.KClass

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

And it has a module that binds the ViewModelKey to a particular subtype of ViewModel in such a way that it is exposed as ViewModel (and marked with a key):

/*
 * Copyright 2017 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@Binds
@IntoMap
@ViewModelKey(PopularShowsViewModel::class)
abstract fun bindPopularShowsViewModel(viewModel: PopularShowsViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(TrendingShowsViewModel::class)
abstract fun bindTrendingShowsViewModel(viewModel: TrendingShowsViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(ShowDetailsNavigatorViewModel::class)
abstract fun bindDetailsNavigatorViewModel(viewModel: ShowDetailsNavigatorViewModel): ViewModel

So you need to set these multi-binding configuration to a component using a module.

And it is also important that their ViewModel classes have @Inject annotated constructor for this to work.

Upvotes: 1

dglozano
dglozano

Reputation: 6607

This is how I made my Android Application using MVVM architecture work with Dagger 2. It is Java, but I hope it can lead you in the right direction anyway.

AppComponent

@ApplicationScope
@Component(modules = {
        AndroidInjectionModule.class,
        AppModule.class,
        ActivityBuilder.class
})
public interface AppComponent {

    void inject(MyApp app);

    @Component.Builder
    interface Builder {

        @BindsInstance
        Builder application(Application application);

        AppComponent build();
    }
}

ViewModelFactory

@ApplicationScope
public class ViewModelFactory implements ViewModelProvider.Factory {
    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

    @Inject
    public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
        this.creators = creators;
    }

    @SuppressWarnings("unchecked")
    @Override
    @NonNull
    public <T extends ViewModel> T create(@NonNull 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);
        }
    }
}

ViewModelModule

@Module
public abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(MainActivityViewModel.class)
    abstract ViewModel bindMainActivityViewModel(MainActivityViewModel mainActivityViewModel);

    // Same thing for each view model

    ...

}

ActivityBuilder

@Module
public abstract class ActivityBuilder {

    @ContributesAndroidInjector(modules = {
            MainActivityModule.class
            // Add the Provider of each child fragment's viewmodel.
    })
    public abstract MainActivity bindMainActivity();

    // Same for each new activity
}

ViewModelKey

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
public @interface ViewModelKey {
    Class<? extends ViewModel> value();
}

ApplicationScope

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationScope {
}

AppModule

@Module(includes = {
        ViewModelModule.class,
})
public class AppModule {

    // Provides all the things needed for the whole application, such as Daos, Retrofit interface, contexts, etc.

}

MyApp

public class MyApp extends Application implements HasActivityInjector {

    @Inject
    DispatchingAndroidInjector<Activity> activityDispatchingAndroidInjector;


    @Override
    public void onCreate() {

        DaggerAppComponent
                .builder()
                .application(this)
                .build()
                .inject(this);

        super.onCreate();
    }

    @Override
    public DispatchingAndroidInjector<Activity> activityInjector() {
        return activityDispatchingAndroidInjector;
    }
}

MainActivity

public class MainActivity extends AppCompatActivity {
    @Inject
    ViewModelProvider.Factory mViewModelFactory;

    private MainActivityViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MainActivityViewModel.class);

        ...
     }
}

MainActivityModule

@Module
public class MainActivityModule {

    //Provides all the things needed just for your MainActivity

}

MainActivityViewModel

public class MainActivityViewModel extends ViewModel {

    // Member variables

    @Inject
    public MainActivityViewModel(... things to inject into the viewmodel, such as daos, repositories, contexts, etc. ... ) {
         ...
    }

}

So, it looks like a lot of configuration, but once you have finished doing the initial setup it will be easier to build on top of that.

To sum up:

  • Create the annotations @ApplicationScope (or use the default @Singleton) and, more importantly, the @ViewModelKey annotation.
  • Make your application class implement HasActivityInjector and use the DaggerAppComponent builder to make the injection.
  • Implement the AppComponent and ViewModelFactory exactly as I mentioned before.
  • Define your AppModule to provide all the things your app need. But remember to include the ViewModelModule because that one is in charge of providing the ViewModels.
  • Everytime you want to add a new activity with its own ViewModel, you have to do the following:
    1. Create the Activity, ActivityModule and ActivityViewModel.
    2. Add an entry in the ViewModelModule to bind the View Model.
    3. Add an entry in the ActivityBuilder to provide the Activity.
    4. Enjoy.

Upvotes: 1

Related Questions