Flutter中管理路由棧的方法和應用

入魔的冬瓜發表於2019-02-03

本文首先講的Flutter中的路由,然後主要講下Flutter中棧管理的幾種方法。

  • 瞭解下Route和Navigator
  • 簡單的路由
  • 命名路由
  • 自定義路由
  • Flutter中使用的路由場景
  • Flutter中的路由棧管理
  • 實戰
  • 程式碼連結
  • 下一步

瞭解下Route和Navigator

在Flutter中,我們需要在不同螢幕或者頁面之間進行切換和傳送資料,這些“screens”或者“pages”被稱為Route(路由),是由一個Navigator的小部件進行管理。

Navigator可以管理包含若干Route物件的堆疊,並提供了管理的方法,平常我們經常用的就是[Navigator.push]和[Navigator.pop]。

儘管我們可以自己直接建立一個navigator,但是當我們建立一個WidgetsApp或者MaterialApp,Flutter會自動預設建立一個Navigator。
所以我們一般是使用由[WidgetsApp]或者[MaterialApp]所建立的Navigator就行了,然後通過呼叫[Navigator.of]
來拿到當前的Navigator的狀態NavigatorState,然後呼叫它的pop或者push方法。

簡單的路由

比如要導航到一個新的頁面,我們可以建立一個[MaterialPageRoute]的例項,然後呼叫Navigator.of(context).push()方法就將新頁面新增到堆疊的頂部。

返回上一個頁面,則呼叫Navigator.pop(context)就可以從堆疊中刪除這個螢幕;

     Navigator.of(context)
                        .push(new MaterialPageRoute(builder: (context) {
                      return new DetailPage();
                    }));
 Navigator.pop(context);
複製程式碼

命名路由

如果每次跳轉到一個新的路由頁面,都要跟上面一樣的寫法,建立MaterialPageRoute例項然後呼叫push方法,這樣的話就太麻煩了。

所以,Flutter提供了另外一種方式來管理路由,可以使用命名路由,然後使用Navigator.pushNamed()方法來彈出路由。

建立MaterialApp的時候需要傳入一個routes引數,routes本質上是一個Map<String,WidgetBuilder>,key值對應自定義的路徑名字,value值會對映到對應的WidgetBuilder,我們可以在WidgetBuilder中建立對應的頁面。

Navigator.pushNamed()方法有兩個引數(BuildContext,String),第一個是上下文,第二個是在路由中預定義的string。

特殊情況處理:當push一個不存在的路由頁面的時候,需要進行提示操作。可以使用UnknownRoute的屬性。比如下面的例子,
當push一個不存在的路由的時候,會跳轉到NotFoundPage的頁面。

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      routes: {
        //Map<String, WidgetBuilder>
        "/splash": (context) => new SplashPage(),
        "/login": (context) => new LoginPage(),
        "/home": (context) => new HomePage(),
        "/detail": (context) => new DetailPage(),
      },
      onUnknownRoute: (RouteSettings setting) {
        String name = setting.name;
        print("onUnknownRoute:$name");
        return new MaterialPageRoute(builder: (context) {
          return new NotFoundPage();
        });
      },
      title: `Flutter Demo`,
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SplashPage(),
    );
  }
}

//彈出路由,跳轉到其他頁面
 Navigator.of(context).pushNamed("/detail");
複製程式碼

自定義路由

