我们通常用屏(Screen)来称呼一个页面(Page),一个完整的APP应该有多个Page组成的
在之前的豆瓣案例中,我们通过IndexedStack来管理首页中的Page切换
通过点击BottomNavigationBarItem来设置IndexedStackindex属性来切换

除了上面这种管理方式,我们还需要实现其他页面的 跳转 :比如你点击按钮跳转到另外一个页面
这种页面的管理和导航,我们通常会使用路由进行统一管理。

1. 路由管理

1.1 认识Flutter的路由

路由的概念由来已久,包括网络路由后端路由、到选在广为流行的前端路由

  • 无论路由的概念如何应用,它的核心是一个路由映射表
  • 比如:名字:detail映射到DetailPage页面等
  • 有了这个映射表之后,我们就可以方便的根据名字来完成路由的转发(在前端表现出来就是页面的跳转)

在flutter中,路由管理主要有两个类:RouteNavigator

1.2 Route

Route:一个页面想要被路由统一管理,必须包装为一个Route

  • 官方的说法很清晰:An abstraction for an entry managed by a Navigator.

但是我们发现Route是一个抽象类,所以它是不能被实例化的
在这里插入图片描述

我们可以查看Roter是与很多子类的, 但是我们可以通过该类的上面的说明信息,告诉我们可以使用MaterialPageRoute

其实MaterialPageRoute并不是Route的直接子类:

  • MaterialPageRoute在不同的平台有不同的表现
  • 对Android平台,打开一个页面会从屏幕底部滑动到屏幕的顶部,关闭页面时从顶部滑动到底部消失
  • 对iOS平台,打开一个页面会从屏幕右侧滑动到屏幕的左侧,关闭页面时从左侧滑动到右侧消失
  • 当然,iOS平台我们也可以使用CupertinoPageRoute
MaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route

1.3 Navigator

Navigator:管理所有的Route的Widget,通过一个Stack来进行管理的

  • 官方的说法也很清晰:A widget that manages a set of child widgets with a stack discipline.

那么我们开发中需要手动去创建一个Navigator吗?

  • 并不需要,我们开发中使用的MaterialAppCupertinoAppWidgetsApp它们默认是有插入Navigator
  • 所以,我们在需要的时候,只需要直接使用即可:Navigator.of(context)

Navigator有几个最常见的方法:

// 路由跳转:传入一个路由对象
Future<T> push<T extends Object>(Route<T> route)

// 路由跳转:传入一个名称(命名路由)
Future<T> pushNamed<T extends Object>(
  String routeName, {
    Object arguments,
  })

// 路由返回:可以传入一个参数
bool pop<T extends Object>([ T result ])

//API中还有类似的方法
//该方法是跳转到一个新的路由, 并把之前在栈中的其它路由全部删除
static Future<T?> pushNamedAndRemoveUntil<T extends Object?>

2. 路由的基本使用

2.1 基本跳转

我们来实现一个最基本的跳转:

  • 创建首页页面,中间添加一个按钮,点击按钮跳转到详情页面
  • 创建详情页面,中间添加一个按钮,点击按钮返回到首页页面

2.1.1 错误一

在这里插入图片描述

发现如果直接在 MaterialApp 中使用 Navigator 是会报错误:Navigator operation requested with a context that does not include a Navigator

如果把MaterialApp这一层提取出来, 就可以正常使用Navigator对象来跳转页面
在这里插入图片描述

总结: 要使用 路由(Navigator),根控件不能直接是 MaterialApp

其具体原因有待研究

2.1.2 跳转、返回、相互传值

  • main的核心代码:
void _onPushTap(BuildContext context) {
    //typedef WidgetBuilder = Widget Function(BuildContext context);
    Navigator.of(context).push(MaterialPageRoute(builder: (context) {
      return GYDeatilsPage("a home message");
    })).then((value) {
      setState(() {
        message = value;
      });
    });
  }
  • GYDetailesPage:详情页面代码
