Flutter小白实操之Widgets路由管理

以下内容参考《Flutter实战·第二版》和《Flutter中文网》,仅为个人学习、熟悉Flutter,不同版本可能有稍微不一样。

环境

///
/// 例子来源:
/// 《Flutter实战·第二版》:https://book.flutterchina.club/chapter2/flutter_router.html
/// Flutter中文网:https://flutter.cn/docs/cookbook/navigation
///
/// 环境:
/// Flutter 3.10.1 • channel stable • https://github.com/flutter/flutter.git
/// Framework • revision d3d8effc68 (7 days ago) • 2023-05-16 17:59:05 -0700
/// Engine • revision b4fb11214d
/// Tools • Dart 3.0.1 • DevTools 2.23.1
///

路由管理,其实就是常见的导航管理,push打开新页面,pop关闭页面

MaterialPageRoute和Navigator

Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

  • 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
  • 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。

打开页面

Navigator.of(context).push(
  MaterialPageRoute(
    maintainState: true, //默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
    fullscreenDialog: false, //表示新的路由页面是否是一个全屏的模态对话框,[true]iOS从屏幕底部滑入
     builder: (context) => TipRoute(text: param),
   ),
);

关闭页面

Navigator.of(context).pop();

路由传值

传数据到第二页面

  • 方式一:跳转的时候,对应Widget带参数过去,见DetailScreen
  • 方式二:使用RouteSettings传递参数,使用 ModalRoute.of() 方法获取参数,见DetailScreenRoute
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 传递数据到新页面
/// https://flutter.cn/docs/cookbook/navigation/passing-data

TodosScreen的数据源
  static final todos = List.generate(
      20,
          (i) => Todo(
          title: "Todo $I",
          description: "A description of what needs to be done for Todo $I"
      )
  );
//////////////////////////////////////////////////////////////////////

class Todo {
  final String title;
  final String description;

  const Todo({required this.title, required this.description});
}

class TodosScreen extends StatelessWidget {
  const TodosScreen({super.key, required this.todos});

  final List<Todo> todos;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          Todo todo = todos[index];
          return ListTile(
            title: Text(todo.title),
            //subtitle: Text(todos[index].description),
            onTap: () {
              if (index % 2 == 0) {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => DetailScreen(todo: todo), // 方式一
                  )
                );
              } else {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => const DetailScreenRoute(),
                    settings: RouteSettings(
                      arguments: todo, // 方式二
                    ),
                  )
                );
              }
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key, required this.todo});

  final Todo todo; // 方式一默认带参

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: customAppBar(title: "${todo.title} 方式一", backgroundColor: navigationBarColor),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(todo.description),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final todo = ModalRoute.of(context)!.settings.arguments as Todo;  // 方式二获取参数
    return Scaffold(
      appBar: customAppBar(title: "${todo.title} 方式二", backgroundColor: navigationBarColor),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(todo.description),
      ),
    );
  }
}

一些自定义的导航bar

AppBar customAppBar({required String title, required Color backgroundColor}) {
  return AppBar(
    title: Text(
        title,
        style: const TextStyle(color: Colors.white)
    ),
    iconTheme: const IconThemeData(color: Colors.white),
    backgroundColor: backgroundColor,
  );
}

Color navigationBarColor = Colors.red;
传数据到第二页面

回传数据

调用API:Navigator.pop(context, "");添加参数返回
使用async和await获取返回数据,用SnackBar显示返回数据

/// 从一个页面回传数据
/// https://flutter.cn/docs/cookbook/navigation/returning-data
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Padding(
        padding: EdgeInsets.all(20),
        child: SelectionButton(),
      ),
    );
  }
}

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

  @override
  State<SelectionButton> createState() => _SelectionButtonState();
}

