Flutter之导航和路由

📚 目录

  1. 核心概念
  2. 基本导航
  3. 命名路由
  4. 路由传参
  5. 返回数据
  6. 路由生成器
  7. 高级路由(Router API)
  8. 导航栏和抽屉
  9. 深层链接(Deep Linking)
  10. 最佳实践
  11. 常见问题

核心概念

什么是导航和路由?

  • 路由(Route):表示应用中的一个页面或屏幕。在 Flutter 中,每个页面都是一个 Widget。
  • 导航器(Navigator):管理路由堆栈的组件,负责在路由之间进行跳转和管理。
  • 路由堆栈(Route Stack):类似浏览器的历史记录,使用后进先出(LIFO)的方式管理页面。

导航的基本原理

路由堆栈(从下到上):
┌─────────────┐
│  页面 C     │ ← 当前页面(栈顶)
├─────────────┤
│  页面 B     │
├─────────────┤
│  页面 A     │ ← 初始页面(栈底)
└─────────────┘
  • push:将新页面推入堆栈顶部
  • pop:从堆栈顶部移除当前页面
  • replace:替换当前页面
  • popUntil:返回到指定页面

基本导航

1. Navigator.push() - 导航到新页面

使用 Navigator.push() 导航到新页面,这是最常用的导航方式。

// 基本用法
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => SecondScreen(),
  ),
);

// 或者使用 Navigator.of(context)
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => SecondScreen(),
  ),
);

完整示例:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '导航示例',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const FirstScreen(),
    );
  }
}

// 第一个页面
class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('第一页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 导航到第二个页面
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => const SecondScreen(),
              ),
            );
          },
          child: const Text('前往第二页'),
        ),
      ),
    );
  }
}

// 第二个页面
class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('第二页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 返回上一页
            Navigator.pop(context);
          },
          child: const Text('返回'),
        ),
      ),
    );
  }
}

2. Navigator.pop() - 返回上一页

使用 Navigator.pop() 返回上一页。

Navigator.pop(context);

// 可以返回数据
Navigator.pop(context, '返回的数据');

3. MaterialPageRoute vs CupertinoPageRoute

  • MaterialPageRoute:Android 风格的页面过渡动画
  • CupertinoPageRoute:iOS 风格的页面过渡动画
// Material 风格(Android)
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);

// Cupertino 风格(iOS)
Navigator.push(
  context,
  CupertinoPageRoute(builder: (context) => SecondScreen()),
);

4. 自定义页面过渡动画

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 淡入淡出
      return FadeTransition(opacity: animation, child: child);
      
      // 或者滑动
      // return SlideTransition(
      //   position: Tween<Offset>(
      //     begin: const Offset(1.0, 0.0),
      //     end: Offset.zero,
      //   ).animate(animation),
      //   child: child,
      // );
    },
    transitionDuration: const Duration(milliseconds: 300),
  ),
);

命名路由

命名路由可以减少代码重复,特别适合在多个地方导航到同一页面。

1. 定义路由表

MaterialApp 中定义路由表:

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomeScreen(),
    '/details': (context) => const DetailsScreen(),
    '/settings': (context) => const SettingsScreen(),
  },
);

2. 使用命名路由导航

// 导航到命名路由
Navigator.pushNamed(context, '/details');

// 或者使用 pushReplacementNamed 替换当前路由
Navigator.pushReplacementNamed(context, '/details');

3. 完整示例

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由示例',
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/details': (context) => const DetailsScreen(),
        '/settings': (context) => const SettingsScreen(),
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('首页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/details');
              },
              child: const Text('前往详情页'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/settings');
              },
              child: const Text('前往设置页'),
            ),
          ],
        ),
      ),
    );
  }
}

⚠️ 注意事项

虽然命名路由提供了便利,但在处理深层链接和复杂导航需求时存在一定限制。对于大多数现代应用,推荐使用 Router API(见下文)。


路由传参

1. 通过构造函数传参

这是最简单直接的方式:

// 定义接收参数的页面
class DetailsScreen extends StatelessWidget {
  final String title;
  final int id;

  const DetailsScreen({
    super.key,
    required this.title,
    required this.id,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Text('ID: $id'),
      ),
    );
  }
}

// 导航时传递参数
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailsScreen(
      title: '详情',
      id: 123,
    ),
  ),
);

2. 通过命名路由传参

使用 arguments 参数:

// 定义路由时使用 settings.arguments
MaterialApp(
  routes: {
    '/details': (context) {
      final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
      return DetailsScreen(
        title: args['title'],
        id: args['id'],
      );
    },
  },
);

// 导航时传递参数
Navigator.pushNamed(
  context,
  '/details',
  arguments: {
    'title': '详情',
    'id': 123,
  },
);

3. 使用路由生成器传参(推荐)

MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == '/details') {
      final args = settings.arguments as Map<String, dynamic>;
      return MaterialPageRoute(
        builder: (context) => DetailsScreen(
          title: args['title'],
          id: args['id'],
        ),
      );
    }
    return null;
  },
);

返回数据

从新页面返回数据到上一页:

1. 返回数据

// 在第二个页面返回数据
class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('选择选项')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context, '选项A');
              },
              child: const Text('选择选项A'),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context, '选项B');
              },
              child: const Text('选择选项B'),
            ),
          ],
        ),
      ),
    );
  }
}

2. 接收返回的数据

// 在第一个页面接收数据
ElevatedButton(
  onPressed: () async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const SecondScreen()),
    );
    
    if (result != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('选择了: $result')),
      );
    }
  },
  child: const Text('打开选择页面'),
);

3. 完整示例

class FirstScreen extends StatefulWidget {
  const FirstScreen({super.key});

  @override
  State<FirstScreen> createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
  String? _selectedOption;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('第一页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_selectedOption != null)
              Text('已选择: $_selectedOption'),
            ElevatedButton(
              onPressed: () async {
                final result = await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const SecondScreen(),
                  ),
                );
                
                if (result != null) {
                  setState(() {
                    _selectedOption = result as String;
                  });
                }
              },
              child: const Text('选择选项'),
            ),
          ],
        ),
      ),
    );
  }
}

路由生成器

onGenerateRoute 允许动态生成路由,适合处理复杂的路由逻辑。

1. 基本用法

MaterialApp(
  onGenerateRoute: (settings) {
    // 根据路由名称生成不同的页面
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => const HomeScreen());
      case '/details':
        return MaterialPageRoute(builder: (_) => const DetailsScreen());
      case '/settings':
        return MaterialPageRoute(builder: (_) => const SettingsScreen());
      default:
        return MaterialPageRoute(
          builder: (_) => const NotFoundScreen(),
        );
    }
  },
);

2. 处理未知路由

MaterialApp(
  onGenerateRoute: (settings) {
    // 处理已知路由
    if (settings.name == '/details') {
      return MaterialPageRoute(builder: (_) => const DetailsScreen());
    }
    
    // 处理未知路由
    return MaterialPageRoute(
      builder: (_) => Scaffold(
        appBar: AppBar(title: const Text('404')),
        body: const Center(child: Text('页面未找到')),
      ),
    );
  },
  
  // 或者使用 onUnknownRoute
  onUnknownRoute: (settings) {
    return MaterialPageRoute(
      builder: (_) => const NotFoundScreen(),
    );
  },
);

3. 结合参数使用

MaterialApp(
  onGenerateRoute: (settings) {
    final uri = Uri.parse(settings.name ?? '/');
    
    switch (uri.path) {
      case '/details':
        final id = uri.queryParameters['id'];
        return MaterialPageRoute(
          builder: (_) => DetailsScreen(id: id),
        );
      default:
        return MaterialPageRoute(builder: (_) => const HomeScreen());
    }
  },
);

高级路由(Router API)

对于具有复杂导航需求的应用,Flutter 提供了 Router API,特别适用于处理深层链接和 Web URL 同步。

1. 使用 go_router(推荐)

go_router 是一个流行的路由包,简化了 Router 的使用。

安装:

dependencies:
  go_router: ^13.0.0

基本用法:

import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/details/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return DetailsScreen(id: id);
      },
    ),
  ],
);

// 在 MaterialApp 中使用
MaterialApp.router(
  routerConfig: router,
);

导航:

// 导航到新页面
context.go('/details/123');

// 或者使用 push
context.push('/details/123');

// 返回
context.pop();

2. 嵌套路由

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'details/:id',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return DetailsScreen(id: id);
          },
        ),
      ],
    ),
  ],
);

3. 路由守卫

final router = GoRouter(
  redirect: (context, state) {
    // 检查是否登录
    final isLoggedIn = AuthService.isLoggedIn();
    final isGoingToLogin = state.matchedLocation == '/login';
    
    if (!isLoggedIn && !isGoingToLogin) {
      return '/login';
    }
    
    if (isLoggedIn && isGoingToLogin) {
      return '/';
    }
    
    return null; // 不重定向
  },
  routes: [
    // ... 路由定义
  ],
);

导航栏和抽屉

1. BottomNavigationBar - 底部导航栏

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [
    const HomeScreen(),
    const SearchScreen(),
    const ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: '搜索',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

2. Drawer - 侧边抽屉

Scaffold(
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Text('菜单'),
        ),
        ListTile(
          leading: const Icon(Icons.home),
          title: const Text('首页'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/');
          },
        ),
        ListTile(
          leading: const Icon(Icons.settings),
          title: const Text('设置'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/settings');
          },
        ),
      ],
    ),
  ),
  body: const Center(child: Text('内容')),
);

3. TabBar - 标签页导航

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('标签页'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.home), text: '首页'),
              Tab(icon: Icon(Icons.search), text: '搜索'),
              Tab(icon: Icon(Icons.person), text: '我的'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            HomeScreen(),
            SearchScreen(),
            ProfileScreen(),
          ],
        ),
      ),
    );
  }
}

