← Назад к вопросам

Расскажи про опыт работы с навигацией

1.0 Junior🔥 181 комментариев
#Навигация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Опыт работы с навигацией в Flutter

Навигация — это один из самых важных и сложных аспектов мобильной разработки. За годы я прошёл эволюцию от простого Navigator к современным решениям.

История эволюции навигации

2015-2017: Простой Navigator
  └─ push/pop, очень базовый

2017-2019: Именованные routes
  └─ named routes, более структурированный

2019-2021: Deep Linking
  └─ Поддержка ссылок из браузера

2021+: GetX / Go Router
  └─ Декларативная, более мощная

Уровень 1: Простой Navigator (начало)

// ❌ Базовый подход (но работает)
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Простой pushes
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => DetailScreen(),
              ),
            );
          },
          child: Text('Go to Details'),
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details'),
        leading: BackButton(),
      ),
      body: Center(child: Text('Detail Page')),
    );
  }
}

Проблемы:

  • Сложно читать навигацию
  • Нелегко отследить историю
  • Deep linking не поддерживается

Уровень 2: Named Routes (лучше)

// ✅ Более структурированный подход
final routes = {
  '/': (context) => HomeScreen(),
  '/details': (context) => DetailScreen(),
  '/settings': (context) => SettingsScreen(),
  '/profile/:id': (context) => ProfileScreen(
    userId: ModalRoute.of(context)!.settings.arguments as int,
  ),
};

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: routes,
      initialRoute: '/',
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Используем имя маршрута
            Navigator.pushNamed(
              context,
              '/profile/123',
            );
          },
          child: Text('View Profile'),
        ),
      ),
    );
  }
}

Преимущества:

  • Централизованная конфигурация
  • Легче следить за маршрутами
  • Параметры маршрута явные

Уровень 3: Deep Linking (production-ready)

// ✅ С поддержкой deep links

final route = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomeScreen(),
      routes: [
        GoRoute(
          path: 'profile/:id',
          builder: (context, state) => ProfileScreen(
            userId: state.params['id']!,
          ),
        ),
        GoRoute(
          path: 'details',
          builder: (context, state) => DetailScreen(),
        ),
      ],
    ),
  ],
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: route,
    );
  }
}

// Android: AndroidManifest.xml
/*
<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="https"
        android:host="myapp.com"
        android:pathPrefix="/profile" />
</intent-filter>
*/

// iOS: Info.plist
/*
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>
*/

Уровень 4: GetX (мощный и простой)

// ✅ GetX навигация (очень популярна)

final routes = [
  GetPage(
    name: '/',
    page: () => HomeScreen(),
  ),
  GetPage(
    name: '/profile/:id',
    page: () => ProfileScreen(),
  ),
  GetPage(
    name: '/settings',
    page: () => SettingsScreen(),
    transition: Transition.cupertino,  # iOS transition
  ),
];

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      getPages: routes,
      initialRoute: '/',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () {
              // Простая навигация
              Get.toNamed('/profile/123');
            },
            child: Text('Go to Profile'),
          ),
          ElevatedButton(
            onPressed: () {
              // С аргументами
              Get.toNamed('/settings', arguments: {'theme': 'dark'});
            },
            child: Text('Settings'),
          ),
          ElevatedButton(
            onPressed: () {
              // Замена маршрута (очистить историю)
              Get.offNamed('/details');
            },
            child: Text('Replace route'),
          ),
        ],
      ),
    );
  }
}

class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userId = Get.parameters['id']!;  # Доступ к параметрам
    
    return Scaffold(
      appBar: AppBar(
        title: Text('Profile $userId'),
        leading: BackButton(
          onPressed: () => Get.back(),  # Вернуться
        ),
      ),
      body: Center(child: Text('Profile of user $userId')),
    );
  }
}

Сложная навигация: Bottom Navigation

// ❌ Неправильно: Создаёт новый стек для каждой вкладки
class BadBottomNav extends StatefulWidget {
  @override
  State<BadBottomNav> createState() => _BadBottomNavState();
}

class _BadBottomNavState extends State<BadBottomNav> {
  int _selectedIndex = 0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _selectedIndex == 0
        ? HomeScreen()
        : _selectedIndex == 1
        ? SearchScreen()
        : ProfileScreen(),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        items: [...],
      ),
    );
  }
}

// ✅ Правильно: Сохраняет стек для каждой вкладки
class GoodBottomNav extends StatefulWidget {
  @override
  State<GoodBottomNav> createState() => _GoodBottomNavState();
}

