tutoriales.com

Navegación Avanzada en Flutter: Rutas Dinámicas y Deep Linking con GoRouter

Este tutorial te guiará a través de la implementación de navegación avanzada en Flutter utilizando el paquete GoRouter. Exploraremos cómo definir rutas dinámicas, pasar y recuperar parámetros, y configurar el Deep Linking para una experiencia de usuario fluida y robusta.

Intermedio18 min de lectura8 views19 de marzo de 2026Reportar error

Flutter, con su enfoque declarativo, ha revolucionado el desarrollo de aplicaciones móviles. Sin embargo, la gestión de la navegación, especialmente en aplicaciones complejas con múltiples pantallas y flujos de usuario, puede volverse desafiante. Es aquí donde soluciones como GoRouter entran en juego, ofreciendo una forma declarativa y potente de manejar el enrutamiento.

🚀 ¿Por qué GoRouter para la Navegación Avanzada?

La navegación es el corazón de cualquier aplicación. Los usuarios necesitan moverse entre pantallas de manera intuitiva y eficiente. Flutter ofrece sus propios mecanismos de navegación (Navigator y MaterialPageRoute), pero para escenarios más complejos como:

  • Rutas Dinámicas: Mostrar diferentes pantallas basadas en IDs de usuario, productos, etc.
  • Navegación Anidada: Múltiples Navigator dentro de otros Navigator.
  • Deep Linking: Abrir una pantalla específica desde un enlace externo (web, notificación).
  • Redirecciones: Guiar al usuario a diferentes pantallas según su estado (autenticado, no autenticado).
  • Manejo de estados: Integración con soluciones de gestión de estado.

GoRouter simplifica enormemente estas tareas, ofreciendo una API limpia y una gestión robusta del estado de navegación. Es una solución declarativa basada en el enrutamiento URI, lo que la hace ideal para Deep Linking y navegación web (cuando se compila para la web).

💡 Consejo: GoRouter es el paquete recomendado por el equipo de Flutter para la navegación avanzada, especialmente a partir de Flutter 3.0.

🛠️ Configuración Inicial de GoRouter

Antes de sumergirnos en las funcionalidades avanzadas, necesitamos configurar GoRouter en nuestro proyecto Flutter.

1. Añadir la Dependencia

Primero, añade go_router a tu archivo pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.0 # Usa la última versión estable

Luego, ejecuta flutter pub get en tu terminal para descargar la dependencia.

2. Configurar el Enrutador Básico

Ahora, configuraremos una instancia de GoRouter en nuestro main.dart o en un archivo de configuración de rutas dedicado. Reemplazaremos MaterialApp por MaterialApp.router.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

// Definición de nuestras rutas
final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: '/',
      builder: (BuildContext context, GoRouterState state) {
        return const HomeScreen();
      },
    ),
    GoRoute(
      path: '/details',
      builder: (BuildContext context, GoRouterState state) {
        return const DetailsScreen();
      },
    ),
  ],
  // Opcional: Manejo de errores 404
  errorBuilder: (context, state) => ErrorScreen(error: state.error.toString()),
);

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'GoRouter Advanced Navigation',
      routerConfig: _router,
    );
  }
}

// Placeholder Screens
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Screen')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to the Home Screen!'),
            ElevatedButton(
              onPressed: () => context.go('/details'),
              child: const Text('Go to Details'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details Screen')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('This is the Details Screen.'),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

class ErrorScreen extends StatelessWidget {
  final String error;
  const ErrorScreen({super.key, required this.error});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Error')),
      body: Center(
        child: Text('An error occurred: $error'),
      ),
    );
  }
}

En este punto, ya tenemos una navegación básica funcionando. Usamos context.go('/ruta') para navegar a una ruta y context.pop() para volver a la pantalla anterior.


🌐 Rutas Dinámicas con Parámetros

Una de las características más potentes de GoRouter es su capacidad para manejar rutas dinámicas, lo que significa que puedes pasar datos como parte de la URL. Esto es crucial para mostrar detalles de un elemento específico, como el perfil de un usuario o los detalles de un producto.

1. Definir Rutas con Parámetros

Para definir un parámetro en una ruta, se utiliza un prefijo :. Por ejemplo, /users/:userId indica que userId es un parámetro dinámico.