class _SelectionButtonState extends State<SelectionButton> {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: (){
        _navigateAndDisplaySelection(context);
      },
      child: const Text("请选择")
    );
  }

  Future<void> _navigateAndDisplaySelection(BuildContext context) async {
    final result = await Navigator.of(context).push(
      MaterialPageRoute(
        fullscreenDialog: true, //模态从下到上弹出
        builder: (context) => const SelectionScreen()),
    );

    // When a BuildContext is used from a StatefulWidget, the mounted property
    // must be checked after an asynchronous gap.
    // 不加这个会有警告 Don't use 'BuildContext's across async gaps. (Documentation)
    if (!mounted) return;

    ScaffoldMessenger.of(context)
    ..removeCurrentSnackBar()
    ..showSnackBar(SnackBar(content: Text("$result")));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: customAppBar(title: "请选择", backgroundColor: navigationBarColor),
      body: Padding(padding: const EdgeInsets.all(30),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(8),
              child: ElevatedButton(
                onPressed: () {
                  Navigator.pop(context, "好的");
                },
                child: const Text("好的"),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8),
              child: ElevatedButton(
                onPressed: (){
                  Navigator.pop(context, "不行");
                },
                child: const Text("不行"))
            ),
          ],
        ),
      ),
    );
  }
}
回传数据

命名路由

main.dart处理,封装了个路由管理

导航到对应名称的routes里

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 导航到对应名称的routes里
/// https://flutter.cn/docs/cookbook/navigation/named-routes
class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: [
            const Padding(
              padding: EdgeInsets.only(left: 20, top: 20, right: 20),
              child: Text("当使用[initialRoute]时,需要确保没有同时定义[home]属性。"),
            ),
            ElevatedButton(
              child: const Text("前往下一页"),
              onPressed: () {
                Navigator.of(context).pushNamed(RouteManager.secondScreen);
              },
            ),
            ElevatedButton(
              child: const Text("下一页arguments传参"),
              onPressed: () {
                Navigator.of(context).pushNamed(
                  RouteManager.extractArguments,
                  arguments: const ScreenArguments(
                    title: "arguments传参",
                    message: "这个页面是使用pushNamed(arguments:)带入参数,内容使用ModalRoute.of()方法。这个方法返回的是当前路由及其携带的参数。"
                  ),
                );
              },
            ),
            const Padding(
              padding: EdgeInsets.all(10),
              child: Text("两种获取参数不兼容", style: TextStyle(color: Colors.red)),
            ),
            const Padding(
              padding: EdgeInsets.only(left: 20, top: 20, right: 20),
              child: Text("使用[onGenerateRoute]时,不需要定义[initialRoute]和[routes],否则[onGenerateRoute]不会调用。"),
            ),
            ElevatedButton(
              child: const Text("下一页onGenerateRoute取参"),
              onPressed: () {
                Navigator.of(context).pushNamed(
                  RouteManager.passArguments,
                  arguments: const ScreenArguments(
                    title: "onGenerateRoute取参",
                    message: "通过 onGenerateRoute() 函数提取参数,然后把参数传递给组件。"
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: customAppBar(title: "第二页", backgroundColor: navigationBarColor),
      body: Column(
        children: [
          Center(child:
            ElevatedButton(
              child: const Text("返回"),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ),
        ]
      ),
    );
  }
}

main.dart配置

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: RouteManager.routers,
    );
  }
}

封装的route管理,包含路由名称,注册Map,还有下一步的onGenerateRoute使用

来着:《Flutter 学习之 命名路由(Navigator 1.0)》https://www.jianshu.com/p/170c3ae8690c

class RouteManager {
  /// 定义路由名字
  static const String rootPageName = "/";  // 名为"/"的路由作为应用的home(首页)
  static const String secondScreen = "/secondScreen";
  static const String extractArguments = "/extractArguments";
  static const String passArguments = "/passArguments";

  /// 注册路由
  static final Map<String, WidgetBuilder> routers = {
    rootPageName: (context) => const MyHomePage(title: "Flutter Demo"), //首页
    secondScreen: (context) => const SecondScreen(),
    extractArguments: (context) => const ExtractArgumentsScreen(),
    passArguments: (context, {arguments}) => PassArgumentsScreen(arguments: arguments),
  };

