Flutter开发入门系列-Navigator2的学习和使用

Navigator2 为了提供 Flutter Web的支持新引入了很多的API 概念,比如: Page, Router, RouteDelegate, RouteInformationParser, 除此之外还需要自己管理路由栈,比如 Navigator 1 的复杂度高出了很多。但是这些 API 并不都是必须的,对于一个App的导航框架来说,只需要 Router, Page, RouteDelegate 就可以了,其中 Router, Page 的使用比较简单,主要是 RouterDelegate 比较复杂。

RouteDelegate 中最为核心的一点就是在build方法中来实现对路由栈的管理。

Navigator2 特性:

1. 支持自定义页面的路由栈

2. 支持一次打开或者关闭多个页面

3. 支持删除当前页面下的页面

以上特性都是Navigator 1.0 没有或者很难实现的功能。

Navigator 2.0 提供了一系列全新的接口, 可以实现将路由状态成为应用状态的一部分,并提供解析来自底层平台如 WebURL 的路由的功能,新增的API 如下:

Page:用来表示 Navigator 路由栈中各个页面的不可变对象;

Page是个抽象类通常使用它的派生类:MaterialPage或CupertinoPage;

Router:用来配置要由 Navigator 展示的页面列表,通常,该页面列表会根据系统或应用程序的状态改变而改变;

除了可以直接使用Router本身外还可以使用MaterialApp.router()来创建Router;

RouterDelegate:定义应用程序中的路由行为,例如 Router 如何知道应用程序状态的变化以及如何响应;

主要的工作就是监听RouteInformationParser和应用状态,并使用当前列表来构建Pages;

RouteInformationParser:可缺省,主要应用与web,持有RouteInformationProvider 提供的 RouteInformation ,可以将其解析为我们定义的数据类型;

BackButtonDispatcher:响应后退按钮,并通知 Router。

Tips:上面API中BackButtonDispatcher是用到的情况很少,另外对应移动端APP开发来说我们只需要用到Navigator 2.0 中的Page、Router、RouterDelegate 这三个API即可,RouteInformationParser与RouteInformationProvider主要是应用于开发web网站的路由用的;

下图展示了 RouterDelegate 与 Router、RouteInformationParser 在一起的交互和应用程序的状态:

流程解析:

1. 当系统打开新页面(如 “/home”)时,RouteInformationParser 会将其转换为应用中的具体数据类型 T(如:BiliRoutePath);

2. 该数据类型会被传递给 RouterDelegate 的 setNewRoutePath 方法,我们可以在这里更新路由状态;

3. notifyListeners 会通知 Router 重建 RouterDelegate(通过 build() 方法)RouterDelegate.build() 返回一个新的 Navigator 实例,并最终展示出我们想要打开的页面;

具体使用Navigator2的步骤:

1. 创建XXPath类;

2.  构建使用MaterialPage 构建Page;

3.  创建XXRouteDelegte类,继承 RouterDelegate,并with 混入ChangeNotifier, PopNavigatorRouterDelegateMixin, 在build方法中返回Navigator;

4. 创建Router对象和XXRouteDelegate对象,并将XXRouteDelegate对象传入Router的构造方法中,最后将Router对象传入MaterialApp的home中。

5. 在XXRouteDelegate的build方法中利用pages来进行路由栈的管理。

一、创建XXPath类

//main.dart
class MxRoutePath {
  final location;

  const MxRoutePath.home() : location = "/";
  const MxRoutePath.detail() : location = "/detail";
}

二、创建各个不同的页面,Login, Registration, Home, VideoList, VideoDetail, 并将这些页面包装成Page,代码如下:

//hi_navigator.dart
pageWrap(Widget child,{VideoModel model}) {
  return MaterialPage(key: ValueKey(child.hashCode), child: child); //ValueKey是用来标记唯一的
  //return MaterialPage(key:ValueKey(model??child.hashCode), child: child);
}

三、创建XXRouteDelegate 

