Flutter Navigation 2.0 y GoRouter en 2026: Deep Linking y Preguntas de Entrevista

Dominar la navegación en Flutter con GoRouter 17.5: enrutamiento declarativo, deep linking, ShellRoute, guards de rutas y preguntas de entrevista con ejemplos prácticos.

Diagrama de arquitectura de navegación Flutter con flujo de deep linking GoRouter

La navegación en Flutter ha evolucionado significativamente desde la introducción de Navigation 2.0. GoRouter 17.5, el paquete oficial del equipo Flutter, ahora maneja la complejidad de la API Router mientras proporciona enrutamiento declarativo, deep linking automático y navegación anidada listos para usar.

GoRouter está funcionalmente completo

El equipo Flutter considera GoRouter funcionalmente completo desde 2026. Soporta parámetros de ruta, parámetros de consulta, redirecciones, ShellRoute para UI persistente y StatefulShellRoute para preservar el estado de las pestañas. Navigator 2.0 en crudo rara vez se escribe manualmente en producción.

Comprendiendo la Arquitectura de Navigator 2.0

Navigator 2.0 introdujo un enfoque declarativo basado en URLs para la navegación en Flutter. En lugar de llamadas imperativas push y pop, el estado de la aplicación determina qué aparece en la pila de navegación. La arquitectura se basa en tres clases fundamentales:

  • Router: El widget de nivel superior que coordina la navegación
  • RouteInformationParser: Traduce URLs en estado de aplicación
  • RouterDelegate: Construye el árbol de widgets basándose en ese estado

Cuando un deep link llega del sistema operativo o una URL cambia en un navegador web, el framework actualiza el estado y la UI reacciona. El problema: escribir un RouterDelegate y RouteInformationParser personalizados manualmente requiere código boilerplate considerable.

raw_navigator2_example.dartdart
// Raw Navigator 2.0 approach - verbose and rarely used directly
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
  
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  AppRoutePath? _currentPath;

  
  AppRoutePath? get currentConfiguration => _currentPath;

  
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        const MaterialPage(child: HomeScreen()),
        if (_currentPath?.isProductPage == true)
          MaterialPage(child: ProductScreen(id: _currentPath!.productId!)),
      ],
      onDidRemovePage: (page) {
        // Handle page removal
      },
    );
  }

  
  Future<void> setNewRoutePath(AppRoutePath path) async {
    _currentPath = path;
    notifyListeners();
  }
}

Esta verbosidad es la razón por la que existe GoRouter. Envuelve Navigator 2.0 y maneja el código boilerplate.

Configuración de GoRouter 17.5

GoRouter 17.5 introduce soporte para metadatos de ruta y restricciones con expresiones regulares para parámetros de ruta. El SDK mínimo requerido es Flutter 3.32 y Dart 3.8.

router_config.dartdart
import 'package:go_router/go_router.dart';

final goRouter = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true, // Useful during development
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'products/:id', // Path parameter
          name: 'product',
          builder: (context, state) {
            final productId = state.pathParameters['id']!;
            return ProductScreen(productId: productId);
          },
        ),
        GoRoute(
          path: 'search',
          name: 'search',
          builder: (context, state) {
            // Query parameters: /search?q=flutter&category=books
            final query = state.uri.queryParameters['q'] ?? '';
            final category = state.uri.queryParameters['category'];
            return SearchScreen(query: query, category: category);
          },
        ),
      ],
    ),
  ],
);

El router se integra con MaterialApp.router:

main.dartdart
import 'package:flutter/material.dart';
import 'router_config.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: goRouter,
      title: 'GoRouter Demo',
    );
  }
}

Configuración de Deep Linking para Android e iOS

El deep linking permite que URLs externas abran pantallas específicas en la aplicación. GoRouter maneja el enrutamiento automáticamente una vez que la configuración de la plataforma está en su lugar.

Android requiere un intent-filter en AndroidManifest.xml y un archivo Digital Asset Links alojado en el dominio:

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
      android:scheme="https"
      android:host="example.com"
      android:pathPrefix="/products" />
  </intent-filter>
</activity>

El archivo assetlinks.json debe servirse en https://example.com/.well-known/assetlinks.json.

