Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM Dagger2 A Binding With Matching Key Exists in Component

I am using the following google sample project: https://github.com/googlesamples/android-architecture-components as a reference for my new project and having difficulties trying to add a second activity to the project.

Here is the error I get when compiling

Error:(22, 8) error: [dagger.android.AndroidInjector.inject(T)] com.apps.myapp.ui.common.MainActivity cannot be provided without an @Inject constructor or from an @Provides-annotated method. This type supports members injection but cannot be implicitly provided.
com.apps.myapp.ui.common.MainActivity is injected at
com.apps.myapp.ui.common.NavigationController.<init>(mainActivity)
com.apps.myapp.ui.common.NavigationController is injected at
com.apps.myapp.ui.addContacts.AddContactsFragment.navigationController
com.apps.myapp.ui.addContacts.AddContactsFragment is injected at
dagger.android.AndroidInjector.inject(arg0)
A binding with matching key exists in component: com.apps.myapp.di.ActivityModule_ContributeMainActivity.MainActivitySubcomponent

Here is my code

ActivityModule

@Module
public abstract class ActivityModule {

    @ContributesAndroidInjector(modules = FragmentBuildersModule.class)
    abstract MainActivity contributeMainActivity();

    @ContributesAndroidInjector(modules = FragmentBuildersModule.class)
    abstract ContactActivity contributeContactActivity();
} 

AppComponent

@Singleton
@Component(modules = {
        AndroidInjectionModule.class,
        AppModule.class,
        ActivityModule.class})
public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance Builder application(Application application);
        AppComponent build();
    }
    void inject(App app);
}

AppInjector

public class AppInjector {
    private AppInjector() {}
    public static void init(App app) {DaggerAppComponent.builder().application(app).build().inject(app);
                    app.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
                    @Override
                    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                        handleActivity(activity);
                    }

                    @Override
                    public void onActivityStarted(Activity activity) {

                    }

                    @Override
                    public void onActivityResumed(Activity activity) {

                    }

                    @Override
                    public void onActivityPaused(Activity activity) {

                    }

                    @Override
                    public void onActivityStopped(Activity activity) {

                    }

                    @Override
                    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

                    }

                    @Override
                    public void onActivityDestroyed(Activity activity) {

                    }
                });
    }

    private static void handleActivity(Activity activity) {
        if (activity instanceof HasSupportFragmentInjector) {
            AndroidInjection.inject(activity);
        }
        if (activity instanceof FragmentActivity) {
            ((FragmentActivity) activity).getSupportFragmentManager()
                    .registerFragmentLifecycleCallbacks(
                            new FragmentManager.FragmentLifecycleCallbacks() {
                                @Override
                                public void onFragmentCreated(FragmentManager fm, Fragment f,
                                        Bundle savedInstanceState) {
                                    if (f instanceof Injectable) {
                                        AndroidSupportInjection.inject(f);
                                    }
                                }
                            }, true);
        }
    }
}

AppModule

@Module(includes = ViewModelModule.class)
class AppModule {
    @Singleton @Provides
    BnderAPIService provideService() {
        return new Retrofit.Builder()
                .baseUrl("serverurl")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(new LiveDataCallAdapterFactory())
                .build()
                .create(APIService.class);
    }

    @Singleton @Provides
    Db provideDb(Application app) {
        return Room.databaseBuilder(app, Db.class,"Db.db").build();
    }

    @Singleton @Provides
    UserDao provideUserDao(Db db) {
        return db.userDao();
    }

    @Singleton @Provides
    ContactDao provideContactDao(Db db) {
        return db.contactDao();
    }
}

FragmentBuildersModule

@Module
public abstract class FragmentBuildersModule {

    @ContributesAndroidInjector
    abstract AddContactsFragment contributeAddUserFragment();

    @ContributesAndroidInjector
    abstract ContactsFragment contributeContactsFragment();

    @ContributesAndroidInjector
    abstract ChalkboardFragment contributeChalkboardFragment();
}

Injectable

