28

Dependency Injection of ViewModel with Dagger 2

 5 years ago
source link: https://www.tuicool.com/articles/hit/AVni6by
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ViewModel is the officially recommended component for implementation of presentation layer in Android applications and Dagger 2 is the most popular dependency injection library in Android world. I would expect these tools to integrate seamlessly, but, unfortunately, it’s not the case.

In this article I’ll explain how to inject ViewModel instances with Dagger and show several alternative approaches that you can choose from. In addition, we are going to review one very serious mistake caused by violation of Liskov Substitution Principle that I myself made in this context.

ViewModel Without External Dependencies

Let’s say that you use ViewModel to just store some data on configuration changes. In this case, you won’t need to pass any arguments into its constructor.

For example, consider this ViewModel:

public class MyViewModel extends ViewModel {

    private List<MyData> mData = new ArrayList<>();

    public List<MyData> getData() {
        return mData;
    }

    public void setData(List<MyData> data) {
        mData = data;
    }
}

To use MyViewModel in your Activities and Fragments, all you need to do is the following:

protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mMyViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
     }

As you can see, if your ViewModel doesn’t have external dependencies, then you don’t need to integrate it with Dagger at all. That’s the simplest case.

Now, even though this scenario is simple, it’s still perfectly valid use case for ViewModel classes. If all you want to achieve is to keep some data on configuration changes, this approach will suffice and there is really no need to complicate things.

ViewModel With External Dependencies

The official guidelines recommend using ViewModels to host actual presentation layer logic. In this case, if you’ll want to have clean code that adheres to Single Responsibility Principle, you’ll need to inject additional external dependencies into ViewModels.

For example, consider the following ViewModel. It uses FetchDataUseCase to fetch some data and then publishes the result using LiveData:

public class MyViewModel extends ViewModel {

    private final FetchDataUseCase mFetchDataUseCase;

    private final MutableLiveData<List<MyData>> mDataLive = new MutableLiveData<>();
    
    private final FetchDataUseCase.Listener mUseCaseListener = new FetchDataUseCase.Listener() {
        @Override
        public void onFetchDataSucceeded(List<MyData> data) {
            mDataLive.postValue(data);
        }

        @Override
        public void onFetchDataFailed() {
            mDataLive.postValue(null);
        }
    };

    public MyViewModel(FetchDataUseCase fetchDataUseCase) {
        mFetchDataUseCase = fetchDataUseCase;
    }

    public LiveData<List<MyData>> getDataLive() {
        mFetchDataUseCase.registerListener(mUseCaseListener);
        return mDataLive;
    }

    public void fetchData() {
        mFetchDataUseCase.fetchData();
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        mFetchDataUseCase.unregisterListener(mUseCaseListener);
    }
}

To instantiate this ViewModel, the system needs to pass it a reference to FetchDataUseCase object. Therefore, if you attempt to use the previously described approach with this ViewModel, you’ll get a runtime error.

Enter ViewModelProvider.Factory interface:

/**
     * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
     */
    public interface Factory {
        /**
         * Creates a new instance of the given {@code Class}.
         * <p>
         *
         * @param modelClass a {@code Class} whose instance is requested
         * @param <T>        The type parameter for the ViewModel.
         * @return a newly created ViewModel
         */
        @NonNull
        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }

This interface represents a standard parametrized abstract factory. Its create(Class ) method takes a class object of the required ViewModel type as an argument and returns a corresponding new instance.

To initialize ViewModels with non-default constructors, you’ll need to inject an object that implements ViewModelProvider.Factory interface into your Activities and Fragments, and then pass it to ViewModelProviders.of() method:

@Inject ViewModelFactory mViewModelFactory;

     private MyViewModel mMyViewModel;

     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mMyViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MyViewModel.class);
     }

So far it doesn’t look too complex, but I haven’t shown you the implementation of ViewModelFactory yet. That’s where things get more involved.

ViewModelFactory Instantiates ViewModels Directly

The first approach you can use is to simply instantiate ViewModels inside ViewModelFactory:

public class ViewModelFactory implements ViewModelProvider.Factory {

    private final FetchDataUseCase mFetchDataUseCase;

    @Inject
    public ViewModelFactory(FetchDataUseCase fetchDataUseCase) {
        mFetchDataUseCase = fetchDataUseCase;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        ViewModel viewModel;
        if (modelClass == MyViewModel.class) {
            viewModel = new MyViewModel(mFetchDataUseCase);
        }
        else {
            throw new RuntimeException("unsupported view model class: " + modelClass);
        }

        return (T) viewModel;
    }
}

This approach is the simplest one, but it has two drawbacks.

The first drawback is that ViewModelFactory will need to know about all the transitive dependencies of all the ViewModels. In this case there is just one transitive dependency, so it’s not that bad. However, if you’ll have 20 ViewModels in your codebase and each of them will have 2 external dependencies on average, ViewModelFactory will need to have 40 constructor arguments. It’s ugly and can become challenging to maintain in the long run.

