第2章:第一个Flutter应用 —— 2.4 路由管理

2.4 路由管理

📚 核心知识点

  1. 路由的概念
  2. Navigator基本使用
  3. 路由传参和返回值
  4. 命名路由
  5. 路由生成钩子

💡 核心概念

什么是路由?

路由(Route) 在移动开发中通常指页面(Page)

  • Android中:一个Activity
  • iOS中:一个ViewController
  • Web中:一个Page
  • Flutter中:一个Widget

路由管理 = 页面导航管理

Navigator维护一个路由栈

复制代码
┌─────────────────┐
│   第三个页面     │ ← 栈顶(当前显示)
├─────────────────┤
│   第二个页面     │
├─────────────────┤
│     首页        │ ← 栈底
└─────────────────┘

基本操作:

  • push - 入栈(打开新页面)
  • pop - 出栈(返回上一页)

路由栈操作流程

flowchart TB subgraph "路由栈状态变化" A1["初始:
[首页]"] A2["push 第二页
[首页, 第二页]"] A3["push 第三页
[首页, 第二页, 第三页]"] A4["pop 返回
[首页, 第二页]"] A5["pushReplacement 登录页
[首页, 登录页]"] end A1 --> |Navigator.push| A2 A2 --> |Navigator.push| A3 A3 --> |Navigator.pop| A4 A4 --> |Navigator.pushReplacement| A5 style A1 fill:#E3F2FD style A2 fill:#BBDEFB style A3 fill:#90CAF9 style A4 fill:#BBDEFB style A5 fill:#FFF9C4

🎯 方式1:基本路由跳转

打开新页面

dart 复制代码
// 使用 MaterialPageRoute 打开新页面
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => SecondPage(),
  ),
);

// 也可以使用其他路由类型(iOS风格)
Navigator.push(
  context,
  CupertinoPageRoute(
    builder: (context) => SecondPage(),
  ),
);

返回上一页

dart 复制代码
// 方法1:手动调用
Navigator.pop(context);

// 方法2:点击AppBar自动返回按钮
// Flutter会自动在AppBar添加返回按钮

完整示例

dart 复制代码
// 首页
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 打开新页面
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => SecondPage(),
              ),
            );
          },
          child: Text('打开第二页'),
        ),
      ),
    );
  }
}

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

🎯 方式2:路由传参和返回值

数据流向图

sequenceDiagram participant A as 页面A participant N as Navigator participant B as 页面B Note over A: 用户触发跳转 A->>N: push(页面B, 参数: "Hello") N->>B: 创建页面B
传入参数 "Hello" Note over B: 显示页面B
接收到参数 "Hello" Note over B: 用户操作完成 B->>N: pop(返回值: "Success") N->>A: 返回到页面A
携带返回值 "Success" Note over A: 接收返回值
更新UI

打开页面时传参

dart 复制代码
// 定义接收参数的页面
class DetailPage extends StatelessWidget {
  final String title;
  final int id;
  
  const DetailPage({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) => DetailPage(
      title: '商品详情',
      id: 123,
    ),
  ),
);

获取返回值

dart 复制代码
// 返回时传递数据
Navigator.pop(context, '返回的数据');

// 接收返回值(使用async/await)
final result = await Navigator.push<String>(
  context,
  MaterialPageRoute(builder: (context) => SelectPage()),
);

if (result != null) {
  print('用户选择了:$result');
}

完整示例:选择器

dart 复制代码
// 首页
class HomePage extends StatefulWidget {
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _selected = '未选择';
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('当前选择:$_selected'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                // 等待返回值
                final result = await Navigator.push<String>(
                  context,
                  MaterialPageRoute(
                    builder: (context) => SelectPage(),
                  ),
                );
                
                if (result != null) {
                  setState(() {
                    _selected = result;
                  });
                }
              },
              child: Text('去选择'),
            ),
          ],
        ),
      ),
    );
  }
}