// ... (código anterior)

final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    // ... (rutas existentes)
    GoRoute(
      path: '/users/:userId',
      builder: (BuildContext context, GoRouterState state) {
        final String userId = state.pathParameters['userId']!;
        return UserProfileScreen(userId: userId);
      },
    ),
    GoRoute(
      path: '/products/:productId/details',
      builder: (BuildContext context, GoRouterState state) {
        final String productId = state.pathParameters['productId']!;
        return ProductDetailsScreen(productId: productId);
      },
    ),
  ],
  // ...
);

// ... (main, MyApp)

class UserProfileScreen extends StatelessWidget {
  final String userId;
  const UserProfileScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('User Profile: $userId')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Displaying profile for user ID: $userId'),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

class ProductDetailsScreen extends StatelessWidget {
  final String productId;
  const ProductDetailsScreen({super.key, required this.productId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product Details: $productId')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Details for product ID: $productId'),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

2. Navegar a Rutas Dinámicas y Recuperar Parámetros

Para navegar a estas rutas, simplemente pasamos el valor del parámetro en la URL. Para recuperar el parámetro, usamos state.pathParameters['nombreDelParametro'] dentro del builder de la ruta.

Ejemplo en HomeScreen:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Screen')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to the Home Screen!'),
            ElevatedButton(
              onPressed: () => context.go('/details'),
              child: const Text('Go to Details'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => context.go('/users/123'), // Navega a un perfil de usuario
              child: const Text('Go to User 123 Profile'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => context.go('/products/ABC456/details'), // Navega a detalles de producto
              child: const Text('Go to Product ABC456 Details'),
            ),
          ],
        ),
      ),
    );
  }
}
📌 Nota: Los parámetros de ruta siempre se recuperan como `String`. Si necesitas otro tipo de dato (como un `int`), deberás parsearlo manualmente, por ejemplo: `int.parse(userId)`.

3. Parámetros de Consulta (Query Parameters)

Además de los parámetros de ruta, GoRouter también soporta parámetros de consulta (query parameters), que son útiles para filtros, ordenaciones, o datos opcionales que no forman parte esencial de la URL.

Definición (no se definen explícitamente en la ruta):

GoRoute(
  path: '/search',
  builder: (BuildContext context, GoRouterState state) {
    final String? query = state.uri.queryParameters['q'];
    final String? category = state.uri.queryParameters['cat'];
    return SearchResultsScreen(query: query, category: category);
  },
),

Navegación:

// En HomeScreen o cualquier otro lugar
ElevatedButton(
  onPressed: () => context.go('/search?q=flutter&cat=mobile'),
  child: const Text('Search for Flutter in Mobile'),
),

Recuperación en SearchResultsScreen:

import 'package:flutter/material.dart';

class SearchResultsScreen extends StatelessWidget {
  final String? query;
  final String? category;
  const SearchResultsScreen({super.key, this.query, this.category});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Search Results')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Search Query: ${query ?? 'N/A'}'),
            Text('Category: ${category ?? 'N/A'}'),
            ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

🔗 Deep Linking y Web Integration

El Deep Linking permite que los usuarios sean dirigidos a una pantalla específica dentro de tu aplicación (ya sea móvil o web) a través de un enlace URL. GoRouter está diseñado para manejar esto de forma nativa.

1. Configuración de Deep Links en Android

Para que tu aplicación de Android pueda manejar Deep Links, necesitas configurar intent-filters en tu archivo AndroidManifest.xml.

Abre android/app/src/main/AndroidManifest.xml y añade el siguiente intent-filter dentro de la <activity> principal:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application ...>
        <activity ...>
            <!-- Existing intent filter for FLUTTER_VIEW and MAIN -->
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

            <!-- Deep Linking intent filter -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data
                    android:scheme="myapp"
                    android:host="example.com"/>
                <!-- Puedes añadir más esquemas/hosts según sea necesario -->
            </intent-filter>
        </activity>
    </application>
</manifest>

Con esta configuración, tu aplicación responderá a enlaces como myapp://example.com/users/123 o http://example.com/users/123 (si usas http como esquema).

Probando Deep Links en Android:

Desde la terminal, puedes simular un Deep Link usando adb:

adb shell am start -W -a android.intent.action.VIEW -d "myapp://example.com/users/456"

O directamente abriendo la URL en un navegador si configuraste un esquema http o https.

2. Configuración de Deep Links en iOS

En iOS, la configuración de Deep Links se realiza a través de Universal Links o Custom URL Schemes.

Custom URL Schemes:

  1. Abre tu proyecto iOS en Xcode (ios/Runner.xcworkspace).
  2. Selecciona el proyecto Runner en el navegador de proyectos.
  3. Ve a la pestaña Info.
  4. Expande URL Types y haz clic en el botón +.
  5. En URL Schemes, añade tu esquema personalizado (por ejemplo, myapp).

Ahora, tu aplicación responderá a enlaces como myapp://users/789.

Universal Links (Recomendado para producción):

Los Universal Links son más complejos e involucran configurar un archivo apple-app-site-association en tu servidor web. GoRouter detectará automáticamente los Universal Links si están configurados correctamente. Este proceso está fuera del alcance de este tutorial, pero es importante conocerlo.

3. Manejo de Rutas Iniciales y Deep Links con GoRouter

GoRouter maneja automáticamente la ruta inicial proporcionada por un Deep Link. Cuando la aplicación se lanza a través de un Deep Link, GoRouter procesa la URL y navega a la pantalla correspondiente.

🔥 Importante: Asegúrate de que las rutas definidas en GoRouter (`/users/:userId`, etc.) coincidan con las URLs que esperas recibir a través de Deep Linking.

🗺️ Gestión Avanzada de Rutas con GoRoute y ShellRoute

GoRouter ofrece una estructura de rutas flexible que permite crear navegaciones complejas y anidadas utilizando GoRoute y ShellRoute.

1. GoRoute: La Ruta Básica

Como ya hemos visto, GoRoute es el componente fundamental para definir una ruta y la pantalla asociada.

GoRoute(
  path: '/',
  builder: (context, state) => const HomeScreen(),
  routes: <RouteBase>[
    // Rutas anidadas pueden ir aquí, como /settings/profile
    GoRoute(
      path: 'settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
),

2. ShellRoute: Navegación Persistente y Barras de Navegación

ShellRoute es ideal para layouts persistentes, como una BottomNavigationBar o un Drawer que permanece visible mientras el contenido principal cambia. Permite mantener el estado de navegación de las pantallas hijas.

Ejemplo de ShellRoute con BottomNavigationBar:

Primero, definimos un ShellRoute que contendrá nuestra ScaffoldWithNavBar (o un widget similar que tenga la barra de navegación).

// Definimos un GlobalKey para el Navigator de nuestro ShellRoute
final _shellNavigatorKey = GlobalKey<NavigatorState>();

final GoRouter _router = GoRouter(
  // ...
  routes: <RouteBase>[
    ShellRoute(
      navigatorKey: _shellNavigatorKey, // Asignamos la clave al ShellRoute
      builder: (BuildContext context, GoRouterState state, Widget child) {
        return ScaffoldWithNavBar(child: child);
      },
      routes: <RouteBase>[
        GoRoute(
          path: '/a',
          builder: (BuildContext context, GoRouterState state) {
            return const ScreenA();
          },
        ),
        GoRoute(
          path: '/b',
          builder: (BuildContext context, GoRouterState state) {
            return const ScreenB();
          },
        ),
        GoRoute(
          path: '/c',
          builder: (BuildContext context, GoRouterState state) {
            return const ScreenC();
          },
        ),
      ],
    ),
    // Podemos tener otras rutas fuera del ShellRoute, como una pantalla de login
    GoRoute(
      path: '/login',
      builder: (BuildContext context, GoRouterState state) => const LoginScreen(),
    ),
  ],
  // ...
);

Ahora, necesitamos el ScaffoldWithNavBar que gestionará la BottomNavigationBar y mostrará el child (la pantalla actual del ShellRoute).

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _calculateSelectedIndex(context),
        onTap: (int idx) => _onItemTapped(idx, context),
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Business'),
          BottomNavigationBarItem(icon: Icon(Icons.school), label: 'School'),
        ],
      ),
    );
  }

  int _calculateSelectedIndex(BuildContext context) {
    final String location = GoRouter.of(context).location;
    if (location.startsWith('/a')) {
      return 0;
    }
    if (location.startsWith('/b')) {
      return 1;
    }
    if (location.startsWith('/c')) {
      return 2;
    }
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0:
        context.go('/a');
        break;
      case 1:
        context.go('/b');
        break;
      case 2:
        context.go('/c');
        break;
    }
  }
}

