Dependency Injection in Android
The last native mobile dev tutorial I wrote on this blog dates to months ago, and it was an hybrid-native work/tutorial ! I am back to post here weekly. My easy go-to option is native mobile dev tutorials, though it’s not my current day-to-day technology ! So please help me brainstorming and do leave in comments or email suggestions for tutorials or posts you need me to write on. Meanwhile, let’s make the Dependency Injection topic more obvious for junior developers through this back-to-blog post !
Dependency Injection – The What:
One SOLID Concept behind
We will explain DI here sort of “backwards”. Let’s say for now that it’s a programming technique that provides the following:
- Code reusability
- Better code refactoring
- Better testing
In an object oriented context, objects need references to other objects. Let’s take the example of a Republic class needing reference to Constitution class. This can technically happen in 3 different ways:
- The Republic class constructs (creates and initializes) its own dependency (instance of Constitution).
- The Republic class gets its Constitution from an external source like an API.
- The Republic class gets its dependency supplied as a parameter. The app provides a Constitution dependency when the Republic class is created, or, as a parameter of one of its methods. This approach is dependency injection!
==> Providing the dependencies of a class rather than making the class create them itself is the simple definition of DI.
Keep this simplified definition of DI in mind until we see it in code example in the last section, and let’s pass – all in keeping things simple – to the different types of DI – providing dependencies – existing :
- Setter injection: The class needing a dependency – Republic class in our example – provides a setter method for the dependency.
public void setConstitution(Constitution aConstitutionDependency) {
// Save the reference to the passed-in dependency inside this class
this.aConstitutionDependency = aConstitutionDependency;
}
- Constructor Injection: The class needing a dependency – Republic class in our example – provides a parameter for the dependency in the constructor.
Republic(Constitution aConstitutionDependency) {
// Save the reference to the passed-in dependency inside this class
this.aConstitutionDependency = aConstitutionDependency;
}
- Interface Injection: The dependency class – Constitution class in our example – publishes a role interface to the setter methods of the class needing a dependency – Republic class – .
// Dependency setter interface public interface ConstitutionSetter { public void setConstitution(Constitution constitution); } // "Client class" public class Republic implements ConstitutionSetter { // Internal reference to the dependency used by this " client " class private Constitution constitution; // Set the "service" that this client is going to use. @Override public void setConstitution(Constitution constitution) { this.constitution = constitution; } }
We are good on The What of DI as we now understand the “concept” of DI, its types, and what does it provide. But we still have to get and keep in mind that DI is one implementation of the SOLID concept of Dependency Inversion . You can not be at this point without being familiar with the object oriented SOLID design principles.
The Dependency Inversion Principle simply states that :
-
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
DI frameworks and manual DI code are implementation of the decoupling stated in this 5th S.O.L.I.D principle.
DI – The Why: Android apps architecture
General rules for best Android app architecture states writing different classes to benefit from separation of concerns, a principle where each class of the hierarchy has a single defined responsibility. By implementing DI you ensure your code applies to the separation of concerns point. Equally, DI sets the ground for good architecture practices on a basic level, all along to the Clean Architecture design pattern and more.
There are several software design techniques we can opt to build scalabe, testable and high quality Android apps, and it all turns back to properly setting up the base architecture of our application through using patterns like clean architecture, app modularization, MVVM, and others.
DI is a cornerstone technique for each of these patterns, it allows writing clean architecture Android apps, which is what we’ll learn and practice in next tutorials. Below are bullet points on DI and Android app architecture for you to keep in mind before moving to next section and tutorials :
- DI allows implementing the Clean Architecture pattern for Android.
- DI allows implementing Modularization in Android projects.
- DI reduces boilerplate code and helps writing clean code for Android projects.
- DI contributes to following best Android app architecture practices and rules.
DI – The How: Manual DI to DI tools in Android
In the previous section we explained DI and presented 3 different methods to implement it. If you are still uncomfortable with DI concept or implementation, you can practice and see more Java samples on the 3 different types of DI before starting this section. In this section, we apply manual DI in a real Android app scenario.
Let’s say we are implementing an Election-Voter operation into our app (voter registration for example). Our flow is the follwoing :
What a developer would do without DI practice in mind is to make the entry pointy to the flow activity, which is ElectionsActivity, create its dependecy – ElectionsViewModel – and all its dependencies, as shown below :
public class ElectionsActivity extends Activity { private ElectionsViewModel electionsViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_elections); // In order to satisfy the dependencies of ElectionsViewModel, we have to also // satisfy the dependencies of all of its dependencies. // 1. satisfy the dependencies of VoterRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://getvoter.com") .build() .create(ElectionsService.class); // 2. satisfy the dependencies of VoterRepository VoterRemoteDataSource remoteDataSource = new VoterRemoteDataSource(retrofit); VoterLocalDataSource localDataSource = new VoterLocalDataSource(); // 3. an instance of VoterRepository that ElectionsViewModel needs VoterRepository voterRepository = new VoterRepository(localDataSource, remoteDataSource); // 4. an instance of VoterViewModel with voterRepository voterViewModel = new VoterViewModel(voterRepository); } }
This simple example demoes what we explained in the previous section about what happens when we dont use DI:
- Alot of boilerplate code. Creating another instance of VoterViewModel will require duplicating all instances of the previous commented steps.
- Reusing objects is difficult. If we’ll need to re-use VoterRepository across different features, we’ll have to make it follow the singleton pattern, which will make testing complicated .
To fix these issues, we can implement Manual DI, by creating what we call a dependencies container. A dependencies container is a class which we use to get all dependencies as follows :
public class AppContainer { // we need need to satisfy the VoterRepository's dependencies only once // before exposing it out of the container private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://getvoter.com") .build() .create(ElectionsService.class); private VoterRemoteDataSource remoteDataSource = new VoterRemoteDataSource(retrofit); private VoterLocalDataSource localDataSource = new VoterLocalDataSource(); // exposing voterRepository public VoterRepository voterRepository = new VotererRepository(localDataSource, remoteDataSource); }
Once our dependencies container – here AppContainer class – is ready, we’ll instanciate in a place where all activities can access and use it. In our case, we’ll create a custom Application class and declare it in Manifest.xml file:
public class MyApplication extends Application {
// instance of AppContainer that will be used by all the Activities of the app
public AppContainer appContainer = new AppContainer();
}
At this point, we can get the shared instance of VoterRepository without having to duplicate code:
public class MainActivity extends Activity {
private ElectionsViewModel electionsViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// we get voterRepository from the instance of AppContainer in Application
AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
voterViewModel = new VoterViewModel(appContainer.voterRepository);
}
}
}
Now let’s say we need to create a VoterViewModel in different places in the app ==> we simply add to our AppContainer and make the ElectionsActivity ( or other ) consume it through the container :
// AppContainer can now provide instances of VoterViewModel public class AppContainer { ... public VoterRepository voterRepository = new VoterRepository(localDataSource, remoteDataSource); public VoterViewModelFactory voterViewModelFactory = new VoterViewModelFactory(voterRepository); } public class MainActivity extends Activity { private VoterViewModel voterViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // we get voterViewModelFactory from the application instance of AppContainer // to create a new VoterViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; voterViewModel = appContainer.voterViewModelFactory.create(); } }
You might be wondering what’s this factory for, or it’s clear here that, to enable our container to provide voterViewModel dependencies – new objects of type voterViewModel – we added this factory:
// 1. a Factory interface with a function to create objects of a type public interface Factory { T create(); } // 2.Factory for voterViewModel // we need an instance of VoterRepository that you pass as a parameter. class VoterViewModelFactory implements Factory { private final VoterRepository voterRepository; public VoterViewModelFactory(VoterRepository voterRepository) { this.voterRepository = voterRepository; } @Override public VoterViewModel create() { return new VoterViewModel(voterRepository); } }
This is all we need to know for now to understand the concept of DI. This abstract simple example explained what manual DI in Android is, we can either go for a more concrete and complete example in text tutorial, or skip manual DI to explore DI frameworks.
There are different frameworks and tools for DI in Android, such as Dagger, Koin, and Kodein. There are tons of comparatives on these frameworks as well. We will have a tutorial on using each of them in coming posts.
Thant’s all coders !
After understanding what DI is, why we need it in general and why we need it for Android projects, we worked on an example of manual implementation of DI in Android. Now it’s time to go for a good Android DI tutorial on how to use of the most popular DI frameworks : Dagger.
Recent Comments