Nawigacja Flutter 2.0 i GoRouter w 2026: Deep Linking i pytania rekrutacyjne

Opanuj nawigację Flutter z GoRouter 17.5: routing deklaratywny, deep linking, ShellRoute, guardy routingu oraz pytania rekrutacyjne z praktycznymi przykładami.

Architektura nawigacji Flutter z diagramem przepływu deep linkingu GoRouter

Nawigacja we Flutterze przeszła znaczącą ewolucję od wprowadzenia Navigation 2.0. GoRouter 17.5, oficjalna paczka zespołu Flutter, obsługuje złożoność Router API, zapewniając jednocześnie deklaratywny routing, automatyczny deep linking oraz zagnieżdżoną nawigację od razu po instalacji.

GoRouter jest kompletny funkcjonalnie

Zespół Flutter uznaje GoRouter za funkcjonalnie kompletny od 2026 roku. Obsługuje parametry ścieżki, parametry zapytań, przekierowania, ShellRoute dla persystentnego UI oraz StatefulShellRoute do zachowania stanu zakładek. Ręczne pisanie czystego Navigator 2.0 praktycznie nie występuje już w produkcji.

Architektura Navigator 2.0

Navigator 2.0 wprowadził deklaratywne, URL-owe podejście do nawigacji Flutter. Zamiast imperatywnych wywołań push i pop, stan aplikacji determinuje, co pojawia się na stosie nawigacji. Architektura opiera się na trzech kluczowych klasach:

  • Router: Widget najwyższego poziomu koordynujący nawigację
  • RouteInformationParser: Przekształca URL-e na stan aplikacji
  • RouterDelegate: Buduje drzewo widgetów na podstawie tego stanu

Gdy deep link przychodzi z systemu operacyjnego lub URL zmienia się w przeglądarce, framework aktualizuje stan, a UI reaguje. Problem polega na tym, że ręczne pisanie własnego RouterDelegate i RouteInformationParser wymaga znacznej ilości boilerplate'u.

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();
  }
}

Ta rozwlekłość jest powodem, dla którego powstał GoRouter. Opakowuje on Navigator 2.0 i obsługuje cały boilerplate.

Konfiguracja GoRouter 17.5

GoRouter 17.5 wprowadza wsparcie dla metadanych routów oraz ograniczenia wyrażeń regularnych dla parametrów ścieżki. Minimalne wymagania to Flutter 3.32 i 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);
          },
        ),
      ],
    ),
  ],
);

Router integruje się z 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',
    );
  }
}

Konfiguracja Deep Linkingu dla Androida i iOS

Deep linking pozwala zewnętrznym URL-om otwierać konkretne ekrany w aplikacji. GoRouter obsługuje routing automatycznie po skonfigurowaniu platformy.

Android wymaga intent-filter w AndroidManifest.xml oraz pliku Digital Asset Links hostowanego na domenie:

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>

Plik assetlinks.json musi być serwowany pod adresem https://example.com/.well-known/assetlinks.json.

Dla iOS należy włączyć Associated Domains w Xcode oraz hostować plik apple-app-site-association:

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

GoRouter 17.5 naprawił krytyczny błąd, w którym deep linki Android cold-start z pustymi ścieżkami traciły scheme i authority. Oznacza to, że deep linki działają teraz niezawodnie nawet gdy aplikacja nie jest uruchomiona.

Testowanie deep linków

Deep linki na Androidzie testuje się poleceniem adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123". Na iOS należy użyć xcrun simctl openurl booted "https://example.com/products/123".

ShellRoute dla persystentnego UI nawigacji

ShellRoute opakowuje routing podrzędny persystentnym elementem UI, takim jak BottomNavigationBar lub Drawer. Shell pozostaje widoczny podczas nawigacji między dziećmi.

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 naprawił problem, w którym gesty cofania na iOS usuwały cały ShellRoute zamiast tylko aktywnego sub-route'a.

Gotowy na rozmowy o Flutter?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

StatefulShellRoute do zachowania stanu zakładek

Gdy użytkownicy przełączają zakładki, ShellRoute przebudowuje dziecko. StatefulShellRoute zachowuje stos nawigacji każdej gałęzi:

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(),
            ),
          ],
        ),
      ],
    ),
  ],
);

Z tą konfiguracją, jeśli użytkownik przejdzie do /home/details/42, przełączy się na zakładkę Explore, a następnie wróci do Home, nadal będzie na ekranie szczegółów.

Route Guards z przekierowaniami

