El problema de navegar entre módulos
En un proyecto monomódulo, navegar de Productos a Checkout es trivial: tenés acceso directo a la CheckoutFragmentDirections generada por Safe Args. En multimodulo, :feature:productos no puede importar :feature:checkout — eso sería una dependencia entre features que viola las reglas del grafo.
El problema entonces: ¿cómo navega :feature:productos a :feature:checkout sin importarlo?
Hay dos soluciones robustas. La primera es la más limpia para la mayoría de los casos:
Deep links como contrato de navegación
Los deep links son URIs que identifican destinos de navegación. En lugar de navegar directamente a un Fragment, navegás a una URI. Cada feature declara sus URIs — son su contrato público de navegación — y cualquier otro módulo puede navegar a ellas sin importar nada.
// :feature:checkout — declarar el deep link en el NavGraph del módulo
// checkout_nav_graph.xml:
// <fragment android:name=".CheckoutFragment">
// <deepLink app:uri="miapp://checkout" />
// <deepLink app:uri="miapp://checkout?productoId={productoId}" />
// </fragment>
// :feature:productos — navegar a checkout SIN importar :feature:checkout
class ProductosViewModel @Inject constructor(
private val navController: NavController // inyectado o pasado como parámetro
) : ViewModel() {
fun irACheckout(productoId: Int) {
// Solo necesita conocer la URI — no la clase CheckoutFragment
navController.navigate(
Uri.parse("miapp://checkout?productoId=$productoId")
)
}
}
// O desde el Fragment directamente:
class ProductosFragment : Fragment() {
fun irACheckout(productoId: Int) {
findNavController().navigate(
Uri.parse("miapp://checkout?productoId=$productoId")
)
}
}
Las URIs son el API público de cada featureTratá los deep links de cada módulo como un contrato versionado. Si cambiás la estructura de la URI de checkout, todos los módulos que navegan a ella dejan de funcionar. Centralizar las URIs en constantes en :core:ui o en un objeto de navegación ayuda a mantener consistencia.
NavGraph por módulo — el setup correcto
// Cada feature tiene su propio NavGraph que define sus fragmentos internos
// :feature:checkout/res/navigation/checkout_nav_graph.xml
// El NavGraph raíz en :app incluye los de cada feature como nested graphs:
// :app/res/navigation/nav_graph.xml
// <navigation app:startDestination="@id/productosFragment">
// <include app:graph="@navigation/checkout_nav_graph" />
// <include app:graph="@navigation/productos_nav_graph" />
// <include app:graph="@navigation/auth_nav_graph" />
// </navigation>
// Las acciones globales (back stack limpio al hacer logout, etc.)
// se definen en el NavGraph raíz de :app
// En MainActivity — el host de navegación
class MainActivity : AppCompatActivity() {
private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment)
.navController
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// El NavController en :app tiene visibilidad de todos los nested graphs
}
}
Patrón Navigator — la alternativa con más control
Cuando los deep links no son suficientes — por ejemplo, cuando necesitás pasar objetos complejos que no caben en una URI — el patrón Navigator da más control:
// :core:domain — la interfaz de navegación (Kotlin puro, sin Android)
interface CheckoutNavigator {
fun irACheckout(carrito: Carrito)
fun irAConfirmacion(numeroPedido: String)
}
interface ProductosNavigator {
fun irADetalle(productoId: Int)
fun irABusqueda(query: String)
}
// :app — las implementaciones que conocen el NavController real
class CheckoutNavigatorImpl @Inject constructor(
private val navController: NavController
) : CheckoutNavigator {
override fun irACheckout(carrito: Carrito) {
// :app sí puede importar :feature:checkout
navController.navigate(
CheckoutFragmentDirections.actionGlobalCheckout(carrito.id)
)
}
override fun irAConfirmacion(numeroPedido: String) {
navController.navigate(Uri.parse("miapp://confirmacion/$numeroPedido"))
}
}
// :app/di/NavigationModule.kt — registrar las implementaciones
@Module
@InstallIn(ActivityRetainedComponent::class)
abstract class NavigationModule {
@Binds
abstract fun bindCheckoutNavigator(impl: CheckoutNavigatorImpl): CheckoutNavigator
}
// :feature:productos — inyectar el navigator sin saber nada de :app ni :feature:checkout
class ProductosViewModel @Inject constructor(
private val repo: ProductoRepository,
private val checkoutNavigator: CheckoutNavigator // interfaz de :core:domain ✓
) : ViewModel() {
fun irACheckout() {
viewModelScope.launch {
val carrito = repo.getCarrito()
checkoutNavigator.irACheckout(carrito)
}
}
}
NavGraph raíz en :app — el integrador
// :app es el único módulo que tiene visibilidad de todos los NavGraphs
// Por eso las acciones globales van acá:
// Limpiar el back stack completo y volver al login (ej: logout)
navController.navigate(R.id.authGraph) {
popUpTo(navController.graph.id) { inclusive = true }
}
// Deep link desde una notificación push — el NavController lo resuelve:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Si la Activity se abrió desde una notificación con deep link:
val deepLink = intent.data
if (deepLink != null) {
navController.navigate(deepLink)
}
}
Back stack entre módulos
// Navegar entre features con back stack limpio:
// "Ir a checkout y que el back no vuelva a la pantalla anterior"
findNavController().navigate(
Uri.parse("miapp://checkout"),
NavOptions.Builder()
.setPopUpTo(R.id.productosFragment, inclusive = false)
.build()
)
// Limpiar el back stack completo (ej: después de confirmar el pago)
findNavController().navigate(
Uri.parse("miapp://home"),
NavOptions.Builder()
.setPopUpTo(findNavController().graph.id, inclusive = true)
.build()
)