比如我們上面使用的是MaterialPageRoute,在頁面切換的時候,會有預設的自適應平臺的過渡動畫。
如果想自定義頁面的進場和出場動畫,那麼需要使用PageRouteBuilder來建立路由。
PageRouteBuilder是主要的部分,一個是“pageBuilder”,用來建立所要跳轉到的頁面,另一個是“transitionsBuilder”,也就是我們可以自定義的轉場效果。

  PageRouteBuilder({
    RouteSettings settings,
    @required this.pageBuilder,//構造頁面
    this.transitionsBuilder = _defaultTransitionsBuilder,//建立轉場動畫
    this.transitionDuration = const Duration(milliseconds: 300),//轉場動畫的持續時間
    this.opaque = true,//是否是透明的
    this.barrierDismissible = false,//舉個例子,比如AlertDialog也是利用PageRouteBuilder進行建立的,barrierDismissible若為false,點選對話方塊周圍,對話方塊不會關閉;若為true,點選對話方塊周圍,對話方塊自動關閉。
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  }
複製程式碼

只修改單獨一個頁面的過渡動畫,可以這樣操作,例如下面的程式碼,

//              自定義跳轉動畫
              Navigator.push(
                  context,
                  PageRouteBuilder(
                      opaque: false,
                      pageBuilder: (BuildContext context, _, __) {
                        return new HomePage();
                      },
                      transitionsBuilder: (___, Animation<double> animation,
                          ____, Widget child) {
                        return FadeTransition(
                          opacity: animation,
                          child: RotationTransition(
                            turns: Tween<double>(begin: 0.5, end: 1.0)
                                .animate(animation),
                            child: child,
                          ),
                        );
                      }));
複製程式碼

Flutter中使用的路由場景

在Flutter中,我們會使用到這些方法,例如[showDialog()], [showMenu()], and [showModalBottomSheet()]等,這些方法其實本質上是建立了一個路由的頁面後,並呼叫Navigator的push方法去push到當前的螢幕上。

showDialog()其實是呼叫了showGeneralDialog(),所以下面貼了showGeneralDialog的原始碼,可以看出,也是利用了Navigator的push方法的。

這裡插一下,關於對話方塊的使用,比如列表對話方塊,自定義對話方塊的使用和踩坑,可以看下我的另外一篇文章:Flutter之Dialog使用和踩坑

Future<T> showGeneralDialog<T>({
  @required BuildContext context,
  @required RoutePageBuilder pageBuilder,
  bool barrierDismissible,
  String barrierLabel,
  Color barrierColor,
  Duration transitionDuration,
  RouteTransitionsBuilder transitionBuilder,
}) {
  return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
  ));
}

複製程式碼

Flutter中的路由棧管理

相信大家對棧Stack都有一定的瞭解,push方法是將元素新增到堆疊的頂部,pop方法是刪除頂部元素。

下面用圖文的方式來講解Flutter中幾個管理棧的方法之間的區別。

push()

push(),就是直接將一個元素插入到堆疊的頂部。

這個方法很簡單,並且我們會經常用到。比如從Screen1中利用navigator的push方法,將Screen2的路由彈到堆疊的頂部。堆疊的情況如下圖:

Flutter中管理路由棧的方法和應用

可以利用下面的兩個push方法,實現這個目的。

Navigator.of(context).pushNamed("/111");
Navigator.of(context).push(route);
複製程式碼

pop()

pop(), 就是將堆疊的頂部元素進行刪除,回退到上一個介面。

比如上面的例子,在Screen2中利用pop()將頂部的Screen2從堆疊中移除,之後的堆疊如下圖:

Flutter中管理路由棧的方法和應用
Navigator.of(context).pop();
複製程式碼

注意:

  • 當利用push()跳轉到一個使用Scaffold的頁面的時候,Scaffold會自動在其AppBar上新增一個“ 後退 ”按鈕,點選按鈕會自動呼叫Navigator.pop()。
  • 在Android中,按下裝置後退按鈕也會自動呼叫Navigator.pop()。

pushReplacementNamed()和popAndPushNamed()

有下面的這種場景,我們進入到Screen3頁面後,要跳轉到Screen4頁面,不過點選返回按鈕,並不想回退到Screen3頁面。也就是想將Screen4的元素插入棧頂的同時,將Screen3的元素夜進行移除。

Flutter中管理路由棧的方法和應用

這個時候,我們就要用到pushReplacementNamed()或者popAndPushNamed(),pushReplacement()都可以實現這個目的。

Navigator.of(context).pushReplacementNamed(`/screen4`);
Navigator.popAndPushNamed(context, `/screen4`);
Navigator.of(context).pushReplacement(newRoute);
複製程式碼

pushNamedAndRemoveUntil()

一般會有這種場景,我們在已經登入的情況下,在設定介面會有個退出使用者登入的按鈕,點選後會登出使用者退出登入,並且會跳轉到登入介面。那麼路由棧的變化應該會如下圖所示:

Flutter中管理路由棧的方法和應用

如果只是簡單的進行push一個LoginScreen的操作的話,那麼按返回鍵的話,會回到上一個頁面,這樣的邏輯是不對的。

所以我們應該刪除掉路由棧中的所有route,然後再彈出LoginScreen。這個時候就要用到pushNamedAndRemoveUntil()或者pushAndRemoveUntil()了。

typedef RoutePredicate = bool Function(Route<dynamic> route);
//第一個引數context是上下文的context,
//第二個引數newRouteName是新的路由所命名的路徑
//第三個引數predicate,返回值是bool型別,按照我的理解,就是用來判斷Until所結
//束的時機,如果為false的話,就會一直繼續執行Remove的操作,直到為true的時候,停止Remove操作,然後才執行push操作。
  static Future<T> pushNamedAndRemoveUntil<T extends Object>(BuildContext context, String newRouteName, RoutePredicate predicate) {
    return Navigator.of(context).pushNamedAndRemoveUntil<T>(newRouteName, predicate);
  }