Przekierowania obsługują sprawdzanie uwierzytelniania i warunkową nawigację. Callback redirect wykonuje się przed każdą nawigacją:

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 dodał wsparcie dla użycia metod rozszerzających kontekst, takich jak context.namedLocation(), wewnątrz callbacków redirect.

Typowe pytania rekrutacyjne z nawigacji Flutter

Rekruterzy często testują wiedzę o nawigacji, ponieważ dotyka ona architektury, zarządzania stanem i integracji z platformą. Oto pytania, które wyróżniają doświadczonych kandydatów:

P: Jaki problem rozwiązuje Navigator 2.0, którego Navigator 1.0 nie rozwiązywał?

Navigator 1.0 używa imperatywnej nawigacji: Navigator.push() i Navigator.pop(). Aplikacja nie może reprezentować stanu nawigacji jako URL, co utrudnia deep linking i wsparcie dla weba. Navigator 2.0 jest deklaratywny: stan aplikacji determinuje stos nawigacji, URL-e są obywatelami pierwszej klasy, a deep linki działają automatycznie.

P: Kiedy użyć context.go() vs context.push()?

context.go('/path') zastępuje obecny stos nawigacji aż do dopasowanego route'a. context.push('/path') dodaje nowy route na szczycie istniejącego stosu. go() stosuje się do nawigacji najwyższego poziomu (przełączanie zakładek, powrót do strony głównej), a push() do drążenia w szczegóły przy zachowaniu nawigacji wstecz.

P: Jak zachować stan między przełączaniem zakładek?

Należy użyć StatefulShellRoute.indexedStack(). Utrzymuje on oddzielne stosy nawigacji dla każdej gałęzi i zachowuje je przy przełączaniu zakładek. Bez tego przełączanie zakładek przebudowuje całe poddrzewo.

P: Jak przekazywać złożone obiekty między routami?

Istnieją trzy podejścia. Po pierwsze, przekazanie ID w parametrze ścieżki i pobranie obiektu na ekranie docelowym. Po drugie, użycie parametru extra GoRoutera do bezpośredniego przekazania obiektu. Po trzecie, przechowywanie obiektu w rozwiązaniu do zarządzania stanem (Riverpod, Bloc) i dostęp do niego z miejsca docelowego. Pierwsze podejście jest najlepsze dla deep linkingu, ponieważ URL pozostaje udostępnialny.

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: Czym jest callback onEnter w GoRouter 17.5?

Callback onEnter wykonuje się podczas wchodzenia na route i zapewnia dostęp zarówno do obecnego, jak i następnego stanu route'a. Pozwala uruchomić logikę przed pełnym załadowaniem route'a, co jest przydatne dla analityki lub warunkowego pre-loadingu.

Bezpieczne typy routów z generowaniem kodu

GoRouter wspiera routing bezpieczny typowo poprzez generowanie kodu. Definiuje się klasy danych routów, a build_runner generuje kod nawigacji:

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);
}

Po uruchomieniu dart run build_runner build można nawigować z bezpieczeństwem na etapie kompilacji:

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

Eliminuje to literówki w ścieżkach routów i zapewnia, że parametry są zawsze podane.

Opanowanie nawigacji Flutter w aplikacjach produkcyjnych

  • GoRouter 17.5 jest zalecanym rozwiązaniem nawigacyjnym dla aplikacji Flutter wymagających deep linkingu, zagnieżdżonej nawigacji lub wsparcia webowego
  • ShellRoute służy do persystentnych dolnych pasków nawigacji; StatefulShellRoute zachowuje stos nawigacji każdej zakładki
  • Konfiguracja deep linkingu specyficzna dla platformy (Android App Links, iOS Universal Links) jest oddzielona od GoRoutera, który obsługuje logikę routingu
  • Guardy uwierzytelniania implementuje się z callbackiem redirect na poziomie routera lub pojedynczego route'a
  • W scenariuszach rekrutacyjnych należy skupić się na wyjaśnieniu modelu deklaratywnego: stan aplikacji napędza UI, URL-e wywodzą się ze stanu, a GoRouter obsługuje translację
  • Bezpieczne typowo routy z generowaniem kodu eliminują błędy runtime z błędnie wpisanych ścieżek
  • Warto sprawdzić dokumentację nawigacji Flutter oraz changelog GoRoutera dla funkcji specyficznych dla wersji

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Flutter?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 23 sierpnia 2026

Tagi

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

Udostępnij

Powiązane artykuły