Navan Tech Blog
Building a Better Navan App Development Process

Making the Navan App Development Process Smoother for Android

Sachin Hiriyanna

19 Dec 2024
7 minute read
Making the Navan App Development Process Smoother for Android

A behind-the-scenes look at how unifying dependency injection (DI) frameworks improved productivity and reduced complexity for Navan’s Android engineers.

When our company was known as TripActions, we had two apps: the TripActions app for travel and the TripActions Liquid app for expenses. But in early 2023, we merged them into the unified Navan app — and we did it in just eight weeks. 

Completing this merger, especially under such a tight timeline, led to many challenges for our engineers. As we added features and fixed bugs, we uncovered quirks in the codebase. One of the most significant challenges we encountered in our Android codebase was the coexistence of two distinct dependency injection (DI) frameworks.

A DI framework is used in software development to help manage how different modules of an application work together. Many interconnected components (like features or services) need to communicate in an app; a DI framework organizes these connections and makes it easy to set them up or change them without having to rewrite a lot of code.

Think of it as a system that automatically connects the right parts when building something complex. 

Suppose an engineer needs to swap out one part (e.g., an analytics tracking system). The DI framework handles the connections for them, reducing errors and making the app easier to maintain and expand. This is especially beneficial in large apps, where managing these connections manually would be time-consuming and error-prone.

Unifying DI frameworks in a fast-growing codebase is more than just a technical improvement — it’s a leap toward smoother development and frictionless app architecture.

This article details our journey of unifying DI frameworks in the Navan Android codebase. We explore the rationale behind choosing the Hilt framework over Koin, highlight Hilt’s advantages, and describe our migration strategy. Our experience provides valuable insights for teams taking on similar large-scale Android-refactoring initiatives.

Where We Started

We began with two DI frameworks coexisting in our codebase — an undesirable situation. We used:

  • Koin: A lightweight DI framework for standalone Kotlin projects, known for its simplicity and configuration based on Domain-Specific Language (DSL).
  • Hilt: An Android-specific DI framework built on Dagger that offered compile-time safety and enhanced runtime performance through code generation.

Koin handled travel-related functionalities, while Hilt managed expense-related features. This created confusion and unnecessary complexity for engineers working across different app sections and was especially problematic for new team members, who faced a steep learning curve.

We set out to unify our DI frameworks to reduce the cognitive load on engineers, improve code consistency, boost productivity, and enhance maintainability. A unified approach would also simplify onboarding for new team members who had to learn two systems.

But which framework would we choose?

Advantages of Hilt over Koin

Our transition from Koin to Hilt was motivated by the latter’s distinct strengths. Hilt provides enhanced stability, superior runtime performance, and improved long-term maintainability — essential considerations for our expansive Navan Android application.

A key advantage of Hilt is also its compile-time dependency resolution, which offers substantial benefits over Koin’s runtime method. Compile-time dependency resolution refers to the process where a DI framework identifies and connects the dependencies of various components during the application’s compilation phase

Hilt mitigates potential issues early in the development cycle by identifying and resolving dependencies during compilation. That increases the application’s stability and boosts developer productivity due to prompt feedback on dependency-related concerns.

Here’s a concise comparison of key features between Hilt and Koin:

Key Migration Decisions

Transitioning from Koin to Hilt was a substantial undertaking that required careful planning and strategic execution. 

Here’s a breakdown of the important decisions we made in the lead-up to the transition:

  • Single-pull request: To minimize disruptions and maintain stability in our main branch throughout the process, we opted for a single, all-encompassing pull request (PR) for the migration.
  • Daily rebasing: We updated the migration branch by capturing and converting any new Koin-based code to Hilt. This gradual approach helped us stay current with ongoing development.
  • Two-week buffer: With our two-week sprint cycle, we strategically merged the pull requests on the first day of a sprint. This timing allowed engineers to familiarize themselves with the changes and address any issues before the production release.
  • Team training: Post-merge, we conducted training sessions to acquaint the team with Hilt’s usage and best practices to support a smooth transition.

The Migration from Koin to Hilt

The team knew migrating from Koin to Hilt would be challenging, but we were excited about its benefits. Here’s how our journey unfolded:

The Process

Step 1: Remove Koin

We began by eliminating all Koin-related dependencies — effectively severing ties with the old framework. This step allowed us to start with a clean slate so that no remnants of the old system would interfere with our new implementation. With the foundation cleared, we began rebuilding our DI framework using Hilt.

Step 2: Migrate Koin Modules and Dependencies

We methodically converted each Koin dependency module to Hilt. Here’s an example:

Step 2: Migrate Koin Modules and Dependencies

Step 3: Migrate Dependents

We converted each class that used Koin-provided dependencies to use Hilt-provided dependencies. Here’s an example:

Step 3

Step 4: Handle Injections Made to Static Code 

We refactored the code to properly inject dependencies for Koin injections in static classes and functions. 

In cases where refactoring was impractical due to code size or complexity, we employed Hilt’s direct EntryPoint — with a twist. We marked these instances as deprecated, signaling to our team that they should be phased out in future iterations. 

Here’s an example of direct EntryPoint:

Step 4

Step 5: Handle Legacy MVP Code 

Our codebase contained significant legacy code using model-view-presenter (MVP) architecture. Since injecting the view into a presenter wasn’t possible with Hilt, we implemented a custom factory for these cases. 

The presenter serves as a mediator between the model and the view. It fetches data from the model (repositories) and processes it to help ensure it is properly formatted for presentation in the view. 

This factory used “@AssistedInject” to inject the views at runtime for constructing presenters. Here’s an example: 

Step 5

Overcoming Obstacles

While migrating to Hilt, we encountered two key challenges: missing entry points and ViewModel instantiation issues. (The ViewModel acts as a data provider and logic handler for the View in MVVM architecture. It processes data from the Model, manages UI-related state, and exposes data streams for the View to consume.) 

Post-build, runtime crashes occurred when dependencies lacked proper entry point annotations — declarations that allow Hilt to inject required dependencies into specific components. 

To address this, we used Detekt, a static code analysis tool for Kotlin, with custom rule capabilities to flag and resolve orphaned injections. Similarly, crashes related to ViewModels missing the required “@HiltViewModel” annotation and “@Inject constructor” were also flagged using tailored Detekt rules. 

With these fixes, our dependency wiring became compile-time safe — helping to ensure that dependency issues were resolved during code compilation rather than runtime.

Overcoming Obstacles
Overcoming Obstacles 2

Once our app ran smoothly, we focused on verifying everything worked correctly. Extensive unit tests (focused on individual classes), end-to-end tests (validating full features), and bug-bash sessions verified that the migration was spotless and that we had resolved any lingering issues. Testing also helped us further refine our Detekt rules.

Lessons Learned

Throughout this migration process, we learned several valuable lessons:

Lesson 1: Frequent rebasing and creating a single PR, rather than multiple ones, proved to be the smoothest approach for this migration.

Lesson 2: Leveraging a static analysis tool like Detekt was crucial. It helped us catch missing “@AndroidEntryPoint” and “@HiltViewModel” annotations, making our dependency injection virtually foolproof.

Lesson 3: We discovered the critical role of well-covered testing infrastructure in facilitating large-scale refactoring efforts. Our end-to-end tests provided a safety net, giving us the confidence to make sweeping changes across the codebase.

In Conclusion

This migration was more than just a technical upgrade — it was a game-changer for how we work. By unifying our DI framework with Hilt, we not only streamlined our development process but also laid the groundwork for faster, smarter innovation in the Navan Android app. 

The result? A smoother experience for our engineers and, ultimately, for our users. This transformation is just one example of our commitment to building an efficient app architecture that delivers exceptional performance and scalability.

Here’s a screenshot of the actual PR, which highlights the scale of the project. We changed more than 1,500 files and almost 100,000 lines of code.

Interested in joining our innovative engineering team? Explore opportunities on our careers page and be part of Navan’s next technological advancement.

Return to blog

This content is for informational purposes only. It doesn't necessarily reflect the views of Navan and should not be construed as legal, tax, benefits, financial, accounting, or other advice. If you need specific advice for your business, please consult with an expert, as rules and regulations change regularly.

More content you might like