//main.dart
class MXRouteDelegate extends RouterDelegate<MxRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<MxRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;
  MxRoutePath path;
  List<MaterialPage> pages = [];
  RouteStatus _routeStatus = RouteStatus.homePage;
  var videoType;

  //为 Navigator 设置一个key, 必要的时候可以通过 navigatorKey.currentState 获取 NavigatorState 对象
  MXRouteDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
    HiNavigator.instance().registerRouteJumpListener(RouteJumpListener(onJumpTo: (RouteStatus status, {Map args}) {
      _routeStatus = status;
      print("_routeStatus ${_routeStatus.name}");
      if (status == RouteStatus.homePage) {
        //获取传递过来的参数
      } else if (status == RouteStatus.videoListPage) {
        //获取传递过来的视频类别
        videoType = args['videoType'];
      } else if (status == RouteStatus.videoDetailPage) {
        //获取传递过来的视频详情
        videoModel = args['videoModel'];
        print('videoModel ${videoModel.vid}');
      }
      notifyListeners(); // 和setState 一样,会重新调用build 方法
    }));
  }

  RouteStatus get routeStatus {
    if (!hasLogin && _routeStatus != RouteStatus.registrationPage) {
      //没有登录,需要跳转到登录页面
      return _routeStatus = RouteStatus.loginPage; //跳转到登录页面
    } else if (videoType != null && _routeStatus == RouteStatus.videoListPage) {
      return _routeStatus = RouteStatus.videoListPage;
    } else if (videoModel != null && _routeStatus == RouteStatus.videoDetailPage) {
      return _routeStatus = RouteStatus.videoDetailPage;
    }
    return _routeStatus;
  }

  bool get hasLogin => true; //判断是否已经登录

  VideoModel videoModel;

  @override
  Widget build(BuildContext context) {
    int index = getRouteIndex(pages, routeStatus);
    List<MaterialPage> tmpPages = pages;
    if (index != -1) {
      tmpPages = tmpPages.sublist(0, index);
    }
    var page;
    if (routeStatus == RouteStatus.homePage) {
      pages.clear();
      page = pageWrap(HomePage());
    } else if (routeStatus == RouteStatus.videoListPage) {
      page = pageWrap(VideoListPage(videoType));
    } else if (routeStatus == RouteStatus.videoDetailPage) {
      page = pageWrap(VideoDetailPage(videoModel), model: videoModel);
    } else if (routeStatus == RouteStatus.loginPage) {
      pages.clear();
      page = pageWrap(LoginPage());
    } else if (routeStatus == RouteStatus.registrationPage) {
      page = pageWrap(RegistrationPage());
    }
    tmpPages = [...tmpPages, page]; //...展开运算符
    //通知页面发生了变化
    HiNavigator.instance().notify(tmpPages, pages);
    //将最新的页面堆栈赋值给pages
    pages = tmpPages;
    return Navigator(
      key: navigatorKey,
      pages: pages,
      onPopPage: (route, result) {
        //注意:当栈内只有一个页面的时候是不会执行onPopPage回调
        //登录页没有登录返回拦截
        print("是否进行回退...");
        if (route.settings is MaterialPage) {
          if ((route.settings as MaterialPage).child is LoginPage) {
            print("是否在登录页进行回退...");
            if (!hasLogin) {
              return false; //在退出登录页面的时候,加以拦截,不能直接退出
            }
          }
          if ((route.settings as MaterialPage).child is RegistrationPage) {
            print("是否在注册页进行回退...");
          }
        }
        //在这里可以控制是否可以返回
        if (!route.didPop(result)) {
          return false;
        }
        var tmpPages = [...pages];
        pages.removeLast(); //移除栈顶到页面
        HiNavigator.instance().notify(pages, tmpPages);
        //return true 表示在退出的时候,不阻拦
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(MxRoutePath path) async {
    this.path = path;
    print("setNewRoutePath ${path.location}");
  }
}

此处重点关注build 方法是如何返回Navigator对象的,其他的暂时不用考虑。比如使用 GlobalKey<NavigatorState>来创建 key,并赋值给Navigator的key属性,利用List<MaterialPage> 来创建 pages 对象来赋值给Navigato的pages属性,onPopPage 的返回值是用来判断是否将当前即将退出的Page给拦截退出,true 表示不拦截退出,false 表示拦截退出。

四、创建Router对象和XXRouteDelegate对象

//main.dart
class MyApp extends StatelessWidget {
  MXRouteDelegate routeDelegate = MXRouteDelegate();

  @override
  Widget build(BuildContext context) {
    //MaterialApp.router(routeInformationParser: routeInformationParser, routerDelegate: routerDelegate)
    Widget widget = Router(routerDelegate: routeDelegate);
    return MaterialApp(
      home: widget,
    );
  }
}

五、XXRouteDelegate的build方法内部进行页面栈的管理

整体的思路就是pages中由MaterialPage构成的栈是最终整个应用的路由栈,pages列表最后的page就是显示在最前面给用户看到的。具体的逻辑就是判断当前的枚举类型 routeStatus 来判断当前路由栈中是否存在与该 routeStatus 相匹配的 page, 如果存在,则返回该page在路由栈pages的下标,然后在列表pages中移除所有的该下标以后的所有page,包括当前该index对应的page【其实也可以不用删除,看需求】,如果不存在与status对应的page, 那么就创建对应的page,然后添加到pages中。在onPopPage中,如果确定要退出当前页,则在返回true 不进行拦截的前面删除对应的栈顶页面。其他的代码是关于页面跳转和页面切换监听的,代码都在下面,不解释了。

//hi_navigator.dart
//创建Page
import 'package:flutter/material.dart';
import 'package:flutter_navigator/home_page.dart';
import 'package:flutter_navigator/login_page.dart';
import 'package:flutter_navigator/registration_page.dart';
import 'package:flutter_navigator/video_detail_page.dart';
import 'package:flutter_navigator/video_list_page.dart';
import 'package:flutter_navigator/video_mo.dart';

pageWrap(Widget child,{VideoModel model}) {
  return MaterialPage(key: ValueKey(child.hashCode), child: child); //ValueKey是用来标记唯一的
  //return MaterialPage(key:ValueKey(model??child.hashCode), child: child);
}

//1.主页 homePage 2. 列表页 videoListPage 3.详情页 VideoDetailPage
// 1-->2-->3-->1

//1.先定义RouteStatus
//2.根据当前的RouteStatus得到当前页面在路由栈中的位置 index
//3.根据index,排除包括index所指页面的以上所有页面 从List<MaterialPage>
//4.根据RouteStatus 生成新的页面, 然后放入到列表中
//5.更新路由栈页面数据

enum RouteStatus {homePage, videoListPage, videoDetailPage, loginPage, registrationPage, unknownPage}

//给枚举扩展属性
extension RouteStatusExtension on RouteStatus{
  String get name =>
      ['homePage', 'videoListPage', 'videoDetailPage','loginPage','registrationPage','unknownPage'][index];
}


//获取路由在路由栈中的index
int getRouteIndex(List<MaterialPage> pages, RouteStatus status){
  //判断如果栈顶的是VideoDetailPage 那么就不去销毁旧的
  if(pages.isNotEmpty && pages.last.child is VideoDetailPage){
     return -1;
  }
  for(int index= 0; index< pages.length; index++){
     MaterialPage page = pages[index];
     if (getPageStatus(page) == status){
         return index;
     }
  }
  return -1;
}

RouteStatus getPageStatus(MaterialPage page){
   if (page.child is HomePage){
       return RouteStatus.homePage;
   } else if (page.child is VideoListPage){
       return RouteStatus.videoListPage;
   } else if (page.child is VideoDetailPage){
       return RouteStatus.videoDetailPage;
   } else if (page.child is LoginPage){
       return RouteStatus.loginPage;
   } else if (page.child is RegistrationPage){
       return RouteStatus.registrationPage;
   } else {
       return RouteStatus.unknownPage;
   }
}

//定义页面切换生命周期逻辑 比如从哪一个页面切换到一个页面
typedef RouteChangeListener(RouteStatusInfo current, RouteStatusInfo pre);

//路由信息
class RouteStatusInfo {
  final RouteStatus routeStatus;
  final Widget page;

  RouteStatusInfo(this.routeStatus, this.page);
}

//1.定义跳转回调
typedef OnJumpTo = void Function(RouteStatus routeStatus, {Map args});

//2.定义一个接口,准备在跳转的时候进行callback
abstract class _RouteJumpListener {
  void onJumpTo(RouteStatus routeStatus, {Map args});
}

//3.定义Listener, 跳转的监听类, 用于注册在HiNavigator
class RouteJumpListener {
  final OnJumpTo onJumpTo;
  RouteJumpListener({this.onJumpTo});
}

//4.定义 HiNavigator单例,用于注册和监听跳转
class HiNavigator extends _RouteJumpListener {

  static HiNavigator _instance;
  RouteJumpListener _routeJumpListener;


  HiNavigator._();

  static HiNavigator instance() {
    if (_instance == null) {
        _instance = HiNavigator._();
    }
    return _instance;
  }


  //从别处主动调用该方法,传入RouteStatus 和 必要的参数
  @override
  void onJumpTo(RouteStatus routeStatus, {Map args}) {
    _routeJumpListener.onJumpTo(routeStatus, args:args);
  }

  void registerRouteJumpListener(RouteJumpListener listener){
    this._routeJumpListener = listener;
  }

  //添加页面监听
  List<RouteChangeListener> _listeners = [];
  void addRouteChangeListener(RouteChangeListener listener){
    if (!_listeners.contains(listener)){
        _listeners.add(listener);
    }
  }

  //移除页面监听
  void removeRouteChangeListener(RouteChangeListener listener){
     _listeners.remove(listener);
  }

  //触发生命周期callback
  void notify(List<MaterialPage> currentPages, List<MaterialPage> prePages){
     if (currentPages == prePages){
         return;
     }
     var current = RouteStatusInfo(getPageStatus(currentPages.last), currentPages.last.child);
     _notify(current);
  }

  RouteStatusInfo _current; //当前页面的信息

  void _notify(RouteStatusInfo current) {
    print("notify current page --> ${current.routeStatus.name} prePage--->${_current?.routeStatus?.name}");
    _listeners.forEach((listener) {
        listener(current, _current);
    });
    _current = current;
  }

}
//video_mo.dart
class VideoModel {
  int vid;
  VideoModel(this.vid);
}

其他页面代码:

//login_page.dart
import 'package:flutter/material.dart';
import 'hi_navigator.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({Key key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();

}

class _LoginPageState extends State<LoginPage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        titleSpacing: 0,
        leading: BackButton(),
        title: Text('loginPage'),),
      body: Container(
        child: Column(
          children: [
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('点击跳转到注册页面'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.registrationPage);
                  }),
            ),
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('登录成功点击跳转到首页'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.homePage);
                  }),
            ),
          ],
        ),
      ),
    );
  }

}
//registration_page.dart
import 'dart:async';
import 'package:flutter/material.dart';

