Flutter Navigation 2.0 та GoRouter у 2026: Deep Linking та питання співбесід

Опанування навігації Flutter з GoRouter 17.5: декларативний роутинг, deep linking, ShellRoute, guard'и маршрутів та питання співбесід з практичними прикладами.

Архітектура навігації Flutter з діаграмою потоку deep linking GoRouter

Навігація у Flutter зазнала значної еволюції з моменту впровадження Navigation 2.0. GoRouter 17.5, офіційний пакет команди Flutter, тепер обробляє складність Router API, надаючи декларативний роутинг, автоматичний deep linking та вкладену навігацію одразу після встановлення.

GoRouter є функціонально завершеним

Команда Flutter вважає GoRouter функціонально завершеним станом на 2026 рік. Він підтримує параметри шляху, параметри запитів, перенаправлення, ShellRoute для персистентного UI та StatefulShellRoute для збереження стану вкладок. Написання чистого Navigator 2.0 вручну практично не зустрічається у продакшені.

Архітектура Navigator 2.0

Navigator 2.0 запровадив декларативний, URL-орієнтований підхід до навігації Flutter. Замість імперативних викликів push та pop, стан додатку визначає, що з'являється на стеку навігації. Архітектура спирається на три ключові класи:

  • Router: Віджет верхнього рівня, що координує навігацію
  • RouteInformationParser: Перетворює URL-адреси на стан додатку
  • RouterDelegate: Будує дерево віджетів на основі цього стану

Коли deep link надходить від операційної системи або URL змінюється у веб-браузері, фреймворк оновлює стан, і UI реагує. Проблема полягає в тому, що написання власного RouterDelegate та RouteInformationParser вручну вимагає значної кількості шаблонного коду.

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

Ця багатослівність є причиною існування GoRouter. Він обгортає Navigator 2.0 та обробляє весь шаблонний код.

Налаштування GoRouter 17.5

GoRouter 17.5 впроваджує підтримку метаданих маршрутів та обмеження регулярних виразів для параметрів шляху. Мінімальні вимоги — Flutter 3.32 та 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);
          },
        ),
      ],
    ),
  ],
);

Роутер інтегрується з 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',
    );
  }
}

Налаштування Deep Linking для Android та iOS

Deep linking дозволяє зовнішнім URL-адресам відкривати конкретні екрани у додатку. GoRouter обробляє роутинг автоматично після налаштування платформи.

Android вимагає intent-filter у AndroidManifest.xml та файл Digital Asset Links, розміщений на домені:

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>

Файл assetlinks.json повинен бути доступний за адресою https://example.com/.well-known/assetlinks.json.

Для iOS необхідно увімкнути Associated Domains у Xcode та розмістити файл 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 виправив критичну помилку, коли deep links Android cold-start з порожніми шляхами втрачали scheme та authority. Це означає, що deep links тепер працюють надійно навіть коли додаток не запущено.

Тестування deep links

Deep links на Android тестуються командою adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123". Для iOS використовується xcrun simctl openurl booted "https://example.com/products/123".

ShellRoute для персистентного UI навігації

ShellRoute обгортає дочірні маршрути персистентним елементом UI, таким як BottomNavigationBar або Drawer. Shell залишається видимим під час навігації між дочірніми елементами.

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 виправив проблему, коли жести назад на iOS видаляли весь ShellRoute замість лише активного підмаршруту.

Готовий до співбесід з Flutter?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

StatefulShellRoute для збереження стану вкладок

Коли користувачі перемикають вкладки, ShellRoute перебудовує дочірній елемент. StatefulShellRoute зберігає стек навігації кожної гілки:

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

З цією конфігурацією, якщо користувач переходить до /home/details/42, перемикається на вкладку Explore, а потім повертається до Home, він все ще буде на екрані деталей.

Route Guards з перенаправленнями

Перенаправлення обробляють перевірки автентифікації та умовну навігацію. Callback redirect виконується перед кожною навігацією:

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 додав підтримку використання методів розширення контексту, таких як context.namedLocation(), всередині callback'ів redirect.

Типові питання співбесід з навігації Flutter

Інтерв'юери часто перевіряють знання навігації, оскільки ця тема охоплює архітектуру, керування станом та інтеграцію з платформою. Ось питання, що вирізняють досвідчених кандидатів:

П: Яку проблему вирішує Navigator 2.0, яку не вирішував Navigator 1.0?

Navigator 1.0 використовує імперативну навігацію: Navigator.push() та Navigator.pop(). Додаток не може представити стан навігації як URL, що ускладнює deep linking та підтримку вебу. Navigator 2.0 є декларативним: стан додатку визначає стек навігації, URL-адреси є повноцінними сутностями, а deep links працюють автоматично.

П: Коли використовувати context.go() проти context.push()?

context.go('/path') замінює поточний стек навігації до відповідного маршруту. context.push('/path') додає новий маршрут на вершину існуючого стеку. go() використовується для навігації верхнього рівня (перемикання вкладок, перехід на головну), а push() — для заглиблення в деталі зі збереженням навігації назад.

П: Як зберегти стан між перемиканням вкладок?

Використовується StatefulShellRoute.indexedStack(). Він підтримує окремі стеки навігації для кожної гілки та зберігає їх при перемиканні вкладок. Без нього перемикання вкладок перебудовує все піддерево.

П: Як передавати складні об'єкти між маршрутами?

Існує три підходи. Перший — передати ID у параметрі шляху та отримати об'єкт на цільовому екрані. Другий — використати параметр extra GoRouter для безпосередньої передачі об'єкта. Третій — зберігати об'єкт у рішенні для керування станом (Riverpod, Bloc) та отримувати доступ до нього з місця призначення. Перший підхід найкращий для deep linking, оскільки URL залишається придатним для поширення.

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

П: Що таке callback onEnter у GoRouter 17.5?

Callback onEnter виконується при вході на маршрут і надає доступ як до поточного, так і до наступного станів маршруту. Він дозволяє виконувати логіку перед повним завантаженням маршруту, що корисно для аналітики або умовного попереднього завантаження.

Типобезпечні маршрути з генерацією коду

GoRouter підтримує типобезпечний роутинг через генерацію коду. Визначаються класи даних маршрутів, а build_runner генерує код навігації:

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

Після виконання dart run build_runner build можна навігувати з безпекою на етапі компіляції:

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

Це усуває друкарські помилки у шляхах маршрутів та гарантує, що параметри завжди надані.

Опанування навігації Flutter для продакшн-додатків

  • GoRouter 17.5 є рекомендованим рішенням для навігації у Flutter-додатках, що потребують deep linking, вкладеної навігації або підтримки вебу
  • ShellRoute використовується для персистентних нижніх панелей навігації; StatefulShellRoute зберігає стек навігації кожної вкладки
  • Платформо-специфічна конфігурація deep linking (Android App Links, iOS Universal Links) налаштовується окремо від GoRouter, який обробляє логіку роутингу
  • Guard'и автентифікації реалізуються за допомогою callback'у redirect на рівні роутера або окремого маршруту
  • У сценаріях співбесід слід зосередитися на поясненні декларативної моделі: стан додатку керує UI, URL-адреси виводяться зі стану, а GoRouter обробляє трансляцію
  • Типобезпечні маршрути з генерацією коду усувають runtime-помилки від неправильно введених шляхів
  • Варто перевірити документацію навігації Flutter та changelog GoRouter для функцій, специфічних для версії

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Щоденний виклик

Чи знайдеш ти помилку в Flutter?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 23 серпня 2026 р.

Теги

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

Поділитися

Пов'язані статті