Introduction to Dependency Injection & its popular Android Frameworks
When laying out the blueprint for a new mobile project, an architect needs to consider a variety of technologies, frameworks, & design patterns to be used. Amongst the various architectural decisions needing to be made, one may need to form a dependency injection approach. As this endeavor may be time consuming especially for the unacquainted with dependency injection, we are first going to dive into what is dependency injection (here on out denoted as DI). Then explore how it benefits the average project. We're also going to tap into the top 3 most popular frameworks for Android, and lay out simply which best suits your next project.
The term "dependency injection" may have been floated around the coding veteran, however never come across in day-to-day projects. The reason being the term was first introduced by Martin Fowler in 2004 in an article titled "Inversion of Control Containers and the Dependency Injection pattern", where he laid out practical means to address the Inversion of Control design pattern. The unacquainted may announce: "One can do without DI, as many large projects have done so before." What's not apparent is how DI is commonly used unknowingly by developers, however in an un-automated manner. Take for example the following example:
class Car {
private Engine engine = new Engine();
public void start() {
}
}
Class Car depends on class Engine, right? It can't start without it. Notice however how limiting Engine construction is in Car; it only has one mean for its creation for all Cars ever created. Imagine a developer wanting to customize the Engine a Car uses. Instead of having the Car create the Engine object itself, the developer simply allows the passing of an Engine object to Car, such that one may pass an Engine configuration of their liking:
class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
With such ease, did we accomplish DI, albeit manually. By passing dependencies, we're manually "injecting" them. While having accomplished the allowance of Engine object tailoring when creating a Car, we've too introduced other benefits:
- Future subtypes of Engine may now be passed to Car.
- Mock Engine objects may now be passed to Car, which eases testing, as dependencies & their construction may not always be straight forward to accomplish in unit testing environments.
- We may now share an Engine object amongst many Cars, instead of forcing a 1:1 relationship.
In the above example we've passed this Engine the dependency via the constructor, however could have instead passed it through a setter method, which collectively covers the two possible ways in doing DI manually.
While it may look like a case closed when it comes to resolving dependencies, consider the implications should we have multiple layers of dependency. Say we introduce a dependency for Engine, like class Cylinder. While at it, add another set of dependencies for Car like: List<Wheels>, RearMirror, Swiper, etc. The boilerplate code added along with its maintenance, as you could imagine, would quickly get of hand.
A skeptic veteran by now may be saying: "I see where this is going. Since we're talking about mobile projects, you're going to inject in services of sorts, be it database or network layer related. Why not just create singletons for these services like we always do?"
class DatabaseService {
private static DatabaseService instance = null;
private DatabaseService() {}
public synchronized static DatabaseService getInstance() {
if (instance == null) {
instance = new DatabaseService();
}
return instance;
}
public UserPreference getUserPreference() {
// db logic ...
}
}
class SettingsActivity extends AppCompatActivity {
DatabaseService dbService = DatabaseService.getInstance();
@Override
public void onCreate() {
super.onCreate();
UserPreference currentUserPreference =
dbService.getUserPreference();
// bind view against currentUserPreference object
}
}
While this approach eliminates the boilerplate code problem mentioned earlier, notice we're now back to square one; the dependent class, SettingsActivity, is specifying what exactly it depends on, a DatabaseService. This is very similar to how the Car class created its own Engine object, which as we showed brings about its set of problems. This approach is actually Service Locator pattern, not true DI. More on Service Locator Pattern later.
Enter Automatic DI
Before jumping into an automatic DI framework that saves the day, let's discuss a couple of concepts that most DI frameworks use: Modules & Components. Think first of the typical services an Android project uses. From a networking perspective: Retrofit for networking, OkHttp for Http client, and GSON for a JSON parser. From a data persistence perspective, Room as a database for example. In designing a feature that uses both groups of services, instead forcing the developer in having to think of all of what's entailed within each group, a developer may instead create modules that groups these respective related services. In this example, it'd make sense to create two modules: a network and database module. The developer therefore will more simply declare what a feature depends on in choosing one or both modules. Some frameworks will take it a step further and not allow you access modules immediately, instead enforce a middleman, a Component, whose role is to provide a recipe of modules. So instead of having to think of what modules your feature needs, you think of what recipes, or sets of modules, your feature will depend on and specify it accordingly.
Let's now jump into an automatic DI framework, Koin, which is the lightest & most simple out of the bunch today. Written in Kotlin; it requires no code generation, nor does it use reflection. To do our first Car example using Koin, you'd do the following:
// floating in global namespace
val servicesModule = module {
single<Engine> {
Engine()
}
}
class AndroidApplication: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@AndroidApplication)
modules(servicesModule)
}
}
}
class Car {
val engine by inject<EngineService>()
fun start() {
engine.start()
}
}
In using `single` the above servicesModule, Cars would use the same exact Engine. To have Cars use a unique Engine, you'd simple change it to `factory`:
val servicesModule = module {
factory<Engine> {
Engine()
}
}
This subtle ability is a powerful one, as what you inject as a dependency value may then rely on the state of your application. Think for example updating your network service layer per the current signed on user.
Notice also Koin did not require us to create a Component, instead is optional in usage.
Lastly, notice the way we injected the dependency:
val engine by inject<EngineService>()
This was actually a call to fetch the value of engine, making the framework a Service Locator more than a true Dependency Injector. We still have the same benefits of truly injecting however like removing that boilerplate code in constructors or setters.
Dagger2 is a Google adoption of Dagger by Square. It's a static, compile-time (non-reflective) implementation, however code generative DI framework. Unlike Koin, requires Components defined, furthermore adds build time due to code generation. The upside of this is that you'll never run into runtime DI issues, unlike Koin when misconfigured, instead find them at compile time. Let's again do our car example:
@Module
internal class EngineModule {
@Provides
@Singleton
class Engine @Inject constructor() {
}
}
@Component(modules = [EngineModule::class])
@Singleton
internal interface ActivityComponent {
fun inject(homeActivity: HomeActivity)
}
class AndroidApplication: Application() {
override fun onCreate() {
super.onCreate()
val myActivityComponent = DaggerActivityComponent
.builder()
.build()
myActivityComponent(this)
}
}
class CarActivity : AppCompatActivity() {
@Inject
lateinit var engine: Engine
private val activityComponent: ActivityComponent by lazy {
DaggerActivityComponent
.builder()
.build()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
}
}
Because Dagger2 uses code auto generation, the engine variable is automatically placed. While automatic placement sounds great, there are some downsides:
- lateinit is needed, which is a way for Kotlin to tell the compiler: "don't worry this is non-nullable & will provide its value later."
- In using lateinit, we force the type of the variable be mutatble and not a constant, unlike how we're able to do so with Koin.
- The variable has to be public, as it has to be accessed directly by external classes.
Another note worthy mention of Dagger2 is its sophistication with setup. You need Components planned out as well as their scoping. Because Dagger2 allows for great configurability, it brings forth great complexity. For instance one may have Components depend on other Components who depend on Subcomponents that depend on Modules. While there are use cases for such complex configurations, the average application doesn't need them.
Hilt is the newest kid on the block, and in English is a noun that describes a handle of a weapon, which in this case is a Dagger. Hilt too is maintained by Google, and is a wrapper for Dagger2 that's meant to simplify its usage. As of writing this article is in its Alpha stages of development. It brings forth syntatic sugaring to Dagger2 via annotations as well as out of the box Components that are meant to be widely used in an application such as: ActivityComponent, FragmentComponent, ViewComponent & more.
@HiltAndroidApp
class AndroidApplication: MultiDexApplication() {
}
@Module
@InstallIn(ActivityComponent::class)
internal class EngineModule {
@Provides
@Singleton
class Engine @Inject constructor() {
}
}
@AndroidEntryPoint
class CarActivity : AppCompatActivity() {
@Inject
lateinit engine: Engine
}
Beware of adding Module dependencies to out of box Components like ActivityComponents. Hilt advocates usages of their monolithic out of box Components. What may not be obvious, especially in a multiple App Project Module (not DI modules), when using these components and adding module dependencies to them, they're now shared across the entire app! This becomes problematic when you want Activities not over sharing their dependencies with others. There are ways about avoiding this in creating your very own Components, however when doing so end up with the exact same syntax & configuration work as needed as with Dagger2. See here for more details.
Recommendation:
My recommendation is if you're a beginner to DI, definitely start with Koin. If you're running a smaller to medium sized application then too start with Koin until you need the sophistication of Dagger2. While Hilt has good intentions, I think their simplification of out of box components come at risk of misusage to the unaware developer. Furthermore, Hilt is in its alpha stages, making its API subject to change. Don't forget its a wrapper for Dagger2, meaning Dagger2 is not leaving the DI block anytime soon.
That was a lot to cover in a single go, and believe it or not, there’s a ton more to DI! I hope you’ve for now learned what DI is, its benefits, the most popular Android frameworks that help in its automation, and finally how to choose the right one for your next project. Until the next time, happy coding!