public interface Injectable {
}

ViewModelKey

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

ViewModelModule

@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(AddContactsViewModel.class)
    abstract ViewModel bindAddContactsViewModel(AddContactsViewModel addContactsViewModel);

    @Binds
    @IntoMap
    @ViewModelKey(ContactsViewModel.class)
    abstract ViewModel bindContactsViewModel(ContactsViewModel contactsViewModel);

    @Binds
    @IntoMap
    @ViewModelKey(ChalkboardViewModel.class)
    abstract ViewModel bindChalkboardViewModel(ChalkboardViewModel chalkboardViewModel);

    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);
}

Application

public class App extends Application implements HasActivityInjector {

    @Inject
    DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {

        }
        AppInjector.init(this);
    }

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

NavigationController

public class NavigationController {
    private final int containerId;
    private final FragmentManager fragmentManager;
    @Inject
    public NavigationController(MainActivity mainActivity) {
        this.containerId = R.id.container;
        this.fragmentManager = mainActivity.getSupportFragmentManager();
    }

    public void navigateToUsers() {
        Log.i("TAG", "Navigate to users");
        String tag = "users";
        AddContactsFragment userFragment = AddContactsFragment.create();
        fragmentManager.beginTransaction()
                .replace(containerId, userFragment, tag)
                .addToBackStack(null)
                .commitAllowingStateLoss();
    }

    public void navigateToContacts() {
        Log.i("TAG", "Navigate to contacts");
        String tag = "contacts";
        ContactsFragment contactsFragment = ContactsFragment.create();
        fragmentManager.beginTransaction()
                .add(contactsFragment, tag)
                .addToBackStack(null)
                .commitAllowingStateLoss();
    }

    public void navigateToChalkboard() {
        Log.i("TAG", "Navigate to chalkboard");
        String tag = "chalkboard";
        ChalkboardFragment chalkboardFragment = ChalkboardFragment.create();
        fragmentManager.beginTransaction()
                .add(chalkboardFragment, tag)
                .addToBackStack(null)
                .commitAllowingStateLoss();
    }
}

MainActivity

public class MainActivity extends AppCompatActivity implements LifecycleRegistryOwner, HasSupportFragmentInjector {
    private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
    @Inject
    DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
    @Inject
    NavigationController navigationController;
    private Toolbar toolbar;
    @Override
    public LifecycleRegistry getLifecycle() {
        return lifecycleRegistry;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setHandler(this);
        binding.setManager(getSupportFragmentManager());
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

    @Override
    public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
        return dispatchingAndroidInjector;
    }

    static class ViewPagerAdapter extends FragmentPagerAdapter {
        private final List<Fragment> mFragmentList = new ArrayList<>();
        private final List<String> mFragmentTitleList = new ArrayList<>();

        public ViewPagerAdapter(FragmentManager manager) {
            super(manager);
        }

        @Override
        public Fragment getItem(int position) {
            return mFragmentList.get(position);
        }

        @Override
        public int getCount() {
            return mFragmentList.size();
        }

        public void addFragment(Fragment fragment, String title) {
            mFragmentList.add(fragment);
            mFragmentTitleList.add(title);
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return mFragmentTitleList.get(position);
        }
    }

    @BindingAdapter({"handler"})
    public static void bindViewPagerAdapter(final ViewPager view, final MainActivity activity) {
        final ViewPagerAdapter adapter = new ViewPagerAdapter(activity.getSupportFragmentManager());
        adapter.addFragment(new ChalkboardFragment(), "Chalkboard");
        adapter.addFragment(new ContactsFragment(), "Contacts");
        view.setAdapter(adapter);
    }

    @BindingAdapter({"pager"})
    public static void bindViewPagerTabs(final TabLayout view, final ViewPager pagerView) {
        view.setupWithViewPager(pagerView, true);
    }
}

ContactActivity

