天天看點

Flutter命名路由及傳參的深度實踐與解讀Flutter命名路由及傳參的深度實踐與解讀

這裡寫自定義目錄标題

  • Flutter命名路由及傳參的深度實踐與解讀
    • 本次深入實踐的起因
    • 跳轉後點傳回黑屏或退出應用的BUG再現
      • BUG示例代碼:
      • 執行pop黑屏或退出應用的原由
      • 實作類初始化接收不到參數的原由
    • 深度實踐整理後的解讀
      • 深度實踐後的Flutter命名路由示例代碼
      • Flutter命名路由示例用到的路由方法
      • MaterialApp路由相關屬性配置要點
      • routes清單onGenerateRoute攔截器要點
      • 示例不同情況路由棧内容分析
    • 我的感悟

Flutter命名路由及傳參的深度實踐與解讀

在寫Flutter應用時,實作頁面間交叉跳轉時,通常多是使用命名路由,這樣更友善。但是經常會遇到某些情況不能達到理想的效果,網羅衆文基本都千篇一律,大多數是源自Flutter中文官網或幾個前期博文的加工再發,沒有針對Flutter的命名路由特性進行深度的分析。本人在近期項目需求中遇到了路由問題,順便對Flutter命名路由做了深度實踐,記錄發文,希望對後來者有所幫助!

.

本次深入實踐的起因

在我們的一個項目中有個頁面暫稱為“Page4”,會有不同的頁面在不同的路由位置跳轉到這個頁面,但是需要在這個頁面點确定按鈕時,跳轉到目前路由的前N個頁面,例如是“Page2”(Page4是經過Page2從首頁一路加載過來的),并刷“Page2”。然後要求在“Page2”執行pop方法應該可以傳回到“Page1”頁面。

期望流程及路由記錄如下:

  1. 從首頁進入到Page4:Home → Page1 → Page2→ Page3 → Page4
  2. 在Page4點确定按鈕:Home → Page1 → Page2(新建立的)
  3. 在Page2點傳回按鈕:Home → Page1

在Flutter的路由方法裡,我們發現Navigator.pushNamedAndRemoveUntil方法攜帶ModalRoute.withName參數,剛好是我們需要的。這個方法攜帶該參數後,是從最近向前删除路由到指定的路由為止(不是清空),然後再建立指定的頁面(新創就等于重新整理了)。然後我們在使用時,發現跳轉沒問題,跳轉後再點傳回出現了黑屏,網羅了一大堆原因均不吻合。後又遇到命名路由傳參問題,經仔細實踐,更深入了解路由的原理,最終一并得以解決。

雖然有其他實作方法可以達到目的,我們不在這裡讨論,在這隻探讨Flutter命名路由的深入實踐過程及得出的結果!

.

跳轉後點傳回黑屏或退出應用的BUG再現

BUG示例代碼:

main.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'routes.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '示範命名路由使用',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
      routes: routes,
      onGenerateRoute: onGenerateRoute,
    );
  }
}
           

.

home.dart
import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  Home({Key key}) : super(key: key);
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('命名路由示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Flutter 歡迎您!'),
            RaisedButton(
              child: Text("pushNamed跳page01頁"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments: '這是來自Home頁的資料');
              },
            ),
          ],
        ),
      ),
    );
  }
}

           

.

page01.dart
import 'package:flutter/material.dart';

class Page01 extends StatefulWidget {
  Page01({Key key, this.param}) : super(key: key);
  final String param;
  @override
  _Page01State createState() => _Page01State(msg:this.param);
}

class _Page01State extends State<Page01> {
  _Page01State({this.msg});
  String msg;

  @override
  void initState() {
    print('類初始化收到:' + (msg ?? '空'));     // 控制台輸出msg參數,監測傳值變化
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    String args = ModalRoute.of(context).settings.arguments;
    print('settings收到:' + (args ?? '空'));    // 控制台輸出args參數,監測傳值變化
    return Scaffold(
      appBar: AppBar(title: Text('頁面 page01')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('類初始化收到:' + (msg ?? '空')),
            Text('settings收到:' + (args ?? '空')),
            RaisedButton(
              child: Text("pushNamed跳page02頁"),
              onPressed: () {
                Navigator.pushNamed(context, '/page02', arguments: '這是來自page01頁的資料');
              },
            ),
          ],
        ),
      ),
    );
  }
}
           

