Flutter - 路由管理 - 01 - 基礎使用

_樟清_發表於2019-06-12

參考 book.flutterchina.club/chapter2/fl…

1. 示例

路由(Route)在移動開發中通常指頁面(Page),在Android中通常指一個Activity。所謂路由管理,就是管理頁面之間如何跳轉,通常也可被稱為導航管理。這和原生開發類似,無論是Android還是iOS,導航管理都會維護一個路由棧,路由入棧(push)操作對應開啟一個新頁面,路由出棧(pop)操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //導航到新路由
              Navigator.push(context, MaterialPageRoute(builder: (context) {
                return SecondRoute();
              }));
            },
            child: Text("進入第二頁"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

MaterialPageRoute繼承自PageRoute類,PageRoute類是一個抽象類,表示佔有整個螢幕空間的一個模態路由頁面,它還定義了路由構建及切換時過渡動畫的相關介面及屬性。MaterialPageRouteMaterial元件庫的一個Widget,它可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫:

對於Android,當開啟新頁面時,新的頁面會從螢幕底部滑動到螢幕頂部;當關閉頁面時,當前頁面會從螢幕頂部滑動到螢幕底部後消失,同時上一個頁面會顯示到螢幕上。 對於iOS,當開啟頁面時,新的頁面會從螢幕右側邊緣一致滑動到螢幕左邊,直到新頁面全部顯示到螢幕上,而上一個頁面則會從當前螢幕滑動到螢幕左側而消失;當關閉頁面時,正好相反,當前頁面會從螢幕右側滑出,同時上一個頁面會從螢幕左側滑入。 下面我們介紹一下MaterialPageRoute建構函式的各個引數的意義:

  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
複製程式碼

builder是一個WidgetBuilder型別的回撥函式,它的作用是構建路由頁面的具體內容,返回值是一個widget。我們通常要實現此回撥,返回新路由的例項。

settings包含路由的配置資訊,如路由名稱、是否初始路由(首頁)。

maintainState:預設情況下,當入棧一個新路由時,原來的路由仍然會被儲存在記憶體中,如果想在路由沒用的時候釋放其所佔用的所有資源,可以設定maintainStatefalse

fullscreenDialog表示新的路由頁面是否是一個全屏的模態對話方塊,在iOS中,如果fullscreenDialogtrue,新頁面將會從螢幕底部滑入(而不是水平方向)。

2. Navigator

Navigator是一個路由管理的widget,它通過一個棧來管理一個路由widget集合。通常當前螢幕顯示的頁面就是棧頂的路由。Navigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:

1. Future push(BuildContext context, Route route)

將給定的路由入棧(即開啟新的頁面),返回值是一個Future物件,用以接收新路由出棧(即關閉)時的返回資料。

2. bool pop(BuildContext context, [ result ])

將棧頂路由出棧,result為頁面關閉時返回給上一個頁面的資料。

Navigator還有很多其它方法,如Navigator.replaceNavigator.popUntil等,詳情請參考API文件或SDK原始碼註釋,在此不再贅述。

3. 例項方法

Navigator類中第一個引數為context的靜態方法都對應一個Navigator的例項方法, 比如Navigator.push(BuildContext context, Route route)等價於Navigator.of(context).push(Route route) ,下面命名路由相關的方法也是一樣的。

3. 命名路由

​ 命名路由(Named Route)即給路由起一個名字,然後可以通過路由名字直接開啟新的路由。這為路由管理帶來了一種直觀、簡單的方式。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      //home: MainRoute(),
      //註冊路由表
      routes: {
          /// '/'是特殊地址,第一個頁面
        "/" :(context) => MainRoute(),
        "new_page": (context) => SecondRoute(),
      },
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () async {
              //導航到新路由
              var result = await Navigator.pushNamed(context, "new_page");
              debugPrint("返回:$result");
            },
            child: Text("進入第二頁"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context, "結束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}
複製程式碼

命名路由的最大優點是直觀,我們可以通過語義化的字串來管理路由。

但其有一個明顯的缺點:不能直接傳遞路由引數。假設SecondRoute,需要接受一個字串引數tip,然後再在螢幕中心將tip的內容顯示出來。因為命名路由需要提前註冊到路由表中,所以就無法動態修改tip引數。但是後面的版本已經支援了引數,看下面

命名路由引數

在Flutter最初的版本中,命名路由是不能傳遞引數的,後來才支援了引數;下面展示命名路由如何傳遞並獲取路由引數:

我們先註冊一個路由:

 routes:{
   "new_page": (context) => SecondRoute(),
  } ,
複製程式碼

在開啟路由時傳遞引數

Navigator.of(context).pushNamed("new_page", arguments: "hi");
複製程式碼

在路由頁通過RouteSetting物件獲取路由引數:

class SecondRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //獲取路由引數  
    var args=ModalRoute.of(context).settings.arguments
    //...省略無關程式碼
  }
}
複製程式碼

