Navigate between feature modules without direct dependencies?

Tests dependency inversion in modular apps. Answer by defining navigation interfaces in a shared module, implementing them in the :app module, and using DI to connect them. A red flag is suggesting direct module dependencies or using reflection for navigation.
What's really being asked
This question tests your understanding of dependency inversion and architectural patterns for maintaining strict module boundaries. The interviewer wants to see if you can design a system where modules communicate (navigate) without creating tight coupling, which would negate the benefits of modularization like improved build times and team autonomy.
The full answer
An excellent answer describes a solution using inversion of control. First, state the problem: a direct dependency from :feature_profile to :feature_settings is an anti-pattern that leads to a tangled dependency graph and slower builds. Second, propose an abstraction. Feature modules should only depend on a lightweight contract or API module, for example, :navigation-api. This module defines interfaces for navigation, like interface SettingsNavigator { fun openSettings() }. Third, explain the implementation. The :app module, which has visibility over all feature modules, provides the concrete implementation of these interfaces. It uses dependency injection (e.g., Hilt) to bind the interface to a class that can access the root NavController. Fourth, describe the flow: the profile feature gets the SettingsNavigator injected, calls openSettings(), and the implementation in the :app module executes navController.navigate(R.id.settings_graph).
The mistakes people make
Adding a direct Gradle dependency like implementation project(":feature_settings") in the :feature_profile module's build.gradle.kts file. This is the primary anti-pattern the question is designed to catch. Another red flag is suggesting reflection to find and instantiate fragment or activity classes by name. This is brittle, not type-safe, and can be broken by code shrinking tools like R8/ProGuard. Finally, relying on manually constructed implicit intents with string actions for all internal navigation is a weaker pattern; it lacks the compile-time safety of a navigator interface.
What usually comes next
Expect questions about data transfer and results. For passing arguments, you'd add parameters to the navigator interface method (e.g., openSettings(userId: String)) and pass them via a Bundle or Safe Args. To get a result back, you would use the Jetpack Navigation Component's result APIs, like setFragmentResultListener and setFragmentResult, which work across module boundaries.
A concrete example
:navigation-api module: Contains interface SettingsNavigator { fun navigateToSettings() }.
:feature_profile module: Depends on :navigation-api. Its ViewModel receives SettingsNavigator via its constructor (@Inject constructor(private val navigator: SettingsNavigator)) and calls navigator.navigateToSettings().
:app module: Depends on :navigation-api, :feature_profile, and :feature_settings. It contains the Hilt binding: class AppSettingsNavigator @Inject constructor(...) : SettingsNavigator { override fun navigateToSettings() { / get NavController and navigate to settings graph / } }.
Interview question
In a modular app, how can `:feature_A` navigate to a screen in `:feature_B` without creating a direct dependency on `:feature_B`?
- a.In `:feature_A`'s build file, add `implementation project(':feature_B')` and navigate directly using the screen's class.
- b.Start an implicit Intent from `:feature_A` with a custom string action, and add an intent filter to the destination screen in `:feature_B`.
- c.Define a navigation interface in a shared `:api` module. `:feature_A` calls the interface, and the `:app` module provides the implementation.Correct
- d.From `:feature_A`, use reflection to find the destination screen's class by its string name and create an Intent to start it.
Why? this is the answer
This approach uses dependency inversion. By defining a contract in a shared module, `:feature_A` depends only on the contract, not the concrete implementation, maintaining decoupling. Adding a direct dependency (Option A) is an anti-pattern that negates the benefits of modularization.
Just read this? Test yourself on what you have been reading.
Read the original → developer.android.com
- #android
- #architecture
- #multi-module
- #navigation
You just looked this up. Could you explain it out loud?
That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on android — each one lists the topics its interview covers.
See open roles