One straightforward solution to this issue is to use several factories in different parts of your application. The other solution is to implement nested factories. This will address the maintainability concern, but will add even more boilerplate.

The other drawback is that you’ll have a big if-else if-else block because you’ll need to add one condition for each ViewModel type. In addition, you’ll need to manually instantiate ViewModels inside this block. So, even more boilerplate.

As for dependency injection and Dagger, there is nothing special in this case. The only thing to note is that if you choose to use this approach, then ViewModelFactory’s constructor will probably change quite often. Therefore, it’s a good idea to annotate it with @Inject and let Dagger provide its dependencies rather than instantiating it manually inside one of your Dagger modules.

ViewModelFactory Returns Pre-Constructed ViewModels (BAD!)

To reduce the amount of boilerplate, you can implement ViewModelFactory in the following way:

public class ViewModelFactory implements ViewModelProvider.Factory {

    private final MyViewModel mMyViewModel;

    @Inject
    public ViewModelFactory(MyViewModel myViewModel) {
        mMyViewModel = myViewModel;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        ViewModel viewModel;
        if (modelClass == MyViewModel.class) {
            viewModel = mMyViewModel;
        }
        else {
            throw new RuntimeException("unsupported view model class: " + modelClass);
        }

        return (T) viewModel;
    }
}

Now, instead of constructing ViewModels by itself, the factory receives them as constructor arguments. This way the factory isn’t exposed to transitive dependencies and you can annotate ViewModels’ constructors with @Inject to let Dagger take care of their instantiation.

Unfortunately, even though this approach indeed reduces the amount of boilerplate, there will still be quite a bit of it. For instance, if you’ll have 20 ViewModels in your application, then ViewModelFactory’s constructor will have 20 constructor arguments. It’s still quite ugly.

However, boilerplate isn’t the biggest problem with this approach.

Note that to get an instance of such ViewModelFactory, all ViewModels that it provides will need to be instantiated as well. So, if you’ll have 50 ViewModels with 100 transitive dependencies and you’ll want to get an instance of ViewModelFactory, you’ll need to construct 150+ objects. Every single time. Not good.

But even that isn’t the biggest problem. The main problem with this implementation that makes it absolute no-go is the fact that it violates Liskov Substitution Principle and can lead to serious bugs in your application.

Consider this part of the contract of create(Class) method in ViewModelProvider.Factory interface:

Creates a new instance of the given {@code Class}.

Can you see the problem?

The above implementation of ViewModelFactory doesn’t create a new instance. It will always return the same instance given the same argument. That’s really bad.

For example, imagine that one day you’ll want to use the same ViewModel class in Activity and Fragment. Furthermore, you’ll want to get Activity’s ViewModel from within the Fragment. Something like this:

@Inject ViewModelFactory mViewModelFactory;

     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mMyViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MyViewModel.class);
         mMyViewModelActivity = ViewModelProviders.of(requireActivity(), mViewModelFactory).get(MyViewModel.class);
     }

Naturally, you’d expect these ViewModels to be different objects. However, given this implementation of ViewModelFactory, it’s not guaranteed and the result will depend on whether Activity’s ViewModel had been accessed in the past in another place in the application. That’s a serious bug that can be extremely difficult to reproduce due to flow’s dependency on the previous history.

Therefore, you should never use this approach.

Liskov Substitution Principle

If you find it difficult to see why the above implementation is buggy, then it’s worth taking time to understand this point. I myself made a serious error and didn’t realize that it violates Liskov Substitution Principle initially. Therefore, I taught it as a viable approach in my course Dependency Injection in Android with Dagger 2 .

It was only months later, when I was working on the Liskov Substitution Principle section for my latest course SOLID Principles of Object-Oriented Design and Architecture , that I realized my mistake. Now I’m dreaded to think that somewhere in the world there is a developer who followed my tutorial and introduced a time bomb into his or her application.

This mistake is the reason why I write this post, by the way. I need to get my thoughts together before I update the course and fix it.

You can read more about Liskov Substitution Principle in this article .

Dagger’s Multi-Binding and Providers

To address the issue of boilerplate, you can use special Dagger’s convention called “multi-bindings”. It’s very complex convention, so I’ll describe it in steps.

You’ll start by defining a special annotation (I do it in the module class, but it’s not mandatory):

@Module
public class ViewModelModule {

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

}

ViewModelKey annotation, when used on provider methods, basically says that the services returned by these methods must be wrapped in a special Provider object and put into special Map. The keys in the aforementioned Map will be of type Classand the values will be of type Provider .

I’m pretty sure that this explanation sounds absolutely gibberish at this point. Hopefully, it will become clearer after you see the entire picture, so keep reading.

Once I have ViewModelKey annotation, I can add provider method for a specific ViewModel in the following manner:

@Module
public class ViewModelModule {

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

    @Provides
    @IntoMap
    @ViewModelKey(MyViewModel1.class)
    ViewModel viewModel1(FetchDataUseCase1 fetchDataUseCase1) {
        return new MyViewModel1(fetchDataUseCase1);
    }
}