複製程式碼
  • 如果想在彈出新路由之前,刪除路由棧中的所有路由,那可以使用下面的這種寫法,(Route route) => false,這樣能保證把之前所有的路由都進行刪除,然後才push新的路由。
Navigator.of(context).pushNamedAndRemoveUntil(`/LoginScreen`, (Route<dynamic> route) => false);
複製程式碼
  • 如果想在彈出新路由之前,刪除路由棧中的部分路由。比如只彈出Screen1路由上面的Screen3和Screen2,然後再push新的Screen4,堆疊的情況如下圖:
Flutter中管理路由棧的方法和應用

利用ModalRoute.withName(name),來執行判斷,可以看下面的原始碼,當所傳的name跟堆疊中的路由所定義的時候,會返回true值,不匹配的話,則返回false。

Navigator.of(context).pushNamedAndRemoveUntil(`/screen4`,ModalRoute.withName(`/screen1`));
 
 //ModalRoute.withName的原始碼
   static RoutePredicate withName(String name) {
    return (Route<dynamic> route) {
      return !route.willHandlePopInternally
          && route is ModalRoute
          && route.settings.name == name;
    };
  }
複製程式碼

popUntil()

popUntil()方法的過程其實跟上面差不多,就是是少了push一個新頁面的操作,只是單純的進行移除路由操作。

popUntil(RoutePredicate predicate);
Navigator.of(context).popUntil(ModalRoute.withName("/XXX"));

複製程式碼

實戰

這裡寫了一個Demo,將上面的幾種管理棧的用法都運用了一下。

Flutter中管理路由棧的方法和應用

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      routes: {
        //Map<String, WidgetBuilder>
        "/splash": (context) => new SplashPage(),
        "/login": (context) => new LoginPage(),
        "/home": (context) => new HomePage(),
        "/detail": (context) => new DetailPage(),
      },
      onUnknownRoute: (RouteSettings setting) {
        String name = setting.name;
        print("onUnknownRoute:$name");
        return new MaterialPageRoute(builder: (context) {
          return new NotFoundPage();
        });
      },
      title: `Flutter Demo`,
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SplashPage(),
    );
  }
}
複製程式碼

首先是定義在MaterialApp中定義多個路由,Demo中有多個路由:

  • SplashPage 啟動頁面,設定2秒後自動跳轉到LoginPage
  • LoginPage 登入頁面,點選按鈕,模仿登入成功的動作並跳轉到HomePage
  • HomePage 主頁,顯示一個列表,點選列表項跳轉到DetailPage
  • DetailPage 詳細資訊頁面,並且有一個退出登入的按鈕
  • NotFoundPage 當沒有路由可以匹配的時候,彈出這個頁面

下面簡單講一下程式碼:

SplashPage->LoginPage,LoginPage->HomePage

用的都是pushReplacementNamed()。因為跳到登入介面後,不需要返回到SplashPage,所以需要將SplashPage從路由棧中移除。LoginPage->HomePage,也是同樣的道理。

Navigator.of(context).pushReplacementNamed("/login");
Navigator.of(context).pushReplacementNamed("/home");
複製程式碼

HomePage->DetailPage

使用的是簡單的pushNamed()就可以了,沒必要移除HomePage,因為從DetailPage點選返回後,需要返回到HomePage介面。

//下面的兩種寫法都是可以的
Navigator.of(context).pushNamed("/detail");
Navigator.of(context)
         .push(new MaterialPageRoute(builder: (context) {
         return new DetailPage();
       }));
複製程式碼

DetailPage->LoginPage

利用的是從DetailPage點選退出登入按鈕,會彈出路由棧中的所有路由頁面,然後再將LoginPage的路由插入到棧頂。這樣的話,路由棧中就只剩下LoginPage了,若是點選返回按鈕的話,預設就會退出應用程式了,因為堆疊為空了。

Navigator.of(context).pushNamedAndRemoveUntil("/login", (Route<dynamic> route) => false);
複製程式碼

程式碼連結

github.com/LXD31256949…

歡迎大家關注我的公眾號,會推送關於Flutter和Android學習的一些文章

Flutter中管理路由棧的方法和應用

下一步

學習使用fluro的第三方路由框架,並做下整理和總結。

相關文章