Flutter路由导航与页面生命周期

页面在flutter中是用路由(Route)的概念来表示的,Route是对屏幕界面的抽象,每一个页面在页面栈中对应一个Route对象。 Flutter使用Navigator来管理页面栈,通过页面栈的入栈和出栈来显示页面跳转和返回,Navigator提供了很多方法来管理Route的出栈和入栈,如Navigator.push Navigator.pushNamed Navigator.pop等方法。基本上Navigator提供两种方式来实现页面跳转:

  • 直接push Route对象
  • 通过name来push相应页面
1. 跳转页面基本用法
Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new SecondScreen()),
  );

可以直接使用MaterialPageRoute来实现Route对象,MaterialPageRoute需要一个builder参数来生成页面widget,它通过自适应的平台的过渡效果来切换页面。默认情况下,当一个路由push进来,上一个路由将保留在内存中,如果想释放所有资源,可以将MaterialPageRoute的 maintainState属性设置为 false。

另一种是name来push页面,可以预先定义页面name,如下:

new MaterialApp(
      title: 'Navigation',
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => new HomeScreen(),
        '/second': (BuildContext context) => new SecondScreen(),
      },
    );

之后可以使用下面方法切换页面:

Navigator.of(context).pushNamed('/second');
2. 页面间传值

现实情况下,往往需要上一级页面需要携带参数值到下一级页面。

  1. 如果使用第一种方式直接push Route对象的方法传值比较简单,需要在下一级页面widget构造方法定义接收的参数,然后build widget的时候使用带参数的构造方法即可,如:
class SecondScreen extends StatelessWidget {
  final String param1;
  final int param2;

  const SecondScreen({Key key, this.param1, this.param2}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return  ...;
  }
}
Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new SecondScreen(param1:'xxxxx', param2: 1234)),
  );
  1. 如果使用name来跳转,需要在pushNamed方法中添加arguments参数,这里的routeName和arguments将会被封装成RouteSettings对象传递到Route构建方法中,如:
Navigator.pushNamed(context, routeName, arguments: args);

然而如果在MaterialApp中预先使用routes参数定义了routeName和WidgetBuilder的对应关系后,系统会自动生成route而不会回调给我们,从而无法给生成的widget携带参数。
我采用的方法是不预先设置routes,而是通过onGenerateRoute(RouteSettings settings)回调来生成route对象,RouteSettings对象携带了前面传递的routeName和arguments。
首先,定义一个结构和一个Map存储页面信息和pageBuilder, 这里的shouldLogin是处理业务中是否需要先登录情况,可以根据实际情况改造MyPageBuilder:

class MyPageBuilder {
  final Function pageBuilder;
  final bool shouldLogin;

  MyPageBuilder(this.pageBuilder, this.shouldLogin);
}

//定义routeName与MyPageBuilder关系,并处理参数
static Map<String, MyPageBuilder> routes = {
    PAGE_SPLASH: MyPageBuilder(() => new SplashPage(), false),
    PAGE_SECOND: MyPageBuilder((args) {
      int index = 0;
      if (args != null) {
        try {
          index = int.parse(args['index']);
        } catch (e) {
          index = 0;
        }
      }
      return new SecondScreen(
        param2: index,
      );
    }, false),
  };