// 选择页
class SelectPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('请选择')),
      body: ListView(
        children: [
          ListTile(
            title: Text('选项A'),
            onTap: () => Navigator.pop(context, 'A'),
          ),
          ListTile(
            title: Text('选项B'),
            onTap: () => Navigator.pop(context, 'B'),
          ),
          ListTile(
            title: Text('选项C'),
            onTap: () => Navigator.pop(context, 'C'),
          ),
        ],
      ),
    );
  }
}

🎯 方式3:命名路由

为什么用命名路由?

优点:

  1. 语义化更明确(/home, /detail
  2. 代码更好维护(统一管理)
  3. 可以做全局拦截(权限控制)

注册路由表

MaterialApp中注册:

dart 复制代码
MaterialApp(
  title: 'My App',
  // 设置首页
  initialRoute: '/',
  // 注册路由表
  routes: {
    '/': (context) => HomePage(),
    '/detail': (context) => DetailPage(),
    '/settings': (context) => SettingsPage(),
  },
)

使用命名路由

dart 复制代码
// 打开页面
Navigator.pushNamed(context, '/detail');

// 替换当前页面
Navigator.pushReplacementNamed(context, '/home');

// 清空栈并打开新页面
Navigator.pushNamedAndRemoveUntil(
  context,
  '/home',
  (route) => false,  // 移除所有页面
);

命名路由传参

有两种传参方式:

方法1:通过 arguments 传参(推荐)

优点: 灵活,适合需要传递多个参数的场景

dart 复制代码
// 1. 注册路由时获取参数
routes: {
  '/detail': (context) {
    final args = ModalRoute.of(context)?.settings.arguments as Map?;
    return DetailPage(
      title: args?['title'] ?? '',
      id: args?['id'] ?? 0,
    );
  },
}

// 2. 跳转时传参
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {
    'title': '商品详情',
    'id': 123,
  },
);

方法2:通过 onGenerateRoute 统一处理(推荐用于复杂项目)

优点: 统一管理,可以做参数校验、类型转换、权限检查

dart 复制代码
// 1. 不注册 routes,使用 onGenerateRoute
MaterialApp(
  onGenerateRoute: (settings) {
    // 根据路由名称判断
    if (settings.name == '/detail') {
      final args = settings.arguments as Map?;
      return MaterialPageRoute(
        builder: (context) => DetailPage(
          title: args?['title'] ?? '',
          id: args?['id'] ?? 0,
        ),
      );
    }
    
    if (settings.name == '/user') {
      final userId = settings.arguments as int?;
      // 可以在这里做权限检查
      if (userId == null) {
        return MaterialPageRoute(
          builder: (context) => ErrorPage(message: '用户ID不能为空'),
        );
      }
      return MaterialPageRoute(
        builder: (context) => UserPage(userId: userId),
      );
    }
    
    return null; // 未找到路由
  },
)

// 2. 跳转时传参(和方法1一样)
Navigator.pushNamed(context, '/detail', arguments: {'title': '商品详情', 'id': 123});

🎯 方式4:路由生成钩子

onGenerateRoute - 统一权限控制

onGenerateRoute会在打开命名路由时调用,可以用来:

  • 统一权限检查
  • 路由拦截
  • 动态路由生成
dart 复制代码
MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    // 获取路由名称
    String? routeName = settings.name;
    
    // 需要登录的页面列表
    List<String> authRoutes = ['/profile', '/cart', '/orders'];
    
    // 检查是否需要登录
    if (authRoutes.contains(routeName)) {
      // 检查登录状态
      bool isLoggedIn = checkLoginStatus();
      
      if (!isLoggedIn) {
        // 未登录,跳转到登录页
        return MaterialPageRoute(
          builder: (context) => LoginPage(
            redirectTo: routeName,  // 记录原本要去的页面
          ),
        );
      }
    }
    
    // 其他情况返回null,让Flutter使用routes表
    return null;
  },
  routes: {
    '/home': (context) => HomePage(),
    '/profile': (context) => ProfilePage(),
    '/cart': (context) => CartPage(),
  },
)