// Placeholder Screens for ShellRoute
class ScreenA extends StatelessWidget {
  const ScreenA({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen A')),
      body: Center(child: Text('Content for Screen A')),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen B')),
      body: Center(child: Text('Content for Screen B')),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Screen C')),
      body: Center(child: Text('Content for Screen C')),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Login Screen'),
            ElevatedButton(
              onPressed: () => context.go('/a'), // Después del login, ir a Screen A
              child: const Text('Log In'),
            ),
          ],
        ),
      ),
    );
  }
}

Este setup permite que la BottomNavigationBar persista y mantenga su estado al cambiar entre las rutas /a, /b, /c.

Estructura GoRouter Ruta: '/login' LoginScreen ShellRoute ScaffoldWithNavBar Ruta: '/a' ScreenA Ruta: '/b' ScreenB Ruta: '/c' ScreenC
💡 Consejo: Usa `GoRouter.of(context).location` para obtener la URL actual y determinar qué ítem de la barra de navegación debe estar seleccionado.

🔄 Redirecciones y Guardias de Ruta

Las redirecciones son esenciales para controlar el flujo de navegación, por ejemplo, para redirigir a un usuario no autenticado a la pantalla de inicio de sesión o para redirigir rutas antiguas a nuevas.

1. redirect a Nivel Global

Puedes definir una función redirect a nivel de GoRouter que se ejecuta antes de cualquier navegación. Esto es ideal para la lógica de autenticación.

// ... (Imports y pantallas placeholder)

// Simulamos un servicio de autenticación
class AuthService extends ChangeNotifier {
  bool _isLoggedIn = false;
  bool get isLoggedIn => _isLoggedIn;

  void login() {
    _isLoggedIn = true;
    notifyListeners();
  }

  void logout() {
    _isLoggedIn = false;
    notifyListeners();
  }
}

final AuthService _authService = AuthService();

// Definimos nuestro enrutador con redirección
final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      // Podemos anidar rutas con protección
      routes: [
        GoRoute(
          path: 'dashboard',
          builder: (context, state) => const DashboardScreen(),
        ),
      ],
    ),
    // ... (otras rutas como /users/:userId, etc.)
  ],
  // La función de redirección global
  redirect: (BuildContext context, GoRouterState state) {
    final bool loggedIn = _authService.isLoggedIn;
    final bool goingToLogin = state.matchedLocation == '/login';

    // Si no está logueado y no va a la pantalla de login, redirige a login
    if (!loggedIn && !goingToLogin) return '/login';
    // Si está logueado y va a la pantalla de login, redirige a home
    if (loggedIn && goingToLogin) return '/';

    // No redirige
    return null;
  },
  // Escuchar cambios en el estado de autenticación para que GoRouter reevalúe las rutas
  refreshListenable: _authService,
  // ... (errorBuilder)
);

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'GoRouter Auth Example',
      routerConfig: _router,
    );
  }
}

// ... (HomeScreen, LoginScreen adaptadas para usar _authService)

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home Screen')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to the Home Screen!'),
            ElevatedButton(
              onPressed: () => context.go('/dashboard'),
              child: const Text('Go to Dashboard'),
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: () => _authService.logout(),
              child: const Text('Log Out'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Please log in.'),
            ElevatedButton(
              onPressed: () => _authService.login(),
              child: const Text('Log In'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dashboard')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to the Dashboard! (Protected Route)'),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('Go Home'),
            ),
          ],
        ),
      ),
    );
  }
}

La propiedad refreshListenable es crucial aquí. Cuando _authService notifica a sus oyentes (notifyListeners()), GoRouter reevalúa la función redirect, permitiendo que la navegación se ajuste dinámicamente al estado de autenticación del usuario.

2. redirect a Nivel de GoRoute