.

page02.dart、page03.dart

與page01.dart頁代碼基本一樣,僅标題和傳遞參數略有差别,這裡不再複制

.

page04.dart
import 'package:flutter/material.dart';

class Page04 extends StatefulWidget {
  Page04({Key key, this.param}) : super(key: key);
  final String param;
  @override
  _Page04State createState() => _Page04State(msg:this.param);
}

class _Page04State extends State<Page04> {
  _Page04State({this.msg});
  String msg;

  @override
  void initState() {
    print('類初始化收到:' + (msg ?? '空'));     // 控制台輸出msg參數,監測傳值變化
    super.initState();
  }
  
  @override
  Widget build(BuildContext context) {
    String args = ModalRoute.of(context).settings.arguments;
    print('settings收到:'+(args ?? '空'));    // 控制台輸出args參數,監測傳值變化
    return Scaffold(
      appBar: AppBar(title: Text('頁面 page04')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('類初始化收到:' + (msg ?? '空')),
            Text('settings收到:' + (args ?? '空')),
            RaisedButton(     // A跳轉按鈕
              child: Text("保留路由建立page01頁"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments:'pushNamed');
              },
            ),
            RaisedButton(     // B跳轉按鈕
              child: Text("删部分路由後建立page01頁"),
              onPressed: () {
                Navigator.pushNamedAndRemoveUntil(context, '/page01', ModalRoute.withName('/home'), arguments:'pushNamedAndRemoveUntil');
              },
            ),
          ],
        ),
      ),
    );
  }
}
           

.

routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';


final routes = {
  '/home': (context) => Home(),
  '/page01': (context, {arguments}) => Page01(param:arguments),
  '/page02': (context, {arguments}) => Page02(param:arguments),
  '/page03': (context, {arguments}) => Page03(param:arguments),
  '/page04': (context, {arguments}) => Page04(param:arguments),
};

Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;
  print('目前通路路由名:$routeName');
  final Function pageContentBuilder = routes[routeName];
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = MaterialPageRoute(
        builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
      );
      return route;
    } else {
      final Route route = MaterialPageRoute(
        builder: (context) => pageContentBuilder(context),
      );
      return route;
    }
  }
  return null;
}
           

.

執行pop黑屏或退出應用的原由

從首頁home頁面,首次一路點選按鈕過來,在page04目前頁時,路由棧内順序應該是:

[’/home’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]

但實際上裡面沒有存在 ‘/home’,因為建立 home 頁面時沒有使用命名路由,是以路由棧内的名字是:

[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’],’/'是路由管理器預設給的初始化名字(具體名字不一定是這個)

在 page04.dart 的 A跳轉按鈕 事件中,我們使用的是 pushNamed 方法,實作的結果與我們預期的是一緻的。也就是之前路由中有一個 page02 頁面,在目前 page04 頁面之後再建立一個 page02(因為要重新整理,是以直接使用了建立),這樣傳回的話需要點5次傳回才到首頁,而期望是傳回2次即到首頁。是以嘗試使用 B跳轉按鈕 的事件。

點 A跳轉按鈕 後,路由棧内是:

[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]

在 page04.dart 的 B跳轉按鈕 事件中,我們使用的是 pushNamedAndRemoveUntil 方法,期望清掉路由棧中的 ‘page02’, ‘page03’, ‘page04’,然後建立一個新的 page02。

點 B跳轉按鈕 後期望路由棧内是:

[’/home’, ‘/page01’, ‘/page02’]

但目前代碼結果實際不是,經過實踐以為找不到 ‘/home’ 和 ‘/page01’ ,則把路由棧全清空了,實際是:

[’/page02’]

也就是隻有 page02 頁面了,沒有上一頁了,是以這是調用pop方法傳回上一頁,找不到 page01了,這是黑屏或退出現象的主因。

.

實作類初始化接收不到參數的原由