完整示例:登录拦截

dart 复制代码
class MyApp extends StatelessWidget {
  // 模拟登录状态
  static bool isLoggedIn = false;
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/home': (context) => HomePage(),
        '/profile': (context) => ProfilePage(),
      },
      onGenerateRoute: (settings) {
        // 拦截需要登录的页面
        if (settings.name == '/profile' && !isLoggedIn) {
          return MaterialPageRoute(
            builder: (context) => LoginPage(
              onLoginSuccess: () {
                // 登录成功后跳转到原页面
                Navigator.pushReplacementNamed(context, '/profile');
              },
            ),
          );
        }
        return null;
      },
    );
  }
}

打开页面

方法 说明 栈变化
push 打开新页面 [A, B] → [A, B, C]
pushReplacement 替换当前页面 [A, B] → [A, C]
pushAndRemoveUntil 打开页面并移除之前的页面 [A, B, C] → [D]

返回页面

方法 说明
pop 返回上一页
popUntil 返回到指定页面
popAndPushNamed 返回并打开新页面
maybePop 如果可以返回则返回

示例

dart 复制代码
// 1. 替换当前页面(登录后跳转首页)
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
);

// 2. 清空栈并打开新页面(退出登录)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => LoginPage()),
  (route) => false,  // 移除所有页面
);

// 3. 返回到首页
Navigator.popUntil(context, ModalRoute.withName('/'));

// 4. 如果可以返回则返回(否则什么都不做)
Navigator.maybePop(context);

🎯 核心总结

选择哪种方式?

场景 推荐方式
简单跳转,无需复用 基本路由
需要传递对象 基本路由 + 构造参数
需要全局管理 命名路由
需要权限控制 命名路由 + onGenerateRoute

最佳实践

建议统一使用命名路由

原因:

  1. 语义化更明确
  2. 代码更好维护
  3. 可以全局拦截
  4. 便于实现deep link

路由流程

flowchart TB A["Navigator.pushNamed"] B{"routes表中
有这个路由?"} C["使用routes中
的builder"] D["调用onGenerateRoute"] E{"返回值"} F["使用返回的Route"] G["调用onUnknownRoute"] A --> B B -->|"✅ 有"| C B -->|"❌ 没有"| D D --> E E -->|"Route"| F E -->|"null"| G style C fill:#C8E6C9 style F fill:#C8E6C9 style G fill:#FFCDD2

📝 常见问题

Q1: pop时如何判断是否能返回?

A: 使用 Navigator.canPop(context)

dart 复制代码
if (Navigator.canPop(context)) {
  Navigator.pop(context);
} else {
  // 已经是栈底,不能再返回
  print('已经是第一个页面了');
}

// 或者使用
Navigator.maybePop(context);  // 自动判断

Q2: 如何监听返回按钮?

A: 使用 WillPopScope

dart 复制代码
WillPopScope(
  onWillPop: () async {
    // 返回true允许返回,false阻止返回
    bool shouldPop = await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('确认退出?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: Text('确认'),
          ),
        ],
      ),
    );
    return shouldPop;
  },
  child: Scaffold(
    // ...
  ),
)

Q3: 命名路由如何传递复杂对象?

A:

dart 复制代码
// 定义参数类
class DetailPageArgs {
  final String title;
  final User user;
  final List<String> tags;
  
  DetailPageArgs({required this.title, required this.user, required this.tags});
}

// 注册路由
routes: {
  '/detail': (context) {
    final args = ModalRoute.of(context)!.settings.arguments as DetailPageArgs;
    return DetailPage(
      title: args.title,
      user: args.user,
      tags: args.tags,
    );
  },
}

// 跳转
Navigator.pushNamed(
  context,
  '/detail',
  arguments: DetailPageArgs(
    title: 'User Detail',
    user: currentUser,
    tags: ['tag1', 'tag2'],
  ),
);

Q4: 如何实现侧滑返回(iOS效果)?