También puedes definir un redirect específico para una GoRoute individual. Esto es útil para reglas de redirección más localizadas.

GoRoute(
  path: '/old-path',
  redirect: (context, state) => '/new-path',
),
GoRoute(
  path: '/new-path',
  builder: (context, state) => const NewScreen(),
),

Este ejemplo redirige automáticamente cualquier intento de acceder a /old-path hacia /new-path.


✨ Animaciones y Transiciones Personalizadas

GoRouter te permite personalizar las animaciones de transición entre pantallas utilizando PageBuilder o transitionsBuilder.

1. Usando PageBuilder

PageBuilder te da control total sobre la página que se construye. Puedes usar páginas predefinidas como MaterialPage, CupertinoPage, o crear tus propias páginas personalizadas.

GoRoute(
  path: '/fade',
  pageBuilder: (context, state) => CustomTransitionPage<void>(
    key: state.pageKey,
    child: const FadeTransitionScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // Implementa una transición de fundido
      return FadeTransition(opacity: animation, child: child);
    },
  ),
),

2. Usando transitionsBuilder (más simple)

Para transiciones más sencillas, puedes usar transitionsBuilder directamente dentro de GoRoute si no necesitas un control tan granular sobre la página.

GoRoute(
  path: '/slide',
  pageBuilder: (context, state) => MaterialPage(
    key: state.pageKey,
    child: const SlideTransitionScreen(),
  ),
  transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    const begin = Offset(1.0, 0.0); // Desliza desde la derecha
    const end = Offset.zero;
    const curve = Curves.ease;

    var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

    return SlideTransition(
      position: animation.drive(tween),
      child: child,
    );
  },
),

Ejemplo de Pantallas para Transiciones:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Fade Transition')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('This screen fades in.'),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Slide Transition')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('This screen slides in.'),
            ElevatedButton(
              onPressed: () => context.pop(),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}

Para usar estas transiciones, solo necesitas navegar a /fade o /slide desde otra pantalla.


🔄 Resumen de Métodos de Navegación de GoRouter

Aquí tienes un resumen de los métodos clave que GoRouter ofrece para la navegación:

MétodoDescripciónUso Común
context.go(path)Navega a la ruta especificada, reemplazando toda la pila de navegación. Ideal para rutas principales.Cambiar de una sección a otra (ej. /home, /settings).
context.push(path)Añade una nueva ruta a la pila de navegación. Puedes volver a la pantalla anterior con pop().Navegar a detalles de un ítem (ej. /product/123).
context.replace(path)Reemplaza la ruta actual en la pila por una nueva, sin añadirla a la pila.Después de un login exitoso, reemplazar /login con /home.
context.pop()Elimina la ruta superior de la pila. Similar a Navigator.pop(context).Volver a la pantalla anterior.
context.goNamed(name)Navega a una ruta por su nombre. Útil para rutas complejas con parámetros.context.goNamed('userDetails', pathParameters: {'id': '123'}).
context.pushNamed(name)Añade una ruta nombrada a la pila.context.pushNamed('productDetails', pathParameters: {'id': 'ABC'}).
📌 Nota: Cuando usas `goNamed` o `pushNamed`, necesitas definir un `name` para tu `GoRoute`:
GoRoute(
  path: '/users/:userId',
  name: 'userDetails',
  builder: (context, state) => UserProfileScreen(userId: state.pathParameters['userId']!),
),

✅ Buenas Prácticas y Consejos Avanzados

Para construir aplicaciones robustas con GoRouter, considera estas buenas prácticas:

  • Centralizar la configuración de rutas: Mantén todas tus definiciones de GoRoute en un archivo separado (ej. app_router.dart) para facilitar la gestión y el mantenimiento.
  • Usar rutas nombradas: Aunque puedes usar rutas basadas en path, las rutas nombradas son menos propensas a errores de escritura y más fáciles de refactorizar, especialmente al pasar parámetros.
  • Integración con gestión de estado: Si usas un patrón como BLoC, Provider, o Riverpod, asegúrate de que tu refreshListenable en GoRouter esté conectado a tu lógica de autenticación o estado global para reacciones dinámicas.
  • Pruebas: GoRouter es altamente testeable. Puedes probar tus rutas y redirecciones de manera aislada.
  • Manejo de errores: No olvides implementar un errorBuilder para manejar rutas no encontradas (404) de forma elegante.
  • Considera redirectLimit: Para evitar bucles infinitos en redirecciones complejas, GoRouter tiene un redirectLimit (por defecto, 5). Si tus redirecciones superan este límite, la aplicación lanzará un error.