class _GoodBottomNavState extends State<GoodBottomNav> {
  int _selectedIndex = 0;
  final _navigatorKeys = [
    GlobalKey<NavigatorState>(),  # Home
    GlobalKey<NavigatorState>(),  # Search
    GlobalKey<NavigatorState>(),  # Profile
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: [
          Navigator(
            key: _navigatorKeys[0],
            onGenerateRoute: _homeRoutes,
          ),
          Navigator(
            key: _navigatorKeys[1],
            onGenerateRoute: _searchRoutes,
          ),
          Navigator(
            key: _navigatorKeys[2],
            onGenerateRoute: _profileRoutes,
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: (index) {
          if (_selectedIndex == index) {
            # Двойной тап = вернись на верх стека
            _navigatorKeys[index].currentState?.popUntil(
              (route) => route.isFirst,
            );
          } else {
            setState(() => _selectedIndex = index);
          }
        },
        items: [...],
      ),
    );
  }
}

Реальный пример: Навигация в банковском приложении

// Bank App навигация (сложная!)

class BankingNavigation {
  // Разные стеки навигации для разных модулей
  final _homeNavigator = GlobalKey<NavigatorState>();
  final _transactionsNavigator = GlobalKey<NavigatorState>();
  final _transferNavigator = GlobalKey<NavigatorState>();
  final _settingsNavigator = GlobalKey<NavigatorState>();
  
  Widget buildApp() {
    return MaterialApp(
      home: BankingShell(),
    );
  }
}

class BankingShell extends StatefulWidget {
  @override
  State<BankingShell> createState() => _BankingShellState();
}

class _BankingShellState extends State<BankingShell> {
  int _selectedTab = 0;
  
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // Специальная логика для back button
        return !await navigatorKeys[_selectedTab].currentState!.maybePop();
      },
      child: Scaffold(
        body: IndexedStack(
          index: _selectedTab,
          children: [
            _homeTab(),
            _transactionsTab(),
            _transferTab(),
            _settingsTab(),
          ],
        ),
        bottomNavigationBar: BottomNavigationBar(
          type: BottomNavigationBarType.fixed,
          currentIndex: _selectedTab,
          onTap: (index) {
            setState(() => _selectedTab = index);
          },
          items: [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.history),
              label: 'Transactions',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.send),
              label: 'Transfer',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Settings',
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _homeTab() => Navigator(
    key: navigatorKeys[0],
    onGenerateRoute: (settings) {
      return MaterialPageRoute(
        builder: (context) => HomeScreen(),
      );
    },
  );
  
  // Other tabs similar...
}

Проблемы которые я решал

Проблема 1: Back button в nested navigation

// Решение: WillPopScope
class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // Твоя логика для back button
        print('Back button pressed');
        return true;  # Продолжи стандартное поведение
      },
      child: Scaffold(...),
    );
  }
}

Проблема 2: Deep link with authentication

// Решение: Redirect после login
GoRouter _buildRouter() {
  return GoRouter(
    redirect: (context, state) {
      final isLoggedIn = Get.find<AuthService>().isLoggedIn;
      
      if (!isLoggedIn) {
        return '/login';  # Перенаправи на логин
      }
      
      return state.location;  # Продолжи к целевому маршруту
    },
    routes: [...],
  );
}

Проблема 3: Сохранение состояния при навигации

// Решение: AutomaticKeepAliveClientMixin
class HomeScreen extends StatefulWidget {
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen>
    with AutomaticKeepAliveClientMixin {
  
  @override
  bool get wantKeepAlive => true;  # Сохрани состояние
  
  @override
  Widget build(BuildContext context) {
    super.build(context);  # ВАЖНО!
    return Scaffold(...);  # Состояние сохранится
  }
}

Резюме

Навигация в Flutter: эволюция от простого к сложному

  1. Navigator — базовый, для простых приложений
  2. Named Routes — лучше структурирован
  3. GoRouter / GetX — production-ready
  4. Deep Linking — необходимо для modern apps
  5. Nested Navigation — для сложных приложений

Best Practice:

  • ✅ Централизуй конфигурацию маршрутов
  • ✅ Используй типизированные параметры
  • ✅ Обрабатывай back button правильно
  • ✅ Сохраняй состояние при навигации
  • ✅ Поддерживай deep linking

Инструменты:

  • GoRouter (официальное, современное)
  • GetX (популярное, мощное)
  • Navigation 2.0 (низкоуровневое)

Моя рекомендация: Используй GoRouter для новых проектов. Это стандарт Google и будущее навигации в Flutter.