  /// 路由生成钩子,main中不需要routes:{}
  Route<dynamic> onGenerateRoute(RouteSettings settings) {
    // 获取rotateName
    final String? routerName = settings.name;
    // 找到对应的页面
    final Function? buildPage = routers[routerName];
    print("routerName=$routerName, buildPage=$buildPage");

    // 如果对应的页面是空的就返回一个空页面
    // MaterialPageRoute:页面切换方式为从下往上,
    // CupertinoPageRoute:页面切换方式从左往右 还可以自定义切换方式
    if (buildPage == null) {
      return MaterialPageRoute(
        builder: (_) => const Scaffold(
          body: Center(
            child: Text("Page Not Found!!!!"),
          ),
        )
      );
    }

    // 查看携带的参数
    dynamic? args = settings.arguments as dynamic;
    if (args != null) {
      //buildPage里面的函数,规范带参arguments:,比如Name(arguments:xxx)
      return MaterialPageRoute(builder: (context) => buildPage(context, arguments: args));
    }
    return MaterialPageRoute(builder: (context) => buildPage(context));
  }
}
导航到对应名称的routes里

给特定的route传参

使用 onGenerateRoute 提取参数

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 给特定的route传参
/// https://flutter.cn/docs/cookbook/navigation/navigate-with-arguments
class ScreenArguments {
  final String title;
  final String message;
  const ScreenArguments({required this.title, required this.message});
}

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

  @override
  Widget build(BuildContext context) {
    //使用ModalRoute.of()方法。这个方法返回的是当前路由及其携带的参数。
    final ScreenArguments args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;

    return Scaffold(
      appBar: customAppBar(title: args.title, backgroundColor: navigationBarColor),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(args.message),
      ),
    );
  }
}

class PassArgumentsScreen extends StatelessWidget {
  final ScreenArguments arguments;

  const PassArgumentsScreen({super.key, required this.arguments});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: customAppBar(title: arguments.title, backgroundColor: navigationBarColor),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(arguments.message),
      ),
    );
  }
}

main.dart配置

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      /// 使用[onGenerateRoute]时,不需要定义[initialRoute]和[routes],否则[onGenerateRoute]不会调用。
      onGenerateRoute: RouteManager().onGenerateRoute,
    );
  }
}
给特定的route传参

遇到的问题

1、Could not find a generator for route RouteSettings("xxx", null) in the _WidgetsAppState.

子页面不能使用MaterialApp,该页面无法Navigator.of(context).push()
找了好几个地方都不对,最后这里解决了Navigator中命名路由使用中的问题

======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/secondScreen", null) in the _WidgetsAppState.

Make sure your root app widget has provided a way to generate 
this route.
Generators for routes are searched for in the following order:
 1. For the "/" route, the "home" property, if non-null, is used.
 2. Otherwise, the "routes" table is used, if it has an entry for the route.
 3. Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
 4. Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.

2、The following NoSuchMethodError was thrown building Builder(dirty)

路由带参数报错,使用路由封装的时候,带了参数,赋值错误导致

    // 查看携带的参数
    dynamic? args = settings.arguments as dynamic;
    if (args != null) {
      //buildPage里面的函数,规范带参arguments:,比如Name(arguments:xxx)
      return MaterialPageRoute(builder: (context) => buildPage(context, arguments: args));
    }
    return MaterialPageRoute(builder: (context) => buildPage(context));

错误信息:

======== Exception caught by widgets library =======================================================
The following NoSuchMethodError was thrown building Builder(dirty):
Closure call with mismatched arguments: function 'RouteManager.routers.<anonymous closure>'
Receiver: Closure: (BuildContext, {dynamic arguments}) => PassArgumentsScreen
Tried calling: RouteManager.routers.<anonymous closure>(Instance of 'StatelessElement', _Map len:1)
Found: RouteManager.routers.<anonymous closure>(BuildContext, {dynamic arguments}) => PassArgumentsScreen
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容