import 'package:flutter/material.dart';

class GYDeatilsPage extends StatelessWidget {
  final String message;
  GYDeatilsPage(this.message);
  @override
  Widget build(BuildContext context) {
    //1.第二种方法: 监听导航栏左上角返回按
    return WillPopScope(
      //final WillPopCallback? onWillPop
      //typedef WillPopCallback = Future<bool> Function()
      //该函数有一个返回值 Future<Bool>
      // 当返回true时,系统会自动返回(flutter帮助我们执行返回操作)
      // 当返回flase时, 需要我们自己手动来执行返回
      onWillPop: () {
        _onBackpop(context);
        return Future.value(false);
      },
      child: Scaffold(
            appBar: AppBar(
              title: Text("route测试"),
              //第一种方法: 自定义详情页面导航栏上的返回图标,如果不自己定义,系统会自动设置
              //leading: IconButton(icon: Icon(Icons.arrow_back_ios),onPressed: () => _onBackpop(context),),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children:[
                      Text(message),
                      ElevatedButton(
                        child: Text("返回首页"),
                        onPressed: () {
                          print("返回首页");
                            _onBackpop(context);
                        },
                      ),
                    ]
                ),
            ),
          ),
    );
  }
  
  void _onBackpop(BuildContext buildContext) {
    Navigator.of(buildContext).pop("return detailes message");
  }
}

2.2 命名路由的使用

2.2.1 基础跳转

我们可以通过创建一个新的Route,使用Navigator来导航到一个新的页面,但是如果在应用中很多地方都需要导航到同一个页面(比如在开发中,首页、推荐、分类页都可能会跳到详情页),那么就会存在很多重复的代码。

在这种情况下,我们可以使用命名路由(named route)

  • 命名路由是将名字和路由的映射关系,在一个地方进行统一的管理
  • 有了命名路由,我们可以通过Navigator.pushNamed() 方法来跳转到新的页面

那么命名路由我们需要再哪里配置了? 可以放在MaterialAppinitialRouteroutes属性中

  • initialRoute:设置引用程序从哪一个路由开始启动,设置了该属性,就不需要在设置home属性
  • routes:定义名称和路由之间的映射关系,类型为Map<String, WidgetBuilder>

在开发中,为了让每个页面对应的routeName统一,我们通常会在每个页面中定义一个路由的常量来使用, 而不是直接使用字符串使用,直接使用字符串容易写错

class GYHomePage extends StatefulWidget {
  static const String routeName = "/home";
  }

class GYAbout extends StatelessWidget {
  static const String routeName = "/about";
 }
  • main函数修改代码
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      //home: GYHomePage(),
      initialRoute: "/home",
      //typedef WidgetBuilder = Widget Function(BuildContext context);
      routes: {
        GYHomePage.routeName : (context) => GYHomePage(),
        GYAbout.routeName : (context) => GYAbout(),
      },
    );
  }
}

 // 跳转about页面
                ElevatedButton(onPressed: () => _onPushAboutPage(context), child: Text("跳转about"))

// 跳转about页面, 使用路由名称的方式
  void _onPushAboutPage(BuildContext context) {
    Navigator.of(context).pushNamed(GYAbout.routeName);
  }

2.1.2 传值

因为通常命名路由,我们会在定义的时候,直接创建好对象,比如GYAbout(),那么命名路由如果需要传递参数,如何传递了?

**pushName()**时

void _onPushAboutPage(BuildContext context) {
    Navigator.of(context).pushNamed(GYAbout.routeName, arguments: "home about message");
  }

那么在GYAbout()如何获取参数了:

 Widget build(BuildContext context) {
    //获取传递的参数
    var result = ModalRoute.of(context)?.settings?.arguments;
    if (result is String && result != null) {
      message = result;
    }
}

2.1.3 路由的钩子(onGenerateRoute)

假如我们有一个GYDeatilsPage,也希望在跳转时,传入对应的参数message,并且已经有一个对应的构造方法