class RegistrationPage extends StatefulWidget {
  const RegistrationPage({Key key}) : super(key: key);

  @override
  _RegistrationPageState createState() => _RegistrationPageState();
}

class _RegistrationPageState extends State<RegistrationPage> {

  bool needNotice =  true; //表示提醒填写个人信息

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        child: Scaffold(
          appBar: AppBar(
            centerTitle: false,
            title: Text('RegistrationPage'),
            titleSpacing: 0,
            leading: BackButton(),
          ),
          body: Container(
            child: Column(
              children: [
                Container(
                  margin: EdgeInsets.only(left: 10, right: 10),
                  child: MaterialButton(
                      minWidth: MediaQuery.of(context).size.width,
                      child: Text('点击跳转到登录页面'),
                      onPressed: () {
                        //HiNavigator.instance().onJumpTo(RouteStatus.loginPage);
                        Navigator.of(context).pop();
                      }),
                ),
                Container(
                  margin: EdgeInsets.only(left: 10, right: 10),
                  child: MaterialButton(
                      minWidth: MediaQuery.of(context).size.width,
                      child: Text('点击更新填写用户信息'),
                      onPressed: () {
                        needNotice = false;
                        setState(() {

                        });
                      }),
                ),
              ],
            ),
          ),
        ),
        onWillPop: _requestPop
    );
  }

  Future<bool> _requestPop() async{
    bool result = await _showNoticeDialog();
    print("_requestPop $result");
    return Future.value(result);
  }

 Future<bool> _showNoticeDialog() async{
    bool result = await showDialog(
        context: context,
        barrierDismissible: false, //是否触摸对话框之外能够消失对话框
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('请填写相关的个人信息'),
            content: SingleChildScrollView(
              child: ListBody(
                children: [
                  Text('这是标题'),
                  Text('这是内容'),
                ],
              ),
            ),
            actions: [
              TextButton(
                  onPressed: () {
                    Navigator.of(context).pop(true);
                  },
                  child: Text('退出')),
              TextButton(
                  onPressed: () {
                    Navigator.of(context).pop(false);
                  },
                  child: Text('取消'))
            ],
          );
        });
    return Future.value(result);
  }
}
//home_page.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_navigator/hi_navigator.dart';

