As apps become large, it often makes sense to build parts of the application as reusable components or modules. For large companies, either with several apps or several mobile teams, reusing the code owned by another team becomes a no-brainer.
For example, a mobile platform team might own networking and shared architecture components. A Money team would build and own payments components — this was my team at Uber! — and a Maps team would own all things mapping-related. Components and modules in the app would often map to the structure of the company's teams, mirroring the observation known as Conway's law.
With multiple modules, modules need to have a way to define their dependencies, both at a module and a class level. This concept is dependency injection, a form of inversion of control. It is a simple, yet often underrated concept in mobile development and one that is far more commonplace with backend and web projects. Here is a Swift code sample and explanation of the concept.
A major challenge with dependency injection is the amount of work it takes to modify or update dependencies if you do not use a framework to solve this. Even without using a framework, the time-consuming nature of this is a trade-off for clearer abstractions and good testability.
Dependency injection is a powerful tool for maintaining testable code consistently across the codebase. With dependency injection, classes that have multiple dependencies can be unit-tested by passing mock dependency classes when instantiating them. Larger, modular apps tend to introduce this concept one way or the other.
Manual dependency injection — creating all interfaces, then hardcoding all dependencies — works fine when there are few such dependencies. However, as the number of components and the number of dependencies grow, maintaining and updating dependencies becomes more difficult. Spotting things such as circular dependencies also becomes tricky and using dependency injection frameworks begins to make more sense.
Android has a mature dependency injection framework that analyzes dependencies’ compile-time, called Dagger2. Google recently introduced Hilt on Dagger; a dependency framework built on top of Dagger, and Koin is becoming more popular with Kotlin.
On iOS, there have historically not been similar frameworks. At Uber, we built, used, and open-sourced Needle, based on similar concepts. We would have had trouble scaling the code with over a hundred engineers working on the same codebase without dependency injection. We used this tool to be explicit about all class dependencies, make unit testing easy — and non-negotiable for most of the code — and reuse components across teams.
This post is an excerpt from my book Building Mobile Apps at Scale: 39 Engineering Challenges. Get the book here as a free PDF, until 31 May.