然后在onGenerateRoute(RouteSettings settings)生成Route对象,在生成Route时,需要设置settings参数,用于标识该Route的信息,后面会介绍作用。

 PageRoute onGenerateRoute(RouteSettings settings) {
    Logger.d(settings.toString());
    String routeName = settings.name;

    MyPageBuilder myPageBuilder = routes[routeName];
    if (pageBuilderWithLogin == null) {
      Logger.e('router name $routeName can not recognized!!!');
      return null;
    }
    Function pageBuilder = myPageBuilder.pageBuilder;
    if (pageBuilder is Widget Function(Map<String, String> args)) {
      dynamic args = settings.arguments;
      return MaterialPageRoute(
          builder: (context) => pageBuilder(args), settings: settings);
    } else {
      return MaterialPageRoute(
          builder: (context) => pageBuilder(), settings: settings);
    }

这样可以将参数传递给目标widget了。
实际上,系统在通过name导航的时候,首先查找在routes中是否有对应的配置,如果有,直接调用pageBuilder方法生成widget,进而生成Route对象;如果没查到,将routeName和arguments包装乘RouteSettings,传递调用onGenerateRoute(RouteSettings settings),返回Route。我们就是在onGenerateRoute方法中处理传参的。

3. 页面返回及回传参数
  1. 如果无需返回值,直接调用pop方法即可
Navigator.pop(context);
  1. 如果需要返回值,需要增加一个参数
Navigator.pop(context, 'back param');

接收页面需要在跳转时使用await接收该返回参数,如:

jumpAndCallback: () async {
  String result = await Navigator.push(
    context,
    new MaterialPageRoute(
      builder: (context) => new SecondScreen(),
    ),
  );

 print('back params  is $result');
}
4. Navigator其他管理栈方法
  1. pushReplacement / pushReplacementNamed
    替换栈顶Route,相当于先pop栈顶,再push新的页面
  2. popUntil
    将制定页面元素上方的元素全部pop,该方法接收一个参数RoutePredicate,它是一个方法,返回bool,判定是否已经到达了制定元素,这里使用name方式标记Route就非常方便使用ModalRoute.withName(untilName)来实现这个RoutePredicate方法,也是上文中需要将RouteSettings添加到Route中的原因。
Navigator.popUntil(context, ModalRoute.withName('/initScreen'));
  1. pushAndRemoveUntil / pushNamedAndRemoveUntil
    顾名思义,该方法先pop到制定页面元素,然后再push一个新的页面。可以使用该方法pop所有页面再打开首页,可以达到回到首页并刷新首页的目的。
  2. canPop
    判断是否可以安全地pop掉当前页面,canPop只有在栈中只有一个元素的时候返回 false, 其它都是 true.
  3. maybePop
    如果你pop掉最后一个页面元素,那整个app就显示一片黑色。所以不应该pop掉栈底的元素,用maybePop可以帮助你,当只有一个元素的时候不会pop,其他情况会pop掉。

还有其他方法,可以查看源码和注释,基本都能看明白,这里就不赘述了。

5. 页面生命周期

原生开发中,经常需要在页面页面重新可见和不可见状态的生命周期回调做一些事情,比如页面数据刷新和状态保存和恢复等。如android Activity的onResume和onPause。
flutter在StatefulWidget中有创建state和销毁state的回调周期函数,initState和dispose方法,但 flutter没有直接在widget中给出类似onResume onPause的生命周期,好在flutter在Route切换的时候有一些监听可以获取到这两个生命周期。
前面有同学已经将State的生命周期回调封装到了一个pub库中,flutter_lifecycle_state,有兴趣的可以去看看。

  1. WidgetsBindingObserver
    首先可以使用WidgetsBindingObserver来监听Application lifecycle,如应用后台和应该重新从后台恢复。
 class AppLifecycleReactor extends StatefulWidget {
   const AppLifecycleReactor({ Key key }) : super(key: key);

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

 class _AppLifecycleReactorState extends State<AppLifecycleReactor> with WidgetsBindingObserver {
   @override
   void initState() {
     super.initState();
     WidgetsBinding.instance.addObserver(this);
   }

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

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        onResume();
        break;
      case AppLifecycleState.paused:
        onPause();
        break;
      default:
        break;
    }
  }

  void onResume() {}

  void onPause() {}

   @override
   Widget build(BuildContext context) {
     return Text('Last notification: $_notification');
   }
 }
  1. MaterialApp中navigatorObservers监听
    navigatorObservers需要一个NavigatorObserver列表,我们可以继承NavigatorObserver来监听回调。
MaterialApp(
      navigatorObservers: [routeObserver],
      onGenerateRoute: AppNavigator.onGenerateRoute,
      initialRoute: AppNavigator.PAGE_SPLASH,
    )
class MyObserver extends NavigatorObserver {
  factory MyObserver() => _getInstance();
  static MyObserver get instance => _getInstance();
  static MyObserver _instance;

  BuildContext curContext;

  MyObserver._internal() {}
  static MyObserver _getInstance() {
    if (_instance == null) {
      _instance = new MyObserver._internal();
    }
    return _instance;
  }

  String topRouteName = "";

  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    notifyHide(previousRoute);
    notifyShow(route);
  }

  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    notifyHide(route);
    notifyShow(previousRoute);
  }

  void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
    notifyHide(route);
    notifyShow(previousRoute);
  }

  /// The [Navigator] replaced `oldRoute` with `newRoute`.
  void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
    notifyHide(oldRoute);
    notifyShow(newRoute);
  }

  void notifyShow(Route<dynamic> route) {}

  void notifyHide(Route<dynamic> route) {}

}

其中didPush,didPop,didRemove,didReplace是Navigator相应动作的回调,通过这些方法可以知道哪个Route正在出栈,哪个Route正在入栈。Route中可以访问RouteSettings对象获取Route name。然后通知相应的Route中widget就行了,可以采用广播等方式通知到相应的State。

6. 参考资料

Flutter中文网
Flutter 官网
Flutter 路由和导航
flutter_lifecycle_state

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • 猗嗟昌兮,颀而长兮。 抑若扬兮,美目扬兮。 巧趋跄兮,射则臧兮。 猗嗟名兮,美目清兮。 仪既成兮。终日射侯, 不出...
    静棠阅读 375评论 0 0
  • 天边等着日暮, 天的边际, 酝酿着日暮的温柔, 如你嫣然一笑。 湖边等着明月, 湖的中心, 凝结着月色的皎洁, 如...
    我的发呆日记阅读 168评论 0 0
  • 恭喜 真拭悟慧 获得得到15专栏分享群的首期“周末原创分享”活动的第一名! 获奖文章: 要不要树立目标?我终于找到...
    GEEKYANG阅读 716评论 0 0
  • 一丝丝的不确定来源于你的举棋不定,源于你的若近若离,源于你的徘徊游离。将对方纳入未来的考量是爱一个人的体现,觉得不...
    沫戚阅读 82评论 0 0
  • 昨天是今年的七夕情人节,我度过了第一个有男朋友的情人节。 并没有什么惊喜和不一样的感悟,出去吃顿饭,然后回家,第二...
    善良的老约翰阅读 305评论 0 0