在main中添加代码:

// 使用路由名称 实现已有构造方法的跳转
  void _onPushDetailesPage(BuildContext context) {
    //跳转详情页面
    Navigator.of(context).pushNamed(GYDeatilsPage.routeName, arguments: "home-路由钩子");
  }

但是我们继续使用routes中的映射关系,就不好进行配置,因为GYDeatilsPage必须要求传入一个参数;

这个时候我们可以使用onGenerateRoute钩子函数

  • 当我们通过pushNamed进行跳转,但是对应的name没有在routes中有映射关系,那么就会执行onGenerateRoute钩子函数
  • 我们可以在该函数中,手动创建对应的Route进行返回
  • 该函数有一个参数RouteSettings,该类有两个常用的属性
    • name: 跳转的路径名称
    • arguments:跳转时携带的参数
 //钩子函数  final RouteFactory? onGenerateRoute
      //typedef RouteFactory = Route<dynamic>? Function(RouteSettings settings);
     onGenerateRoute: (setttings) {
        if (setttings.name == GYDeatilsPage.routeName) {
          return MaterialPageRoute(builder: (context) {
            Object? arg = setttings.arguments;
            if (arg == null) {
              arg = "";
            }
            return GYDeatilsPage(arg as String);
          });
        }
        return null;
      },

2.1.4 onUnknownRoute

如果我们打开的一个路由名称是根本不存在,这个时候我们希望跳转到一个统一的错误页面
比如下面的abc是不存在有对应的页面的

如果没有特殊的处理,那么就会报错

void _onPushUnknownPage(BuildContext context) {
    Navigator.of(context).pushNamed("/abc");
  }

ElevatedButton(onPressed: () => _onPushUnknownPage(context), child: Text("打开位置页面"))

我们可以创建一个错误页面

class GYUnknownPage extends StatelessWidget {
  const GYUnknownPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("错误页面"),
      ),
      body: Container(
        child: Center(
          child: Text("页面跳转错误"),
        ),
      ),
    );
  }
}

//设置onUnknownRoute 属性
onUnknownRoute: (settings) {
        return MaterialPageRoute(builder: (context) {
          return GYUnknownPage();
        });
      },

注意:当使用路由名称的实现跳转的时候,如果输入一个不存在的路由名称, 那么首先回去调用onGenerateRoute(钩子函数),如果钩子函数没有实现, 那么久会调用onUnknownRoute错误页面函数, 如果该函数也没有实现就会报错,所以这里onGenerateRoute这里实现的时候一定要判断

一个完整的APP会有很多页面, 这就意味着会有很多映射关系, 如果我们直接把映射表关系直接写在main中,会导致该文件,频繁修改,而会很复杂,这里我们可以对路由映射关系表做一个抽取

import 'package:flutter/material.dart';
import 'main.dart';
import 'about.dart';
import 'details.dart';
import 'unknown.dart';
class GYRouter {

  static final Map<String, WidgetBuilder> routes = {
    GYHomePage.routeName : (ctx) => GYHomePage(),
    GYAbout.routeName : (ctx) => GYAbout()
  };

  static final String initialRoute = GYHomePage.routeName;

  static final RouteFactory generateRoute = (settings) {
    if (settings.name == GYDeatilsPage.routeName) {
      return MaterialPageRoute(
          builder: (ctx) {
            return GYDeatilsPage(settings.arguments as String);
          }
      );
    }
    return null;
  };

  static final RouteFactory unknownRoute = (settings) {
    return MaterialPageRoute(
        builder: (ctx) {
          return GYUnknownPage();
        }
    );
  };
}

main函数中可以修改成:

@override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      //home: GYHomePage(),
      initialRoute: GYRouter.initialRoute,
      routes: GYRouter.routes,
      onGenerateRoute: GYRouter.generateRoute,
      onUnknownRoute: GYRouter.unknownRoute,
    );
  }
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