2. 自定義路由切換動畫

Material庫中提供了MaterialPageRoute,它在Android上會上下滑動切換。如果想自定義路由切換動畫,可以使用PageRouteBuilder

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () async {
              //導航到新路由
              var result = await Navigator.push(
                context,
                PageRouteBuilder(
                  ///動畫時間
                  transitionDuration: Duration(milliseconds: 500),
                  pageBuilder: (BuildContext context, Animation animation,
                      Animation secondaryAnimation) {
                    ///平移
                    return SlideTransition(
                      ///Tween:在補間動畫中,定義開始點結束點
                      position: new Tween<Offset>(
                        begin: const Offset(1.0, 0.0),
                        end: const Offset(0.0, 0.0),
                      ).animate(animation),
                      child: SecondRoute(),
                    );
                  },
                ),
              );
              debugPrint("返回:$result");
            },
            child: Text("進入第二頁"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context, "結束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

同時我們也可以對動畫進行組合

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () async {
              //導航到新路由
              var result = await Navigator.push(
                context,
                PageRouteBuilder(
                  ///動畫時間
                    transitionDuration: Duration(milliseconds: 500),
                    pageBuilder: (BuildContext context, Animation animation,
                        Animation secondaryAnimation) {
                      ///透明漸變與旋轉
                      return new FadeTransition(
                        opacity: animation,
                        child: new RotationTransition(
                          turns: new Tween<double>(begin: 0.5, end: 1.0)
                              .animate(animation),
                          child: SecondRoute(),
                        ),
                      );
                    },),
              );
              debugPrint("返回:$result");
            },
            child: Text("進入第二頁"),
          )
        ],
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context, "結束");
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

3. 注意點

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一個頁面"),
            RaisedButton(
              onPressed: ()  {
                  ///Navigator.push內部其實就是 Navigator.of(context).push
                  Navigator.of(context).push(MaterialPageRoute(builder: (_){
                    return new SecondRoute();
                  }));
              },
              child: Text("進入第二頁"),
            )
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

這段程式碼執行會出現錯誤:

路由異常

​ 問題關鍵點在於Navigator operation requested with a context that does not include a Navigator.(導航操作請求使用了不包含Navigator的上下文context)

Navigator實際上也是一個Widget,這個異常出現在Navigator.of(context)路由器的獲取上,而這句程式碼會從當前的context的父級一層層向上去查詢一個Navigator,我們當前傳遞的context就是MyApp,它的父級是root——UI根節點。Navigator這個widget的並不是由root建立的,因此在root下一級的上下文中無法獲得Navigator

在之前所有的路由案例中,我們的上下文是MainRoute,它的父級是MaterialApp。MaterialApp內部就會建立一個Navigator。

MaterialApp->_MaterialAppState->WidgetsApp->_WidgetsAppState

Builder原始碼

​ 所以問題就在於,Navigator需要通過MaterialApp或者它孩子的上下文。

1. 解決一

按照此筆記最開始的正常路由演示案例來進行修改。

2. 解決二

使用Builder

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一個頁面"),
            ///
            Builder(builder: (context){
              return RaisedButton(
                onPressed: ()  {
                  Navigator.of(context).push(MaterialPageRoute(builder: (_){
                    return new SecondRoute();
                  }));
                },
                child: Text("進入第二頁"),
              );
            })
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

​ 使用Builder巢狀,Builder的引數可以看成一個回撥,接收自身的context並返回佈局配置。現在路由是從Builder的父親開始查詢啦,自然能找到Navigator。

Flutter - 路由管理 - 01 -  基礎使用

3. 解決三

使用navigatorKey

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        ///指定路由器widget的key
      navigatorKey: navigatorKey,
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁"),
        ),
        body: Column(
          children: <Widget>[
            Text("第一個頁面"),
            RaisedButton(
              onPressed: ()  {
                ///輸出Navigator
                debugPrint(navigatorKey.currentWidget.runtimeType.toString());
                navigatorKey.currentState.push(MaterialPageRoute(builder: (_){
                  return new SecondRoute();
                }));
              },
              child: Text("進入第二頁"),
            )
          ],
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二頁"),
      ),
      body: Column(
        children: <Widget>[
          Text("第一個頁面"),
          RaisedButton(
            onPressed: () {
              //路由pop彈出
              Navigator.pop(context);
            },
            child: Text("返回"),
          )
        ],
      ),
    );
  }
}

複製程式碼

Navigator的建立

​ 在建立Navigator的時候,會給一個key,這個key可以看成一個Widget的id。這裡的**_navigator就是我們指定的navigatorKey**(如果我們沒指定,會給預設值的,所以不要疑惑不指定是不是就不建立Navigator了)。而通過這個key,就能夠獲得這個Navigator。直接獲得了路由自然不需要再去查詢了!

相關文章