深层链接(Deep Linking)

深层链接允许应用通过特定的 URL 直接打开特定的页面。

1. 使用 go_router 处理深层链接

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProductScreen(id: id);
      },
    ),
  ],
);

2. Android 配置

android/app/src/main/AndroidManifest.xml 中:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop">
    <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" />
    </intent-filter>
</activity>

3. iOS 配置

ios/Runner/Info.plist 中:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

最佳实践

1. 路由管理最佳实践

  • 使用 Router API:对于现代应用,推荐使用 go_router 或类似的包
  • 集中管理路由:将路由定义集中在一个文件中
  • 使用类型安全的路由:避免字符串硬编码
  • 处理错误路由:提供 404 页面
  • 使用路由守卫:保护需要认证的页面

2. 导航最佳实践

  • 使用 await 接收返回数据:确保正确处理异步返回
  • 避免深层嵌套:限制导航堆栈的深度
  • 提供返回按钮:确保用户可以返回
  • 使用 WillPopScope:处理返回按钮的拦截

3. 代码组织

// routes/routes.dart - 集中管理路由
class AppRoutes {
  static const String home = '/';
  static const String details = '/details';
  static const String settings = '/settings';
}

// routes/app_router.dart - 路由配置
final router = GoRouter(
  routes: [
    GoRoute(
      path: AppRoutes.home,
      builder: (context, state) => const HomeScreen(),
    ),
    // ...
  ],
);

4. 拦截返回操作

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

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // 显示确认对话框
        final shouldPop = await showDialog<bool>(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('确认'),
            content: const Text('确定要离开吗?未保存的更改将丢失。'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context, false),
                child: const Text('取消'),
              ),
              TextButton(
                onPressed: () => Navigator.pop(context, true),
                child: const Text('确定'),
              ),
            ],
          ),
        );
        return shouldPop ?? false;
      },
      child: Scaffold(
        appBar: AppBar(title: const Text('编辑')),
        body: const Center(child: Text('编辑内容')),
      ),
    );
  }
}

常见问题

1. Navigator 找不到 context

问题Navigator operation requested with a context that does not include a Navigator

解决:确保 context 来自包含 MaterialApp 或 CupertinoApp 的 Widget 树。

// ❌ 错误
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Builder(
        builder: (context) {
          // 这里的 context 是正确的
          return ElevatedButton(
            onPressed: () {
              Navigator.push(context, ...); // ✅
            },
          );
        },
      ),
    );
  }
}

2. 路由参数类型转换错误

问题type 'String' is not a subtype of type 'int'

解决:确保参数类型匹配,使用类型转换。

// 从路由参数获取时进行类型转换
final id = int.parse(state.pathParameters['id']!);

3. 返回数据为 null

问题:从新页面返回时,上一页接收到的数据为 null

解决:确保使用 await 等待返回结果。

// ✅ 正确
final result = await Navigator.push(...);

// ❌ 错误
final result = Navigator.push(...); // result 可能是 null

4. 路由堆栈过深

问题:导航堆栈过深导致内存问题

解决:使用 pushReplacementpushAndRemoveUntil 替换路由。

// 替换当前路由
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => NewScreen()),
);

// 清除所有路由并导航到新页面
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => NewScreen()),
  (route) => false, // 清除所有路由
);

总结

Flutter 的导航和路由系统提供了多种方式来管理页面跳转:

  1. 基本导航:使用 Navigator.push()Navigator.pop()
  2. 命名路由:适合简单的路由需求
  3. 路由生成器:提供更灵活的路由控制
  4. Router API:适合复杂应用和深层链接

选择合适的导航方式取决于你的应用需求:

  • 简单应用:使用基本导航或命名路由
  • 中等复杂度:使用路由生成器
  • 复杂应用:使用 Router API(如 go_router)

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 导航 导航用flutter的自带组件 思路和vue导航是一样的1.声明一个数组,放入几个导航页面2.声明一个ind...
    明月半倚深秋_f45e阅读 709评论 0 0
  • 在传统的 Web 开发过程中,当你需要实现多个站内页面时,以前你需要写很多个 html 页面,然后通过 a 标签来...
    硅谷干货阅读 2,567评论 0 1
  • 我们通常会用屏(Screen)来称呼一个页面(Page),一个完整的App应该是有多个Page组成的。 在之前的案...
    AlanGe阅读 188评论 0 0
  • Flutter 的路由机制主要涉及两个核心类:Navigator 和 Route。这两个类共同协作,实现了应用程序...
    土豆骑士阅读 313评论 0 0
  • 我们通常会用屏(Screen)来称呼一个页面(Page),一个完整的App应该是有多个Page组成的。 在之前的案...
    Imkata阅读 686评论 0 1

友情链接更多精彩内容