經過測試發現,如果使用上述命名路由的方式,雖然在路由Map中,page01-04都配置了參數傳遞,但是相應頁面中的實作類初始化接參都是空的。經過反複實踐,發現原因出在以下幾點:

  1. 路由Map中的“(context, {arguments}) => Page01(param:arguments)”這種寫法要配合路由攔截器才有效,否則傳遞不過去參數。
  2. main.dart 中 MaterialApp 配置了 routes 屬性并指向了 路由Map(routes.dart中的routes),這樣隻要在routes中能找到的頁面名字,就不會觸發路由攔截器,是以參數傳遞也就不可能有效。

    .

深度實踐整理後的解讀

.

深度實踐後的Flutter命名路由示例代碼

下面是示例應用截圖:

Flutter命名路由及傳參的深度實踐與解讀Flutter命名路由及傳參的深度實踐與解讀

.

代碼中關鍵位置标有詳細注釋,這均是經過實踐得出的結論,若有錯誤請以官方文檔為準。

main.dart
import 'package:flutter/material.dart';
import 'routes.dart';


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '示範命名路由使用',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      //home: Home(),
      // 應用初始化時第一個要加載的路由,通常稱為根路由,不使用這個配置會造成查不到根路由頁面的名稱
      //initialRoute: '/',

      // 命名路由Map,建立頁面是這裡有的就不會觸發onGenerateRoute事件,不使用則直接觸發onGenerateRoute事件
      //routes: routes,

      // 路由攔截器,産生新路由頁面時,如果在路由清單routes中查不到,則會觸發這個事件
      // 是以沒有給routes屬性指派的話,則執行任何一個命名路由相關的建立頁面方法都會觸發本事件
      onGenerateRoute: onGenerateRoute,
    );
  }
}
           

.

home.dart
import 'package:flutter/material.dart';


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

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

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('命名路由示例-首頁'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Flutter 歡迎您!'),
            RaisedButton(
              child: Text("pushNamed建立page01頁"),
              onPressed: () {
                Navigator.pushNamed(context, '/page01', arguments: '這是來自Home頁的資料');
              },
            ),
          ],
        ),
      ),
    );
  }
}
           

.

page01.dart
import 'package:flutter/material.dart';


class Page01 extends StatefulWidget {

  // 初始化時接收建立類時傳遞的參數,儲存在param
  Page01({Key key, this.param}) : super(key: key);

  // 這個一定要是終極類型,因為本類直接繼承自StatefulWidget(被标記為@immutable)
  // 為适用命名路由表,這裡隻設一個參數,需要多個可以傳對象或Map
  // 名字不是必須為arguments,但是要與路由配置中參數名稱一緻,見routes.dart的'/page01'一行
  final String param;

  // 為_Page01State類中可以在build外友善擷取param,這裡傳遞this.param給_Page01State的msg
  @override
  _Page01State createState() => _Page01State(msg:this.param);
}

class _Page01State extends State<Page01> {

  // 初始化擷取Page01傳遞過來的param給本類的msg
  _Page01State({this.msg});

  // 這裡可以不是終極類型
  String msg = '空';