Para iOS, la capacidad Associated Domains debe habilitarse en Xcode, y un archivo apple-app-site-association debe alojarse:

https://example.com/.well-known/apple-app-site-associationjson
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.app",
        "paths": ["/products/*", "/search"]
      }
    ]
  }
}

GoRouter 17.5 corrigió un bug crítico donde los deep links de Android en arranque en frío con rutas vacías perdían el esquema y la autoridad. Esto significa que los deep links ahora funcionan de manera confiable incluso cuando la aplicación no está ejecutándose.

Probando deep links

Para probar deep links en Android usar adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123". Para iOS, usar xcrun simctl openurl booted "https://example.com/products/123".

ShellRoute para UI de Navegación Persistente

ShellRoute envuelve las rutas hijas con un elemento de UI persistente como una BottomNavigationBar o un Drawer. El shell permanece visible mientras se navega entre sus hijos.

shell_router_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    ShellRoute(
      builder: (context, state, child) {
        return ScaffoldWithNavBar(child: child);
      },
      routes: [
        GoRoute(
          path: '/home',
          name: 'home',
          builder: (context, state) => const HomeTab(),
        ),
        GoRoute(
          path: '/explore',
          name: 'explore',
          builder: (context, state) => const ExploreTab(),
        ),
        GoRoute(
          path: '/profile',
          name: 'profile',
          builder: (context, state) => const ProfileTab(),
        ),
      ],
    ),
  ],
);

// scaffold_with_nav_bar.dart
class ScaffoldWithNavBar extends StatelessWidget {
  final Widget child;
  const ScaffoldWithNavBar({super.key, required this.child});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _calculateSelectedIndex(context),
        onTap: (index) => _onItemTapped(index, context),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }

  int _calculateSelectedIndex(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    if (location.startsWith('/explore')) return 1;
    if (location.startsWith('/profile')) return 2;
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0: context.go('/home');
      case 1: context.go('/explore');
      case 2: context.go('/profile');
    }
  }
}

GoRouter 17.5 corrigió un problema donde los gestos de retroceso en iOS sacaban todo el ShellRoute en lugar de solo la sub-ruta activa.

¿Listo para aprobar tus entrevistas de Flutter?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

StatefulShellRoute para Preservar Estado de Pestañas

Cuando los usuarios cambian de pestaña, ShellRoute reconstruye el hijo. StatefulShellRoute preserva la pila de navegación de cada rama:

stateful_shell_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeTab(),
              routes: [
                GoRoute(
                  path: 'details/:id',
                  builder: (context, state) => DetailsScreen(
                    id: state.pathParameters['id']!,
                  ),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/explore',
              builder: (context, state) => const ExploreTab(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              builder: (context, state) => const ProfileTab(),
            ),
          ],
        ),
      ],
    ),
  ],
);

Con esta configuración, si un usuario navega a /home/details/42, cambia a la pestaña Explorar, luego regresa a Inicio, seguirá en la pantalla de detalles.

Guards de Ruta con Redirect

Las redirecciones manejan verificaciones de autenticación y navegación condicional. El callback redirect se ejecuta antes de cada navegación:

authenticated_router.dartdart
final goRouter = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final isLoggingIn = state.matchedLocation == '/login';

    // Redirect to login if not authenticated
    if (!isLoggedIn && !isLoggingIn) {
      return '/login?redirect=${state.uri}';
    }

    // Redirect away from login if already authenticated
    if (isLoggedIn && isLoggingIn) {
      final redirect = state.uri.queryParameters['redirect'];
      return redirect ?? '/';
    }

    return null; // No redirect
  },
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/admin',
      redirect: (context, state) {
        // Route-level redirect for admin-only access
        if (!AuthService.instance.isAdmin) {
          return '/';
        }
        return null;
      },
      builder: (context, state) => const AdminScreen(),
    ),
  ],
);

GoRouter 17.5 agregó soporte para usar métodos de extensión de contexto como context.namedLocation() dentro de los callbacks de redirección.

Preguntas de Entrevista Comunes sobre Navegación Flutter

Los entrevistadores frecuentemente evalúan el conocimiento de navegación porque toca arquitectura, gestión de estado e integración de plataforma. Aquí hay preguntas que distinguen a los candidatos senior:

P: ¿Qué problema resuelve Navigator 2.0 que Navigator 1.0 no resuelve?

Navigator 1.0 usa navegación imperativa: Navigator.push() y Navigator.pop(). La aplicación no puede representar el estado de navegación como una URL, haciendo difícil el deep linking y el soporte web. Navigator 2.0 es declarativo: el estado de la aplicación determina la pila de navegación, las URLs son ciudadanos de primera clase, y los deep links funcionan automáticamente.

P: ¿Cuándo usar context.go() vs context.push()?

context.go('/path') reemplaza la pila de navegación actual hasta la ruta coincidente. context.push('/path') agrega una nueva ruta encima de la pila existente. Usar go() para navegación de nivel superior (cambiar pestañas, ir al inicio) y push() para profundizar en detalles mientras se preserva la navegación hacia atrás.

P: ¿Cómo preservar el estado entre cambios de pestañas?

Usar StatefulShellRoute.indexedStack(). Mantiene pilas de navegación separadas para cada rama y las preserva al cambiar de pestaña. Sin esto, cambiar de pestaña reconstruye todo el sub-árbol.

P: ¿Cómo pasar objetos complejos entre rutas?

Existen tres enfoques. Primero, pasar un ID en el parámetro de ruta y obtener el objeto en la pantalla de destino. Segundo, usar el parámetro extra de GoRouter para pasar el objeto directamente. Tercero, almacenar el objeto en una solución de gestión de estado (Riverpod, Bloc) y accederlo desde el destino. El primer enfoque es mejor para deep linking porque la URL permanece compartible.

dart
// Passing via extra (not deep-linkable)
context.push('/product', extra: productObject);

// Receiving via extra
GoRoute(
  path: '/product',
  builder: (context, state) {
    final product = state.extra as Product?;
    return ProductScreen(product: product);
  },
),

P: ¿Qué es el callback onEnter en GoRouter 17.5?

El callback onEnter se ejecuta al entrar a una ruta y proporciona acceso tanto al estado de ruta actual como al siguiente. Permite ejecutar lógica antes de que la ruta se cargue completamente, útil para analytics o pre-carga condicional.

Rutas Type-Safe con Generación de Código

GoRouter soporta enrutamiento type-safe a través de generación de código. Se definen clases de datos de ruta y build_runner genera el código de navegación:

routes.dartdart
import 'package:go_router/go_router.dart';

part 'routes.g.dart';

<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

<ProductRoute>(path: '/product/:id')
class ProductRoute extends GoRouteData {
  final String id;
  const ProductRoute({required this.id});

  
  Widget build(BuildContext context, GoRouterState state) =>
      ProductScreen(productId: id);
}

Después de ejecutar dart run build_runner build, la navegación tiene seguridad en tiempo de compilación:

dart
// Type-safe navigation
ProductRoute(id: '123').go(context);

Esto elimina errores tipográficos en las rutas y asegura que los parámetros siempre se proporcionen.

Dominando la Navegación Flutter para Aplicaciones de Producción

  • GoRouter 17.5 es la solución de navegación recomendada para aplicaciones Flutter que requieren deep linking, navegación anidada o soporte web
  • Usar ShellRoute para barras de navegación inferiores persistentes; usar StatefulShellRoute para preservar la pila de navegación de cada pestaña
  • Configurar el deep linking específico de plataforma (Android App Links, iOS Universal Links) por separado de GoRouter, que maneja la lógica de enrutamiento
  • Implementar guards de autenticación con el callback redirect a nivel de router o a nivel de ruta individual
  • Para escenarios de entrevista, enfocarse en explicar el modelo declarativo: el estado de la aplicación dirige la UI, las URLs se derivan del estado, y GoRouter maneja la traducción
  • Las rutas type-safe con generación de código eliminan errores en tiempo de ejecución por rutas mal escritas
  • Consultar la documentación de navegación Flutter y el changelog de GoRouter para características específicas por versión

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Reto diario

¿Sabrías detectar el bug en Flutter?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 23 de agosto de 2026

Etiquetas

#flutter
#navigation
#gorouter
#deep-linking
#mobile

Compartir

Artículos relacionados