Dependency Injection in iOS and Android Apps

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.

Without dependency injection: a Trip class would create their own Payments and Map dependencies. However, this makes unit testing the Trip class impossible without instantiating those two classes (and all their own dependencies). This makes tests slow, and it might make it very difficult to test scenarios that depend on Payments being in a specific state.
With dependency injection: the Trip class gets passed an IPayments and IMap class/interface. This approach makes it easy to unit test the Trip class, with mocked Payments and Map classes. Those mocks can be set up to behave in ways the tests need, with little effort.

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.


Featured Pragmatic Engineer Jobs

  1. Machine Learning Engineering Lead at Conjecture. £85-210K + equity. London (UK).
  2. Full Stack Software Engineer at Insitro. Poland.
  3. Staff Back-End Engineer - Core Services at BetterUp. Remote (Germany, Netherlands or the UK).
  4. Senior Full Stack/Frontend Engineer at Vitally.io. $180-270K. New York or Remote.
  5. Founding Engineer at Renterra. $140-180K + equity. Remote (Global).
  6. Senior Lead Software Engineer - Kubernetes at Akamai Technologies. Remote (US).
  7. Senior Software Engineer - Cloud Native at Akamai Technologies. Remote (US).
  8. Software Engineer at DevZero. $150-175K. Seattle, Washington.
  9. Senior Backend Developer at Founda Health. Amsterdam, Netherlands.
  10. Senior Backend Engineer at Vital. $70-140K + equity. Remote.
  11. Principal Backend Enginee at Pento. £120-135K. Remote.
  12. Founding Senior Fullstack Engineer (JavaScript) at Playht. $150-200K + equity. San Francisco or Remote.
  13. Staff Software Engineer at Qualified.com. San Francisco or Remote.
  14. Infrastructure Team Lead at Ometria. £90-150K. United Kingdom or Portugal.
  15. Engineering Manager at Gruntwork. $175-240K + equity. Remote (Global).

The above jobs score at least 10/12 on The Pragmatic Engineer Test. Browse more senior engineer and engineering leadership roles with great engineering cultures, or add your own on The Pragmatic Engineer Job board and apply to join The Pragmatic Engineer Talent Collective.

Want to get interesting opportunities from vetted tech companies? Sign up to The Pragmatic Engineer Talent Collective and get sent great opportunities - similar to the ones below without any obligation. You can be public or anonymous, and I’ll be curating the list of companies and people.

Are you hiring senior+ engineers or engineering managers? Apply to join The Pragmatic Engineer Talent Collective to contact world-class senior and above engineers and engineering managers/directors. Get vetted drops twice a month, from software engineers - full-stack, backend, mobile, frontend, data, ML - and managers currently working at Big Tech, high-growth startups, and places with strong engineering cultures. Apply here.