public class ContactActivity extends AppCompatActivity implements LifecycleRegistryOwner, HasSupportFragmentInjector {
    private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
    @Inject
    DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
    @Override
    public LifecycleRegistry getLifecycle() {
        return lifecycleRegistry;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contact);
        if (savedInstanceState == null) {

        }
    }

    @Override
    public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
        return dispatchingAndroidInjector;
    }
}
like image 435
Riley MacDonald Avatar asked Aug 20 '17 15:08

Riley MacDonald


People also ask

Can Dagger 2 multibindings be a lifesaver?

This class is a perfect example of when Dagger 2 multibindings can be real lifesavers. Before to dive into code, I should make a little premise.

What is MVVM and how it works?

What is MVVM? Model-View-ViewModel architecture consists of 3 parts. The View gets user’s actions and sends to the ViewModel, or listens live data stream from the ViewModel and provides to the users. The ViewModel gets user’s actions from the View or provides data to View.

How to contribute to Android-MVVM-dagger-2-rxjava-example development?

Contribute to android-mvvm-dagger-2-rxjava-example development by creating an account on GitHub.


1 Answers

"A binding with matching key exists in component" means that you have bound a dependency somewhere in your entire object graph but it cannot be reached from the subcomponent where it needs to be injected. Here is the javadoc:

Utility code that looks for bindings matching a key in all subcomponents in a binding graph so that a user is advised that a binding exists elsewhere when it is not found in the current subgraph. If a binding matching a key exists in a sub- or sibling component, that is often what the user actually wants to use.

A diagram of where the binding is present in the sibling component

For instance, assume you have two Activities, ActivityA and ActivityB. You generate subcomponents with @ContributesAndroidInjector and bind Foo in the ActivityA module but not the ActivityB module. If you request injection for Foo in ActivityB with @Inject Foo foo you will get that error message.

In your particular case, the error you are getting can be reproduced by:

  1. Cloning the project from GitHub

    git clone https://github.com/googlesamples/android-architecture-components.git`
    
  2. Copy-pasting MainActivity into a new file ContactsActivity

  3. Modifying MainActivityModule to look like your ActivityModule

So from this we can conclude that your ActivityModule is problematic. The @ContributesAndroidInjector is not as simple as it might seem. It actually means you are creating a new Dagger 2 subcomponent for the Activity you specify there (see the docs here).

A subcomponent can use bindings from a parent component but not sibling components. Your two lines for ContributesAndroidInjector in the ActivityModule create two sibling subcomponents: one for MainActivity and one for ContactsActivity.

However, NavigationController is dependent on MainActivity which is bound in the object graph for the MainActivity subcomponent but not in that for the ContactsActivity subcomponent. AddContactsFragment has become part of the object graph for the ContactsActivity subcomponent and doesn't have access to MainActivity anymore. This means that when Dagger 2 tries to inject the NavigationController inside your AddContactsFragment it cannot provide MainActivity as a dependency for it. This explains the "cannot provide" part of the error message.

Although it can't provide MainActivity in that particular object graph, the AndroidInjector does know in general about MainActivity hence the error message "a binding key exists". What binding key is this? A key that binds MainActivity.class to a MainActivityFactory. Where is this key bound? In your ActivityModule when you wrote the @ContributesAndroidInjector for MainActivity.

How to fix this is getting beyond scope of a StackOverflow question because it involves a lengthy refactor of the code. You would need to re-organise the object graph so that the NavigationController no longer depends on MainActivity. Perhaps you could make it depend on AppCompatActivity since that is a superclass of both your Activities. You would then need to stop using ContributesAndroidInjector and write explicit modules for your two Activities that include bindings for AppCompatActivity.

However, for now please go back to basics and start with something easier. It is a recipe for disaster to start with a complex project without a complete understanding and just modify it hoping that it will work.

The Codepath Dagger 2 tutorial project is much easier to understand and will get you familiar with the basic concepts involved in Dagger 2. Once you are comfortable with the basic concepts and have understood dependent components and sub-components, then you can attempt a more difficult example. Good luck!

like image 123
David Rawson Avatar answered Oct 23 '22 19:10

David Rawson