  @override
  void initState() {

    // 控制台輸出msg參數,監測傳值變化
    print('Page01類初始化收到:$msg');
    super.initState();
  }
  @override
  Widget build(BuildContext context) {

    // 通過ModalRoute.of(context).settings.arguments擷取的參數,僅在build内可以
    String args = ModalRoute.of(context).settings.arguments;
    if (args == null) args = '空';

    // 控制台輸出args參數,監測傳值變化
    print('page01頁settings收到:$args');
    return Scaffold(
      appBar: AppBar(

        // 标記是第幾個頁面,便于測試識别
        title: Text('頁面 page01'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 頁面輸出類初始化收到的參數情況,識别更直覺
            Text('類初始化收到:$msg'),

            // 頁面輸出類在build内使用ModalRoute的settings收到的參數情況,識别更直覺
            Text('settings收到:$args'),
            RaisedButton(
              child: Text("pushNamed建立page02頁"),
              onPressed: () {

                // 通過pushNamed方法跳轉到page02,并傳參“這是來自page01頁的資料”
                Navigator.pushNamed(context, '/page02', arguments: '這是來自page01頁的資料');
              },
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: () {

          // 通過popUntil方法直接傳回首頁home,不傳參,便于測試
          // 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
          Navigator.popUntil(context, ModalRoute.withName('/'));
        },
        child: Text('首頁'),
      ),
    );
  }
}
           

page02.dart、page03.dart

與page01.dart代碼基本相同,隻是傳參與描述與頁面名稱相關的更改一下即可,這裡就不複制

.

page04.dart
import 'package:flutter/material.dart';


class Page04 extends StatefulWidget {

  // 初始化時接收建立類時傳遞的參數,儲存在param
  Page04({Key key, this.param}) : super(key: key);

  // 這個一定要是終極類型,因為本類直接繼承自StatefulWidget(被标記為@immutable)
  // 為适用命名路由表,這裡隻設一個參數,需要多個可以傳對象或Map
  // 名字不是必須為arguments,但是要與路由配置中參數名稱一緻,見routes.dart的'/page01'一行
  final String param;

  // 為_Page01State類中可以在build外友善擷取param,這裡傳遞this.param給_Page01State的msg
  @override
  _Page04State createState() => _Page04State(msg:this.param);
}

class _Page04State extends State<Page04> {

  // 初始化擷取Page01傳遞過來的param給本類的msg
  _Page04State({this.msg});

  // 這裡可以不是終極類型
  String msg = '空';

  @override
  void initState() {

    // 控制台輸出msg參數,監測傳值變化
    print('Page04類初始化收到:$msg');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    // 通過ModalRoute.of(context).settings.arguments擷取的參數,僅在build内可以
    String args = ModalRoute.of(context).settings.arguments;
    if (args == null) args = '空';

    // 控制台輸出args參數,監測傳值變化
    print('page02頁settings收到:$args');
    return Scaffold(
      appBar: AppBar(

        // 标記是第幾個頁面,便于測試識别
        title: Text('頁面 page04'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 頁面輸出類初始化收到的參數情況,識别更直覺
            Text('類初始化收到:$msg'),

            // 頁面輸出類在build内使用ModalRoute的settings收到的參數情況,識别更直覺
            Text('settings收到:$args'),

            RaisedButton(
              child: Text("A:保留路由建立page02頁"),
              onPressed: () {

                // 通過pushNamed方法建立page02,并傳參方法名
                // 此方法儲存原路由棧,新建立一個page02頁面,并壓入路由棧
                // 執行後,路由棧内應該是['/home', '/page01', '/page02', '/page03', '/page04', '/page02']
                Navigator.pushNamed(context, '/page02', arguments:'pushNamed');
              },
            ),
            RaisedButton(
              child: Text("B:删部分路由後建立page02頁"),
              onPressed: () {

                // 通過pushNamedAndRemoveUntil方法帶ModalRoute.withName配置建立page02,并傳參方法名
                // 此方法将從路由棧頂部(最近)開始逐一彈出(删除),直到遇到'/home'停止删除('/home'不會被删除),再建立page02
                // 執行後,路由棧裡就隻剩下了['/home', '/page01', '/page02'],page02的上一頁就是page01,而不是目前頁page04
                Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'pushNamedAndRemoveUntil');
              },
            ),
            RaisedButton(
              child: Text("C:清空路由棧後建立page02頁"),
              onPressed: () {

                // 通過pushNamedAndRemoveUntil帶(Route<dynamic> route)=>false配置建立page02,并傳參方法名
                // 此方法将清空路由棧(彈出/删除所有路由),然後再建立page02
                // 執行後,路由棧裡就隻有['/page02'],page02變成了根頁面,沒有上一頁了
                Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'pushNamedAndRemoveUntil');
              },
            ),
            RaisedButton(
              child: Text("D:替換目前路由無轉場效果建立page02頁"),
              onPressed: () {

                // 通過pushReplacementNamed方法跳轉到page02,并傳參方法名
                // 此方法将在原路由棧替換掉目前的路由(page04),然後再建立page02
                // 執行後,路由棧裡是['/home', '/page01', '/page02', '/page03', '/page02'],page02的上一頁就是page03,而不是目前頁page04
                Navigator.pushReplacementNamed(context, '/page02', arguments:'pushReplacementNamed');
              },
            ),
            RaisedButton(
              child: Text("E:替換目前路由有轉場效果建立page02頁"),
              onPressed: () {

                // 通過popAndPushNamed方法跳轉到page02,并傳參方法名
                // 此方法與pushReplacementNamed方法最終在路由棧裡的結果相同
                // 隻是執行過程中,此方法有轉場效果,而pushReplacementNamed方法沒有
                Navigator.popAndPushNamed(context, '/page02', arguments:'pushReplacementNamed');
              },
            ),
            RaisedButton(
              child: Text("F:删除部分路由回到page02頁"),
              onPressed: () {

                // 通過popUntil方法傳回到page02,此方法無法傳參,也不觸發路由攔截器
                // 此方法與pushNamedAndRemoveUntil方法最終在路由棧裡的結果相同
                // 隻是page02不是建立的,不重新整理頁面保持原有狀态,等同于連續的pop;而pushNamedAndRemoveUntil是建立頁面,類似于重新整理了頁面
                // 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
                Navigator.popUntil(context, ModalRoute.withName('/page02'));
              },
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: () {

          // 通過popUntil方法直接傳回首頁home,不傳參,便于測試
          // 此方法指定的頁面名稱必需在目前路由表中存在,否則會黑屏(例如之前有過清空路由操作)
          Navigator.popUntil(context, ModalRoute.withName('/'));
        },
        child: Text('首頁'),
      ),
    );
  }
}
           
routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';


final routes = {
  // 如果标注'/',而main.dart中又沒有指定home頁,但指定routes為本Map,則應用初始化時自動加載'/'指定的頁面
  // 若是main.dart中指定了home頁,指定routes為本Map,這裡就不要有'/',否則會報警告
  '/': (context) => Home(),

  // 這裡的“arguments”必須是這個名字,否則取不到通過“pushNamed”等方法傳遞的參數
  // 這裡的“param”是與對應的類中的接收參數命名對應的,不是一定要使用“arguments”這個名字
  // 這裡的參數傳遞僅對使用onGenerateRoute路由攔截器,并在攔截器内寫入參數傳遞的代碼并觸發了攔截器時才有效
  // 這裡的參數傳遞是傳遞給類初始化的參數,與ModalRoute.of(context).settings.arguments無關
  //
  // 若不需要給類初始化傳參,可以寫成 '/page01': (context) => Page01()這樣
  // 同樣可以用ModalRoute.of(context).settings.arguments接收“pushNamed”等方法傳遞的參數
  '/page01': (context, {arguments}) => Page01(param:arguments),
  '/page02': (context, {arguments}) => Page02(param:arguments),
  '/page03': (context, {arguments}) => Page03(param:arguments),
  '/page04': (context, {arguments}) => Page04(param:arguments),
};

// 路由攔截器,隻有在MaterialApp的routes屬性沒有指派的情況下,才會每次建立頁面都觸發
// 如果指定了routes屬性,那隻有routes中查不到的時候才會觸發
Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;

  // 控制台輸出目前攔截的路由名稱,友善清晰的了解路由建立流暢
  print('目前通路路由名:$routeName');

  // 為了避免報錯,将在命名路由清單查不到的都指向首頁(根頁)
  if (!routes.containsKey(routeName)) routeName = '/';

  // 擷取頁名對應的類
  final Function pageContentBuilder = routes[routeName];

  if (settings.arguments != null) {

    // 如果帶有參數,則調用該類的時候,需要将參數傳遞過去
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context, arguments: settings.arguments),

      // 這一行是命名路由順暢應用的關鍵,否則新建立的頁面壓入路由棧的将不是自己定義的名字
      // 主要是使用 settings.name 的作用,為新建立的頁面命名
      settings: settings,
    );
  } else {

    // 如果沒有帶參數,則調用該類的時候,不需要傳遞參數
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context),
      settings: settings,
    );
  }
}
           