class HomePage extends StatefulWidget {

  const HomePage({Key key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();

}

class _HomePageState extends State<HomePage> with WidgetsBindingObserver{

  var routeChangeListener;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    HiNavigator.instance().addRouteChangeListener(this.routeChangeListener=
        (RouteStatusInfo current, RouteStatusInfo pre){
         print("onChange current:${current.routeStatus.name} pre:${pre?.routeStatus?.name}");
         if (widget == current.page || current.page is HomePage){
             print("HomePage onResume...");
         } else if (widget == pre?.page || pre?.page is HomePage){
             print("HomePage onPause");
         }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('HomePage'),),
      body: Container(
        child: Column (
          children: [
            FractionallySizedBox(
              widthFactor: 1,
              child: Container(
                alignment: Alignment.center,
                child: Text('首页'),
                height: 45,
              ),
            ),
            Padding(
              padding: EdgeInsets.only(left: 10, right: 10),
              child:  FractionallySizedBox(
                widthFactor: 1,
                child: MaterialButton (
                    color: Colors.blue,
                    padding: EdgeInsets.all(10),
                    onPressed: (){
                      HiNavigator.instance().onJumpTo(RouteStatus.videoListPage, args: {"videoType": "scenery"});
                    },
                    child:Text('跳转到视频列表页面', style: TextStyle(color: Colors.white),)
                ),
              ))
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    HiNavigator.instance().removeRouteChangeListener(routeChangeListener);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    //print("didChangeAppLifecycleState ${state}");
    switch(state){
      case AppLifecycleState.resumed://可以展示UI
        print("didChangeAppLifecycleState onResume");
        break;
      case AppLifecycleState.inactive://不可交互
        print("didChangeAppLifecycleState inactive");
        break;
      case AppLifecycleState.paused://进入后台
        print("didChangeAppLifecycleState paused");
        break;
      case AppLifecycleState.detached://销毁的时候
        print("didChangeAppLifecycleState detached");
        break;
    }
  }
  //监听系统的明亮度发生变化
  @override
  void didChangePlatformBrightness() {
    super.didChangePlatformBrightness();
    print("didChangePlatformBrightness");
  }
}
//video_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_navigator/hi_navigator.dart';
import 'package:flutter_navigator/video_mo.dart';

class VideoListPage extends StatefulWidget {
  final String videoType;

  const VideoListPage(this.videoType, {Key key}) : super(key: key);

  @override
  _VideoListPageState createState() => _VideoListPageState();
}

class _VideoListPageState extends State<VideoListPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('VideoListPage'),),
      body: Container(
        child: Column(
          children: [
            Text('视频列表页面${widget.videoType}'),
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('点击跳转到视频详情页面'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.videoDetailPage, args: {'videoModel':VideoModel(100)});
                  }),
            )
          ],
        ),
      ),
    );
  }

}
//video_detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_navigator/video_mo.dart';

import 'hi_navigator.dart';

class VideoDetailPage extends StatefulWidget {
  final VideoModel videoModel;
  const VideoDetailPage(this.videoModel,{Key key}) : super(key: key);

  @override
  _VideoDetailPageState createState() => _VideoDetailPageState();
}

class _VideoDetailPageState extends State<VideoDetailPage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('VideoDetailPage'),),
      body: Container(
        child: Column (
          children: [
            Text('视频详情页面 ${widget.videoModel?.vid}'),
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('点击跳转到首页'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.homePage);
                  }),
            ),
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('点击跳转到登录页'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.loginPage);
                  }),
            ),
            Container(
              margin: EdgeInsets.only(left: 10, right: 10),
              child: MaterialButton(
                  minWidth: double.infinity,
                  child: Text('点击继续跳转详情页'),
                  onPressed: (){
                    HiNavigator.instance().onJumpTo(RouteStatus.videoDetailPage, args: {'videoModel':VideoModel(200)});
                  }),
            )
          ],
        ),
      ),
    );
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值