Note that the return type of the provider method is ViewModel and not ViewModel1. It’s intentional. @IntoMap annotation says that Provider object for this service will be inserted into Map, and @ViewModelKey annotation specifies under which key it will reside. Still gibberish, I know.

The net result of having the above code is that Dagger will create implicit Map data structure filled with special Provider objects and add it to the objects graph where it can be used. You make use of that Map by passing it into ViewModelFactory in the following manner:

@Module
public class ViewModelModule {

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

    @Provides
    ViewModelFactory viewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> providerMap) {
        return new ViewModelFactory(providerMap);
    }

    @Provides
    @IntoMap
    @ViewModelKey(MyViewModel1.class)
    ViewModel viewModel1(FetchDataUseCase1 fetchDataUseCase1) {
        return new MyViewModel1(fetchDataUseCase1);
    }
}

So, in essence, all this magic was required to allow Dagger to create this Map that you pass into ViewModelFactory. The implementation of the factory then becomes:

public class ViewModelFactory implements ViewModelProvider.Factory {

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

    @Inject
    public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> providerMap) {
        mProviderMap = providerMap;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        return (T) mProviderMap.get(modelClass).get();
    }
}

As you can see, the factory becomes relatively simple. It retrieves the required Provider object from the Map , calls its get() method, casts the obtained reference to the required type and returns it. The nice thing about it is that this code is independent of the actual types of ViewModels in your application and you don’t need to change anything here when you add new ViewModel classes.

Now, to add new ViewModel, you’ll simply add the respective provider method:

@Module
public class ViewModelModule {

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

    @Provides
    ViewModelFactory viewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> providerMap) {
        return new ViewModelFactory(providerMap);
    }

    @Provides
    @IntoMap
    @ViewModelKey(MyViewModel1.class)
    ViewModel viewModel1(FetchDataUseCase1 fetchDataUseCase1) {
        return new MyViewModel1(fetchDataUseCase1);
    }

    @Provides
    @IntoMap
    @ViewModelKey(MyViewModel2.class)
    ViewModel viewModel2(FetchDataUseCase2 fetchDataUseCase2) {
        return new MyViewModel2(fetchDataUseCase2);
    }
}

Dagger will put Provider for ViewModel2 into the Map and ViewModelFactory will be able to use it automatically.

The benefit of this method is clear: it reduces the amount of boilerplate that you need to write. However, there is also one major drawback: it greatly increases the complexity of the code. Even experienced developers find this multi-bindings stuff tricky to understand and debug. For less experienced ones, it might be absolute hell.

Therefore, I wouldn’t recommend using this approach on projects with many developers, or if staff turnover rate is high. On such projects, the effort of learning Dagger’s multi-bindings might very well be higher than the effort required to implement the very first approach described in this article. On small or personal projects, on the other hand, this approach might be alright.

Why ViewModel Needs Special Approach to be Used With Dagger

One very interesting question to ask in context of ViewModel is: why it requires a special approach which is even more complicated than the “standard Dagger” (which isn’t simple to begin with)?

Well, consider this code that you’d use in either Activity or Fragment:

@Inject ViewModelFactory mViewModelFactory;

     private MyViewModel mMyViewModel;

     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mMyViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MyViewModel.class);
     }

Note that what you need in this Activity or Fragment is MyViewModel, but what’s being injected is ViewModelFactory.

Factories are very useful constructs when you need to instantiate objects with runtime dependencies, or when you need to instantiate many objects at runtime, or when the specific type of object that you’ll need isn’t known at compile time. Nothing of that applies in this case, however. You need one instance of ViewModel and you know its type and its dependencies at compile time. Therefore, usage of abstract factory pattern is unjustified in this case.

Unnecessary dependencies like ViewModelFactory violate fundamental principle of object-oriented design called Law of Demeter. Usually you can refactor the code to fix LoD violations, but, in this case, it’s violated by ViewModel framework itself. Therefore, you can’t work around that.

It’s the violation of Law of Demeter that causes this excessive complexity with respect to dependency injection. For example, in multi-bindings approach, you have your ViewModels on object graph already, but, instead of simply injecting them directly, you are forced to use this ugly and excessively complex hack.

The reason I bring this topic up is to show you that it’s important to know the rules of object-oriented design and apply them in practice. Especially if you design APIs for millions of other developers to use.

Conclusion

In this post I demonstrated several alternative approaches to integrate ViewModel with Dagger. I remind you that one of them violates Liskov Substitution Principle, and, as such, should never be used.

When it comes to ViewModel, there is no “clean” way of doing things. Therefore, you need to take into account your situation and make an educated trade-off. In essence, you need to choose the lesser of the evils.

To make it absolutely clear for long-term readers of my blog: the fact that I write about ViewModel doesn’t mean that I encourage you to use it. My opinion about this framework hasn’t improved . If anything, it became even more negative. I don’t use ViewModel in my projects, and I don’t recommend you to use it either. I wrote this post because I needed to get my thoughts together before I go and fix the mistake I made in my course, and, partly, because many developers search for this information.

As usual, you can leave your comments and questions below.

Check out my top-rated Android courses on Udemy


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK