5

I'm trying to add the new Architecture Components ViewModel to my application while injecting them with dagger. I based my code on what google showed here. I'm trying to avoid having a ViewModelFactory for each ViewModel type, so I used the ViewModelFactory that receives Map<Class<? extends ViewModel>, Provider<ViewModel>> creators. It works for ViewModels that have dependencies with @Singleton scope. However, one of my ViewModels has a dependency that comes from the fragment. This is the module of that fragment:

@Module
public abstract class DownloadIssueDialogFragmentModule {

    @Binds
    abstract DialogFragment dialogFragment(DownloadIssueDialogFragment dialogFragment);

    @Provides
    @FragmentScope
    static Issue provideIssue(DownloadIssueDialogFragment dialogFragment) {
        return dialogFragment.getIssue();
    }
}

And my ViewModelModule:

@Module
public abstract class ViewModelModule {

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

    @Binds
    @IntoMap
    @ViewModelKey(DownloadIssueViewModel.class)
    abstract ViewModel bindDownloadIssueViewModel(DownloadIssueViewModel viewModel);

}

dagger says it can't provide Issue. It makes sense since Map<Class<? extends ViewModel>, Provider<ViewModel>> seems to be created at compile time. But I will only know the parameter during the scope of that fragment. How can I achieve this?

Thank you.

EDIT:

In the end I went with a different approach. Now I create a factory for each ViewModel and instead of injecting ViewModels, I inject the factory.

I created this library: AutoViewModelFactory

To automatically generate the factories. It's the best solution I've found so far.

  • You can not use `@Binds` and `@Provides` together in same class. You should use one of them. Just fyi. – mertsimsek Sep 09 '17 at 10:20
  • 1
    @mertsimsek You can have a `@Binds` and `@Provides` if the provides are static. I had this working with my own `ViewModel`s before trying to change to the Google ones (I needed the `ViewModel`s retained on rotation). – Ricardo Carrapiço Sep 09 '17 at 18:02

1 Answers1

2

Since Android Architecture Component ViewModels have a greater scope (read more persistent lifecycle) than Fragments you should avoid making the ViewModel depend on the field from the Fragment.

However, if the Issue is only known at runtime and is generated by logic in the Fragment you may be able to escape the small dependency cycle issue by using the Holder pattern.

This has been discussed in some other Dagger 2 StackOverflow questions but you would simply define a Java bean with public accessors/mutators:

class IssueHolder {
    private Issue issue;

    @Inject
    IssueHolder() {} //empty explicit constructor as required by Dagger 2         

    public void setIssue(@Nullable Issue issue) { 
        this.issue = issue;
    }

    @Nullable
    public Issue getIssue() {
        return issue;
    }
}

Then you can make your ViewHolder depend on IssueHolder rather than directly on Issue:

@Inject IssueHolder issueHolder;

public void doSomething() {
    if (issueHolder.get() == null) {
        throw new IllegalStateException("Expected IssueHolder to be set by IssueFragment at this point");
    }
    //TODO: the logic you want here
}

As like any pattern, this Holder pattern should be used sparingly as it can easily degenerate. The best solution is, if possible, to design your modules and dependencies in such a way so as to eliminate the possibility of cycles,

David Rawson
  • 20,912
  • 7
  • 88
  • 124
  • Thank you for the answer. I think this solution ends up being the same as, from the fragment, setting the object directly on the ViewModel, after obtaining it. This screen it's like a detail, I need the `Issue` object, and it comes from the fragment (passed in its arguments). I know when the ViewModel is created/retrieved and conceptually, I think the dependencies are correct, I just have to no way to tell dagger how to get the object. – Ricardo Carrapiço Sep 11 '17 at 09:37
  • @RicardoCarrapiço thanks for the upvote. I don't know if it is exactly the same as setting directly on the ViewModel because you can control which consumers of the ViewModel can set through controlling access to the Holder. – David Rawson Sep 11 '17 at 09:49