Ejemplo de Rutas Centralizadas (app_router.dart)

// app_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import 'auth_service.dart'; // Tu servicio de autenticación
import 'screens/home_screen.dart';
import 'screens/details_screen.dart';
import 'screens/user_profile_screen.dart';
import 'screens/product_details_screen.dart';
import 'screens/search_results_screen.dart';
import 'screens/login_screen.dart';
import 'screens/dashboard_screen.dart';
import 'screens/error_screen.dart';
// ... otras pantallas

// Un servicio de autenticación (simulado)
final AuthService _authService = AuthService();

// Definimos un GlobalKey para el Navigator de nuestro ShellRoute (si lo usas)
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

GoRouter createRouter() => GoRouter(
      navigatorKey: _rootNavigatorKey,
      initialLocation: '/',
      routes: <RouteBase>[
        GoRoute(
          path: '/login',
          name: 'login',
          builder: (BuildContext context, GoRouterState state) => const LoginScreen(),
        ),
        ShellRoute(
          navigatorKey: _shellNavigatorKey, // Asignamos la clave al ShellRoute
          builder: (BuildContext context, GoRouterState state, Widget child) {
            // Aquí podrías envolver el child con tu ScaffoldWithNavBar
            return ScaffoldWithNavBar(child: child);
          },
          routes: [
            GoRoute(
              path: '/',
              name: 'home',
              builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
              routes: <RouteBase>[
                GoRoute(
                  path: 'dashboard',
                  name: 'dashboard',
                  builder: (BuildContext context, GoRouterState state) => const DashboardScreen(),
                ),
              ],
            ),
            GoRoute(
              path: '/details',
              name: 'details',
              builder: (BuildContext context, GoRouterState state) => const DetailsScreen(),
            ),
            GoRoute(
              path: '/users/:userId',
              name: 'userDetails',
              builder: (BuildContext context, GoRouterState state) {
                final String userId = state.pathParameters['userId']!;
                return UserProfileScreen(userId: userId);
              },
            ),
            GoRoute(
              path: '/products/:productId/details',
              name: 'productDetails',
              builder: (BuildContext context, GoRouterState state) {
                final String productId = state.pathParameters['productId']!;
                return ProductDetailsScreen(productId: productId);
              },
            ),
            GoRoute(
              path: '/search',
              name: 'searchResults',
              builder: (BuildContext context, GoRouterState state) {
                final String? query = state.uri.queryParameters['q'];
                final String? category = state.uri.queryParameters['cat'];
                return SearchResultsScreen(query: query, category: category);
              },
            ),
          ],
        ),
      ],
      redirect: (BuildContext context, GoRouterState state) {
        final bool loggedIn = _authService.isLoggedIn;
        final bool goingToLogin = state.matchedLocation == '/login';

        if (!loggedIn && !goingToLogin) return '/login';
        if (loggedIn && goingToLogin) return '/';

        return null;
      },
      refreshListenable: _authService,
      errorBuilder: (context, state) => ErrorScreen(error: state.error.toString()),
    );

// Ahora, en main.dart:
// final GoRouter _router = createRouter();

🏁 Conclusión

GoRouter es una herramienta increíblemente poderosa para gestionar la navegación en aplicaciones Flutter, especialmente a medida que crecen en complejidad. Desde rutas dinámicas y Deep Linking hasta ShellRoutes para layouts persistentes y la gestión de redirecciones, GoRouter ofrece una solución declarativa y robusta que simplifica el desarrollo de la interfaz de usuario.

Al dominar GoRouter, estarás equipando tus aplicaciones Flutter con una experiencia de navegación fluida, intuitiva y profesional, lista para escalar y satisfacer las demandas de cualquier proyecto.

Explora la documentación oficial de GoRouter para profundizar en más funcionalidades y casos de uso.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!