.

Flutter命名路由示例用到的路由方法

  1. pushNamed

    保留目前路由棧,在命名路由清單中,根據名稱查找對應的頁面實作類并建立該頁,然後壓入路由棧。

    可以攜帶參數(非必傳),若要傳參必需以arguments命名,要傳多個參數可封裝成對象或Map(下同)。

    調用示例:

Navigator.of(context).pushNamed("/page1", arguments:'這裡是參數');
// 或
Navigator.pushNamed(context, '/page01', arguments:'這裡是參數');
           

上面示例表示保留目前路由棧建立page01,向page01頁傳遞字元串參數“這裡是參數”五個漢字。

  1. pushNamedAndRemoveUntil

    根據條件删除目前路由棧中的路由,然後建立新頁。

    ⑴ 使用 ModalRoute.withName() 參數:

    從目前路由棧頂部(最近的)開始彈出(删除)路由,直到指定的頁面為止,保留指定的頁面。

    調用示例:

Navigator.of(context).pushNamedAndRemoveUntil('/page02', ModalRoute.withName('/page01'), arguments:'這裡是參數');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'這裡是參數');
           

上面示例表示從目前頁開始向前依次删除路由直到page01為止,保留page01,然後建立page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。

.

⑵ 使用 (Route route)=>false 參數:

将目前路由棧清空

調用示例:

Navigator.of(context).pushNamedAndRemoveUntil('/page02', (Route<dynamic> route)=>false, arguments:'這裡是參數');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'這裡是參數');
           

上面示例表示從将目前路由棧中所有路由删除,然後建立page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。

  1. pushReplacementNamed

    在目前路由棧中删除目前路由,然後建立新頁。也就是使用建立頁的路由替換目前路由。此方法沒有頁面轉場的效果。

    調用示例:

Navigator.of(context).pushReplacementNamed('/page02', arguments:'這裡是參數');
// 或
Navigator.pushReplacementNamed(context, '/page02', arguments:'這裡是參數');
           

上面示例表示将目前路由棧中的目前頁路由替換為建立的page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。沒有轉場效果

  1. popAndPushNamed

    此方法與pushReplacementNamed在路由棧中最終結果相同,隻是在進入建立頁面時,有轉場效果。

    調用示例:

Navigator.of(context).popAndPushNamed('/page02', arguments:'這裡是參數');
// 或
Navigator.popAndPushNamed(context, '/page02', arguments:'這裡是參數');
           

上面示例表示将目前路由棧中的目前頁路由替換為建立的page02,向page02頁傳遞字元串參數“這裡是參數”五個漢字。有轉場效果

  1. popUntil

    回到之前曾經打開的指定頁,不重建不重新整理頁面,保持原頁面狀态,删除指定頁之後的所有路由,不能攜帶參數。

    調用示例:

Navigator.of(context).popUntil(ModalRoute.withName('/page02'));
// 或
Navigator.popUntil(context, ModalRoute.withName('/page02'));
           

上面示例表示回到曾經打開的page02,删除page02頁之後的所有路由

.

MaterialApp路由相關屬性配置要點

在MaterialApp中與本例相關的屬性有 home、initialRoute、routes、onGenerateRoute 四個。

  1. home:指定應用啟動後首次加載的頁面,是類名,不是命名路由的頁面名稱。
  2. initialRoute:同 home,但是指定的是命名路由routes中的頁面名,不是類名。
  3. routes:命名路由清單,是一個Map類型,頁面名映射該頁對應的實作類名。
  4. onGenerateRoute:命名路由攔截器,隻有在routes中查不到頁名時才會觸發這個事件。

▲ 在 home 中指定的頁面,該頁在 routes 中的頁面名稱是不能壓入路由棧的。

▲ 因 home 與 initialRoute 作用基本相同,是以不能同時使用,否則會産生沖突。

▲ 隻有配置了 routes 或 啟用了 onGenerateRoute 後,才可以使用 initialRoute,否則無處查找頁面名會産生錯誤。

▲ 若配置了 routes、home 後,routes 清單中有寫 ‘/’ 映射,将會産生沖突,因為命名路由規則中,’/’ 代表的是根頁,也是初始化應用首先要加載的頁面。是以配置了 routes 就盡量不要配置 home。

▲ 即使在 routes 清單中給實作類配置了參數,也是不能給實作類的初始化參數傳遞參數的。必須配置 onGenerateRoute 并在該事件的方法内實作參數傳遞才可以實作,否則隻能在 build 内通過 ModalRoute.of(context).settings.arguments 擷取傳遞的參數。

▲ 若希望每次通過指令路由建立頁面都能走路由攔截器,那定義routes清單後,不要配置routes屬性。由攔截器去加載routes清單,否則在routes清單存在建立頁名的時候,路由攔截器是不會被觸發的。

.

routes清單onGenerateRoute攔截器要點

通常為了友善,都是把這兩個放在一個獨立的檔案内。

先看一下routes清單的定義,粘貼部分代碼如下:

final routes = {
  '/': (context) => Home(),
  '/page01': (context) => Page01(),
  '/page02': (context, {arguments}) => Page02(param:arguments),
};
           

▲ 上面代碼中 ‘/’ 是應用初始化時,在沒有配置 initialRoute(在MaterialApp中)和 home 時自動加載的頁面,若配置了initialRoute則會加載initialRoute指定的頁面。是以盡量我們把應用的預設加載頁命名為 ‘/’,這更利于代碼閱讀與交流(其他每個頁名前面的 / 也不是必需的)。

▲ 上面代碼中的 {arguments} 和 param:arguments 是為了給頁面實作類(如:Page02)的初始化參數傳遞參數而寫的,arguments 這個名字不能更改,param 必須與 Page02 類中的接收參數名稱一緻。這個配置要最終實作參數傳遞到實作類初始化參數中,必須有 onGenerateRoute 攔截器配合才能實作。

▲ 如果隻需要在 build 内通過 ModalRoute.of(context).settings.arguments 擷取參數,則在這裡不需要配置參數傳遞,像 ‘/’、’/page01’ 那樣寫即可。

再看一下onGenerateRoute攔截器的定義,粘貼示例代碼如下:

Route<dynamic> onGenerateRoute(RouteSettings settings) {
  String routeName = settings.name;
  if (!routes.containsKey(routeName)) routeName = '/';
  final Function pageContentBuilder = routes[routeName];

  if (settings.arguments != null) {
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
      settings: settings,
    );
  } else {
    return MaterialPageRoute(
      builder: (context) => pageContentBuilder(context),
      settings: settings,
    );
  }
           

示例攔截器中,先擷取路由名稱(因在MaterialApp中沒有配置routes,是以每次建立命名路由頁面都會觸發這裡)給routeName,然後查詢是否存在于routes的key中,如果不在則把routeName的值更改為 ‘/’,後面就會加載根頁。這是為了處理代碼意外,避免名稱錯誤查不到而造成應用停止。

然後使用 routeName 到 routes 擷取映射的實作類給 pageContentBuilder。

settings.arguments 是通過 pushNamed 等命名路由方法傳遞的參數,在這裡通過這個擷取。

接下來判斷 settings.arguments 是否為 null,如果不是 null 說明有傳遞參數,那在 MaterialPageRoute 加載路由頁面時就要給實作類傳參,否則就不需要。

settings: settings 這句是重中之重, 大部分文章中都沒有這句,這句是為了讓通過攔截器建立的新頁面也有路由名稱,主要使用的是 settings.name 屬性。如果新建立的頁面沒有給名字,後續的使用頁面名稱操作路由時,就可能會出現問題。

.

示例不同情況路由棧内容分析

以打開應用後,以從home頁一路點選按鈕到page04為前提,在示例中頁面page04點選不同的按鈕,對路由棧内的情況有如下影響(以數組格式代替垂直表示的棧圖,數組左側為棧底):

  1. page04呈現後的路由棧:

    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]

  2. 點選按鈕 A 後的路由棧(使用pushNamed):

    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]

  3. 點選按鈕 B 後的路由棧(使用pushNamedAndRemoveUntil):

    [’/’, ‘/page01’, ‘/page02’]

  4. 點選按鈕 C 後的路由棧(使用pushNamedAndRemoveUntil):

    [’/page02’]

  5. 點選按鈕 D 後的路由棧(使用pushReplacementNamed):

    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]

  6. 點選按鈕 C 後的路由棧(使用popAndPushNamed):

    [’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’]

  7. 點選按鈕 D 後的路由棧(使用popUntil):

    [’/’, ‘/page01’, ‘/page02’]

    .

我的感悟

雖說天下文章一大抄,但是技術問題還是需要真正的實踐才能知曉真實的結果。很多技術源于國外,國内資料多為業餘翻譯版。當遇到問題很難搞定的時候,别忘記放下資料深入實踐一下,可能得到的結論更直接。更多可以嘗試自行翻譯外文官網的相關片段,或許可以得到不一樣資訊。這裡還是要感謝翻譯外文技術資料的朋友們,辛苦了!

希望本文能給相關的後來者一定的解惑,若對本文内容有異議,請以官方文檔為準。