1

This is how I create an Adapter with MVVM (+Databinding) and Dagger-2.11-rc2:

Adapter:

public class ItemAdapter extends RecyclerView.Adapter<BindableViewHolder<ViewDataBinding>>{
    private static int TYPE_A = 0;
    private static int TYPE_B = 1;

    ...

    @Override
    public BindableViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_A) {
            return new ItemViewHolder(new ItemRowView(parent.getContext()).getBinding());
        }
        ...
    }

    @Override
    public void onBindViewHolder(BindableViewHolder holder, int position) {
        if (holder.getItemViewType() == TYPE_A) {
            ((ItemViewHolderBinding) holder.getBinding()).getViewModel().setItemModel(((ItemModel) getItem(position)));
        }        
        ...
    }

    private static class ItemViewHolder extends BindableViewHolder<ItemViewHolderBinding> {
        ItemViewHolder(ItemViewHolderBinding binding) {
            super(binding);
        }
    }
}

BindableViewHolder:

public abstract class BindableViewHolder<ViewBinding extends ViewDataBinding> extends RecyclerView.ViewHolder {

    private ViewBinding mBinding;

    public BindableViewHolder(ViewBinding binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public ViewBinding getBinding(){
        return mBinding;
    }
}

Since I'm using Dagger I wont be creating the ViewModels inside the Adapter instead they will be created (injected) inside their respective Android.View. And I guess it makes sense because my Adapter may have X Android.View types, those views can have Y ViewModel, etc...

BaseView:

public abstract class BaseView<ViewBinding extends ViewDataBinding, ViewModel extends BaseViewModel> extends FrameLayout {

    @Inject
    ViewModel mViewModel;
    protected ViewBinding mBinding;

    protected abstract void initBinding(final ViewBinding binding, final ViewModel viewModel);

    ...

    private void initView(Context context) {
        ViewInjection.inject(this);

        mBinding = DataBindingUtil...
        initBinding(mBinding, mViewModel);
        ...
    }
    ...
}

BaseViewModel:

public class BaseViewModel extends BaseObservable {...}

ItemRowView (or any View):

public class ItemRowView extends BaseView<ItemRowViewBinding, ItemRowViewModel> {

    @Inject
    ViewModelA mViewModelA;
    @Inject
    ViewModelB mViewModelB;
    ...

    @Override
    protected void initBinding(ItemRowViewBinding binding, ItemRowViewModel viewModel) {
        binding.setViewModel(viewModel);
        binding.setViewModelA(mViewModelA);
        binding.setViewModelB(mViewModelB);
        ...
    }
}

Now, this approach works fine with Activities, Fragments, etc, but when I use Views I have to create a ViewInjecton because Dagger doesn't have it out of the box. This is how I do it (read until you've reached "ViewInjection is pretty much a copy from other Injectors.")

My question(s) is(are): Is this a good approach? I'm I using MVVM and Dagger correctly? Is there any better way to achieve this without creating ViewInjecton (and using Dagger-2.11)?

Thanks for your time.

ps: I've used the Adapter example but this approach is the same if I want to use Views instead of Fragments. With Adapters you are restricted to Views.

GuilhE
  • 11,591
  • 16
  • 75
  • 116

2 Answers2

2

There has already been some discussion about whether one should inject inside Views or not in this question.

Since I'm using Dagger I wont be creating the ViewModels inside the Adapter instead they will be created (injected) inside their respective Android.View. And I guess it makes sense because my Adapter may have X Android.View types, those views can have Y ViewModel, etc...

I personally find this a little problematic and if I was working on a team with that code I would prefer a greater degree of separation between layers. At least,

  1. There should be a clear model layer (that is retrieved from a repository or from the cloud for instance). These should be mere data objects.
  2. The Adapter can deal with the model layer directly if it is easily related to the "item" layer i.e., the contents of the backing List for the RecyclerView.
  3. The ViewModel for the RecyclerView.ViewHolder should be extremely lightweight and not need injection. It should essentially be a bag of properties that easily translate into some property of the view (e.g., setText(), setColor()) and can be get/set. These can be created using the new keyword inside the onBindViewHolder method in the adapter. If this is difficult, you could extract a Factory (ViewModelFactory) and inject that as a dependency for your Adapter.

In short, the model data objects should be "dumb". The same goes for the ViewModel for the individual ViewHolder. The Adapter can be "intelligent" and can take tested "intelligent" dependencies (such as a ViewModelFactory if necessary) and this Adapter can be injected into your Activity or Fragment using Dagger 2.

David Rawson
  • 20,912
  • 7
  • 88
  • 124
  • Thanks for your answer. It's quite interesting your ViewModelFactory suggestion. So if you have an Activity's layout composed with 2 custom Views (could be included or not, I guess its the same) and each custom View has its own ViewModel. The Activity should "provide" (new.../Factory) those ViewModels to his custom Views? Bottom line, Activities/Fragments can have their ViewModels injected, but Android.Views should have them "provided"? – GuilhE Aug 05 '17 at 11:05
  • 1
    @GuilhE a View is a UI widget for rendering on the screen. It is nothing more. Activity and Fragment are more like co-ordinators and so it is permissible to inject dependencies for them. It's also possible to inject the Adapter for a RecyclerView and this would seem like a good place to inject a ViewModelFactory if you do need such a thing. In short, I would make the Views and their ViewModels as "stupid" as possible and add more "intelligence" at the level of the Adapter or the Activity/Fragment – David Rawson Aug 05 '17 at 11:08
  • Got it. I've marked your answer as "accepted" because I think this is the way to go too. Thanks again. – GuilhE Aug 05 '17 at 11:14
0

While I agree with David's answer that this should not be done, if you still want to do this, it is possible by going through the activity:

override val activity: FragmentActivity by lazy {
    try {
        context as FragmentActivity
    } catch (exception: ClassCastException) {
        throw ClassCastException("Please ensure that the provided Context is a valid FragmentActivity")
    }
}
override var viewModel = ViewModelProviders.of(activity).get(SharedViewModel::class.java)

This is discussed in more detail here.

Sapan Diwakar
  • 10,480
  • 6
  • 33
  • 43