A: 使用 CupertinoPageRoute

dart 复制代码
Navigator.push(
  context,
  CupertinoPageRoute(
    builder: (context) => SecondPage(),
  ),
);

Q5: onGenerateRoute和routes的区别?

A:

  • routes:静态路由表,简单直接
  • onGenerateRoute:动态路由生成,可以做拦截

执行顺序:

  1. 先查找 routes
  2. 如果没找到,调用 onGenerateRoute
  3. 如果还是null,调用 onUnknownRoute

🎓 跟着做练习

练习1:实现一个商品列表和详情页 ⭐⭐

要求:

  1. 列表页显示商品列表
  2. 点击商品跳转到详情页
  3. 详情页接收商品ID和名称
  4. 详情页有返回按钮
dart 复制代码
// 商品模型
class Product {
  final int id;
  final String name;
  final double price;
  
  Product({required this.id, required this.name, required this.price});
}

// 列表页
class ProductListPage extends StatelessWidget {
  final List<Product> products = [
    Product(id: 1, name: 'iPhone', price: 5999),
    Product(id: 2, name: 'iPad', price: 3999),
    Product(id: 3, name: 'MacBook', price: 9999),
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('商品列表')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ListTile(
            title: Text(product.name),
            subtitle: Text('¥${product.price}'),
            trailing: Icon(Icons.chevron_right),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ProductDetailPage(product: product),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

// 详情页
class ProductDetailPage extends StatelessWidget {
  final Product product;
  
  const ProductDetailPage({required this.product});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product.name)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID: ${product.id}', style: TextStyle(fontSize: 20)),
            Text('名称: ${product.name}', style: TextStyle(fontSize: 20)),
            Text('价格: ¥${product.price}', style: TextStyle(fontSize: 20)),
          ],
        ),
      ),
    );
  }
}

练习2:实现城市选择器 ⭐⭐⭐

要求:

  1. 主页显示当前选择的城市
  2. 点击按钮打开城市列表
  3. 选择城市后返回主页
  4. 主页更新显示选择的城市
dart 复制代码
class CitySelectDemo extends StatefulWidget {
  @override
  State<CitySelectDemo> createState() => _CitySelectDemoState();
}

class _CitySelectDemoState extends State<CitySelectDemo> {
  String _city = '北京';
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('城市选择')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('当前城市:$_city', style: TextStyle(fontSize: 24)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                final result = await Navigator.push<String>(
                  context,
                  MaterialPageRoute(
                    builder: (context) => CityListPage(),
                  ),
                );
                
                if (result != null) {
                  setState(() {
                    _city = result;
                  });
                }
              },
              child: Text('选择城市'),
            ),
          ],
        ),
      ),
    );
  }
}

class CityListPage extends StatelessWidget {
  final List<String> cities = ['北京', '上海', '广州', '深圳', '杭州'];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('选择城市')),
      body: ListView.builder(
        itemCount: cities.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(cities[index]),
            onTap: () {
              Navigator.pop(context, cities[index]);
            },
          );
        },
      ),
    );
  }
}

参考: 《Flutter实战·第二版》2.4节

相关推荐
旧时光_5 小时前
第2章:第一个Flutter应用 —— 2.2 Widget简介
flutter
Bryce李小白5 小时前
Flutter provide框架内部实现原理刨析
flutter
CN-cheng5 小时前
Flutter项目在HarmonyOS(鸿蒙)运行报错问题总结
flutter·华为·harmonyos·flutter运行到鸿蒙
Larry_zhang双栖7 小时前
Flutter Android Kotlin 插件编译错误完整解决方案
android·flutter·kotlin
安卓开发者12 小时前
第1讲:为什么是Flutter?跨平台开发的现状与未来
flutter
芝麻开门-新起点1 天前
Flutter 项目全流程指南:编译、调试与发布
flutter
星释1 天前
鸿蒙Flutter三方库适配指南:11.插件发布上线及使用
flutter·华为·harmonyos