androidandroid-viewmodeldagger

Dagger cycle dependency issue for ViewModelFactory and AndroidInjection


I want to understand why I have dagger cycle dependency here:

class MachineFragment : Fragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    private lateinit var viewModel: MachinesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidSupportInjection.inject(this)
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        viewModel = ViewModelProvider(this, viewModelFactory).get(MachinesViewModel::class.java)
        return inflater.inflate(R.layout.fragment_machines, container, false)
    }
@Singleton
@Component(
    dependencies = [],
    modules = [
        AppModule::class,
        TestViewModelModule::class,
        InjectorModule::class
    ]
)
interface AppComponent {

    fun inject(subject: AppApplication)

public abstract class TestViewModelModule {

    @Binds
    public abstract ViewModelFactory bindViewModelFactory(ViewModelFactory viewModelFactory);

    /*
        you have to explicitly define ViewModel instead of concrete
        implementation to avoid cycle dependency error
    */
    @Binds
    @IntoMap
    @ViewModelKey(MachinesViewModel.class)
    abstract ViewModel bindMachinesViewModel(MachinesViewModel vm);
}
public class ViewModelFactory implements ViewModelProvider.Factory {

    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> viewModels;

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

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        Provider<ViewModel> viewModelProvider = viewModels.get(modelClass);

        if (viewModelProvider == null) {
            throw new IllegalArgumentException("model class " + modelClass + " not found");
        }

        return (T) viewModelProvider.get();
    }

}

However when I define the same injection field in MainActivity, cycle dependency disappear:

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    private lateinit var viewModel: MachinesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /* start destination in navigation.xml defines entry point */

        viewModel = ViewModelProvider(this, viewModelFactory).get(MachinesViewModel::class.java)
    }

I already have solved this issue by adding @Named("machine-vm-factory") annotation, but the root cause is unclear for me. I will appreciate your thoughts on this. Thank you!


Solution

  • I think the problem is this line in TestViewModelModule:

    @Binds
    public abstract ViewModelFactory bindViewModelFactory(ViewModelFactory viewModelFactory);
    

    Here you're asking Dagger to give you a ViewModelFactory whenever anyone requests a ViewModelFactory, and since @Binds has a higher priority than @Inject, Dagger doesn't really know how to construct a ViewModelFactory, hence the cycle.

    Adding qualifier changes the key:

    @Binds
    @Named("machine-vm-factory")
    public abstract ViewModelFactory bindViewModelFactory(ViewModelFactory viewModelFactory);
    

    This means that when anyone is asking for a @Named("machine-vm-factory") ViewModelFactory, Dagger will give us a ViewModelFactory using the @Inject annotated constructor, so the cycle is gone.

    See https://dagger.dev/semantics/#keys