Flutter進階:路由、路由棧詳解及案例分析

Meandni發表於2019-03-04

路由初體驗

路由(Routes)是什麼?路由是螢幕或應用程式頁面的抽象。

Flutter 使我們能夠優雅地管理路由主要依賴的是 Navigator(導航器)類。這是一個用於管理一組具有某種進出規則的頁面的 Widget,也就是說用它我們能夠實現各個頁面間有規律的切換。而這裡的規則便是在其內部維護的一個“ 路由棧”。

學習 Android 的同學知道 Activity 的啟動模式可以實現各種業務需求,iOS 中也有巢狀路由的功能,Flutter 作為最有潛力的跨平臺框架當然要吸取眾家之精華,它當然完全有能力實現原生的各種效果!

我們先嚐試實現一個小的功能。

元件路由

當我們第一次開啟應用程式,出現在眼前的便是路由棧中的第一個也是最底部例項:

void main() {
  runApp(MaterialApp(home: Screen1()));
}
複製程式碼

要在堆疊上推送新的例項,我們可以呼叫導航器 Navigator.push ,傳入當前 context 並且使用構建器函式建立 MaterialPageRoute 例項,該函式可以建立您想要在螢幕上顯示的內容。 例如:

new RaisedButton(
   onPressed:(){
   Navigator.push(context, MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('My Page')),
          body: Center(
            child: FlatButton(
              child: Text('POP'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ),
        );
      },
    ));
   },
   child: new Text("Push to Screen 2"),
),
複製程式碼

點選執行上方操作,我們將成功開啟第二個頁面。

命名路由

在一般應用中,我們用的最多的還是命名路由,它是將應用中需要訪問的每個頁面命名為不重複的字串,我們便可以通過這個字串來將該頁面例項推進路由。

例如,'/ home' 表示 HomeScreen, '/ login' 表示 LoginScreen。 '/' 表示主頁面。 這裡的命名規範與 REST API 開發中的路由類似。 所以 '/' 通常表示的是我們的根頁面。

請看下方案例:

new MaterialApp(
  home: new Screen1(),
  routes: <String, WidgetBuilder> {
    '/screen1': (BuildContext context) => new Screen1(),
    '/screen2' : (BuildContext context) => new Screen2(),
    '/screen3' : (BuildContext context) => new Screen3(),
    '/screen4' : (BuildContext context) => new Screen4()
  },
)
複製程式碼

Screen1()、Screen2()等是每個頁面的類名。

我們同樣可以實現前面的功能:

new RaisedButton(
   onPressed:(){
     Navigator.of(context).pushNamed('/screen2');
   },
   child: new Text("Push to Screen 2"),
),
複製程式碼

或者:

new RaisedButton(
   onPressed:(){
     Navigator.pushNamed(context, "/screen2")
   },
   child: new Text("Push to Screen 2"),
),
複製程式碼

同樣可以實現上方效果。

Pop

實現上面兩種方法後,此時,路由棧中的情況如下:

1_RKtC1MKJbjSfMjUlR-2K7g

現在,當我們想要回退的到主螢幕時,我們則需要使用 pop 方法從 Navigator 的堆疊中彈出 Routes。

Navigator.of(context).pop();
複製程式碼

1_hq7qfAer0wCCSyIBKr7sfg

使用 Scaffold 時,通常不需要顯式彈出路徑,因為 Scaffold 會自動向其 AppBar 新增一個“後退”按鈕,按下時會呼叫 Navigator.pop()

在 Android 中,按下裝置後退按鈕也會這樣做。但是,我們也有可能需要將此方法用於其他元件,例如在使用者單擊“取消”按鈕時彈出 AlertDialog。

這裡要注意的是:切勿用 push 代替 pop,有同學說我在 Screen2 push Screen1 部照樣能實現這個功能嗎?其實不然啊,請看下圖:

1_Xsyo5c8s1JwO6f2OQ1nNEg

所以 push 只用於向棧中新增例項,pop 彈出例項!(特殊需求除外)

詳解路由棧

前面,我們已經知道如何簡單在路由棧中 push、pop 例項,然而,當遇到一些特殊的情況,這顯然不能滿足需求。學習 Android 的同學知道 Activity 的各種啟動模式可以完成相應需求,Flutter 當然也有類似的可以解決各種業務需求的實現方式!

請看下面使用方法與案例分析。

pushReplacementNamed 與 popAndPushNamed

RaisedButton(
  onPressed: () {
    Navigator.pushReplacementNamed(context, "/screen4");
  },
  child: Text("pushReplacementNamed"),
),
RaisedButton(
  onPressed: () {
    Navigator.popAndPushNamed(context, "/screen4");
  },
  child: Text("popAndPushNamed"),
),
複製程式碼

我們在 Screen3 頁面使用 pushReplacementNamedpopAndPushNamed 方法 push 了 Screen4。

此時路由棧情況如下:

1_cr77kgOgz7KRjwvMAVXoAg

Screen4 代替了 Screen3

pushReplacementNamedpopAndPushNamed 的區別在於: popAndPushNamed 能夠執行 Screen2 彈出的動畫與 Screen3 推進的動畫而 pushReplacementNamed 僅顯示 Screen3 推進的動畫。

1_cr77kgOgz7KRjwvMAVXoAg

案例:

pushReplacementNamed:當使用者成功登入並且現在在 HomeScreen 上時,您不希望使用者還能夠返回到 LoginScreen。因此,登入應完全由首頁替換。另一個例子是從 SplashScreen 轉到 HomeScreen。 它應該只顯示一次,使用者不能再從 HomeScreen 返回它。 在這種情況下,由於我們要進入一個全新的螢幕,我們可能需要藉助此方法。

popAndPushNamed:假設您正在有一個 Shopping 應用程式,該應用程式在 ProductsListScreen 中顯示產品列表,使用者可以在 FiltersScreen 中應用過濾商品。 當使用者單擊“應用篩選”按鈕時,應彈出 FiltersScreen 並使用新的過濾器值推回到 ProductsListScreen。 這裡 popAndPushNamed 顯然更為合適。

pushNamedAndRemoveUntil

使用者已經登陸進入 HomeScreen ,然後經過一系列操作回到配合只介面想要退出登入,你不能夠直接 Push 進入 LoginScreen 吧?你需要將之前路由中的例項全部刪除是的使用者不會在回到先前的路由中。

pushNamedAndRemoveUntil 可實現該功能:

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

這裡的 (Route<dynamic> route) => false 能夠確保刪除先前所有例項。

Logging out removes all routes and takes user back to LoginScreen

現在又有一個需求:我們不希望刪除先前所有例項,我們只要求刪除指定個數的例項。

我們有一個需要付款交易的購物應用。在應用程式中,一旦使用者完成了支付交易,就應該從堆疊中刪除所有與交易或購物車相關的頁面,並且使用者應該被帶到 PaymentConfirmationScreen ,單擊後退按鈕應該只將它們帶回到 ProductsListScreenHomeScreen

1_aaZxoLUbKdFPgiIkBAmw7w

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
複製程式碼

通過程式碼,我們推送 Screen4 並刪除所有路由,直到 Screen1

1_D81iZF-BikxXJHak7_NkhA

popUntil

想象一下,我們在應用程式中要填寫一系列資訊,表單分佈在多個頁面中。假設需要填寫三個頁面的表單一步接著一步。 然而,在表單的第 3 部分,使用者取消了填寫表單。 使用者單擊取消並且應彈出所有之前與表單相關的頁面,並且應該將使用者帶回 HomeScreen 或者 DashboardScreen,這種情況下資料屬於資料無效! 我們不會在這裡推新任何新東西,只是回到以前的路由棧中。

1_qV7mF0Kow2zch-fjksmA_Q

Navigator.popUntil(context, ModalRoute.withName('/screen2'));
複製程式碼

Popup routes(彈出路由)

路由不一定要遮擋整個螢幕。 PopupRoutes 使用 ModalRoute.barrierColor 覆蓋螢幕,ModalRoute.barrierColor 只能部分不透明以允許當前螢幕顯示。 彈出路由是“模態”的,因為它們阻止了對下面其他元件的輸入。

有一些方法可以建立和顯示這類彈出路由。 例如:showDialog,showMenu 和 showModalBottomSheet。 如上所述,這些函式返回其推送路由的 Future(非同步資料,參考下面的資料部分)。 執行可以等待返回的值在彈出路由時執行操作。

還有一些元件可以建立彈出路由,如 PopupMenuButton 和 DropdownButton。 這些元件建立 PopupRoute 的內部子類,並使用 Navigator 的push 和 pop 方法來顯示和關閉它們。

自定義路由

您可以建立自己的一個視窗z元件庫路由類(如 PopupRoute,ModalRoute 或 PageRoute)的子類,以控制用於顯示路徑的動畫過渡,路徑的模態屏障的顏色和行為以及路徑的其他各個特性。

PageRouteBuilder 類可以根據回撥定義自定義路由。 下面是一個在路由出現或消失時旋轉並淡化其子節點的示例。 此路由不會遮擋整個螢幕,因為它指定了opaque:false,就像彈出路由一樣。

Navigator.push(context, PageRouteBuilder(
  opaque: false,
  pageBuilder: (BuildContext context, _, __) {
    return Center(child: Text('My PageRoute'));
  },
  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,
      ),
    );
  }
));
複製程式碼

ezgif-3-14c32a6d8764

路由兩部分構成,“pageBuilder”和“transitionsBuilder”。

該頁面成為傳遞給 buildTransitions 方法的子代的後代。 通常,頁面只構建一次,因為它不依賴於其動畫引數(在此示例中以_和__表示)。 過渡是建立在每個幀的持續時間。

巢狀路由

一個應用程式可以使用多個路由導航器。將一個導航器巢狀在另一個導航器下方可用於建立“內部旅程”,例如選項卡式導航,使用者註冊,商店結帳或代表整個應用程式子部分的其他獨立個體。

iOS應用程式的標準做法是使用選項卡式導航,其中每個選項卡都維護自己的導航歷史記錄。因此,每個選項卡都有自己的導航器,建立了一種“並行導航”。

除了選項卡的並行導航之外,還可以啟動完全覆蓋選項卡的全屏頁面。例如:入職流程或警報對話方塊。因此,必須存在位於選項卡導航上方的“根”導航器。因此,每個選項卡的 Navigators 實際上都是巢狀在一個根導航器下面的Navigators。

用於選項卡式導航的巢狀導航器位於 WidgetApp 和 CupertinoTabView 中,因此在這種情況下您無需擔心巢狀的導航器,但它是使用巢狀導航器的真實示例。

以下示例演示瞭如何使用巢狀的 Navigator 來呈現獨立的使用者註冊過程。

儘管此示例使用兩個 Navigators 來演示巢狀的 Navigators,但僅使用一個 Navigato r就可以獲得類似的結果。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ...some parameters omitted...
      // MaterialApp contains our top-level Navigator
      initialRoute: '/',
      routes: {
        '/': (BuildContext context) => HomePage(),
        '/signup': (BuildContext context) => SignUpPage(),
      },
    );
  }
}

class SignUpPage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   // SignUpPage builds its own Navigator which ends up being a nested
   // Navigator in our app.
   return Navigator(
     initialRoute: 'signup/personal_info',
     onGenerateRoute: (RouteSettings settings) {
       WidgetBuilder builder;
       switch (settings.name) {
         case 'signup/personal_info':
           // Assume CollectPersonalInfoPage collects personal info and then
           // navigates to 'signup/choose_credentials'.
           builder = (BuildContext _) => CollectPersonalInfoPage();
           break;
         case 'signup/choose_credentials':
           // Assume ChooseCredentialsPage collects new credentials and then
           // invokes 'onSignupComplete()'.
           builder = (BuildContext _) => ChooseCredentialsPage(
             onSignupComplete: () {
               // Referencing Navigator.of(context) from here refers to the
               // top level Navigator because SignUpPage is above the
               // nested Navigator that it created. Therefore, this pop()
               // will pop the entire "sign up" journey and return to the
               // "/" route, AKA HomePage.
               Navigator.of(context).pop();
             },
           );
           break;
         default:
           throw Exception('Invalid route: ${settings.name}');
       }
       return MaterialPageRoute(builder: builder, settings: settings);
     },
   );
 }
}
複製程式碼

Navigator.of 在給定 BuildContext 中最近的根 Navigator 上執行。 確保在預期的 Navigator 下面提供BuildContext,尤其是在建立巢狀 Navigators 的大型構建方法中。 Builder 元件可用於訪問元件子樹中所需位置的 BuildContext。

頁面間資料傳遞

資料傳遞

在上面的大多數示例中,我們推送新路由時沒有傳送資料,但在實際應用中這種情況應用很少。 要傳送資料,我們將使用 Navigator 將新的 MaterialPageRoute 用我們的資料推送到堆疊上(這裡是 userName

String userName = "John Doe";
Navigator.push(
    context,
    new MaterialPageRoute(
        builder: (BuildContext context) =>
        new Screen5(userName)));
複製程式碼

要在 Screen5 中得到資料,我們只需在 Screen5 中新增一個引數化建構函式:

class Screen5 extends StatelessWidget {

  final String userName;
  Screen5(this.userName);
  @override
  Widget build(BuildContext context) {
  print(userName)
  ...
  }
}
複製程式碼

這表示我們不僅可以使用 MaterialPageRoute 作為 push 方法,還可以使用 pushReplacementpushAndPopUntil 等。基本上從我們描述的上述方法中路由方法,第一個引數現在將採用 MaterialPageRoute 而不是 namedRouteString

資料返回

我們可能還想從新頁面返回資料。 就像一個警報應用程式,併為警報設定一個新音調,您將顯示一個帶有音訊音調選項列表的對話方塊。 顯然,一旦彈出對話方塊,您將需要所選的專案資料。 它可以這樣實現:

new RaisedButton(onPressed: ()async{
  String value = await Navigator.push(context, new MaterialPageRoute<String>(
      builder: (BuildContext context) {
        return new Center(
          child: new GestureDetector(
              child: new Text('OK'),
              onTap: () { Navigator.pop(context, "Audio1"); }
          ),
        );
      }
  )
  );
  print(value);

},
  child: new Text("Return"),)
複製程式碼

Screen4 中嘗試並檢查控制檯的列印值。

另請注意:當路由用於返回值時,路由的型別引數應與 pop 的結果型別匹配。 這裡我們需要一個 String 資料,所以我們使用了 MaterialPageRoute <String>。 不指定型別也沒關係。

其他效果解釋

maybePop

原始碼:

static Future<bool> maybePop<T extends Object>(BuildContext context, [ T result ]) {
    return Navigator.of(context).maybePop<T>(result);
  }

@optionalTypeArgs
  Future<bool> maybePop<T extends Object>([ T result ]) async {
    final Route<T> route = _history.last;
    assert(route._navigator == this);
    final RoutePopDisposition disposition = await route.willPop();
    if (disposition != RoutePopDisposition.bubble && mounted) {
      if (disposition == RoutePopDisposition.pop)
        pop(result);
      return true;
    }
    return false;
  }
複製程式碼

如果我們在初始路由上並且有人錯誤地試圖彈出這個唯一頁面怎麼辦? 彈出堆疊中唯一的頁面將關閉您的應用程式,因為它後面已經沒有頁面了。這顯然是不好的體驗。 這就是 maybePop() 起的作用。 點選 Screen1 上的 maybePop 按鈕,沒有任何效果。 在 Screen3 上嘗試相同的操作,可以正常彈出。

這種效果也可通過 canPop 實現:

canPop

原始碼:

static bool canPop(BuildContext context) {
    final NavigatorState navigator = Navigator.of(context, nullOk: true);
    return navigator != null && navigator.canPop();
  }

bool canPop() {
    assert(_history.isNotEmpty);
    return _history.length > 1 || _history[0].willHandlePopInternally;
  }
複製程式碼

如果佔中例項大於 1 或 willHandlePopInternally 屬性為 true 返回 true,否則返回 false。

我們可以通過判斷 canPop 來確定是否能夠彈出該頁面。

如何去除預設返回按鈕

AppBar({
    Key key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation = 4.0,
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    this.toolbarOpacity = 1.0,
    this.bottomOpacity = 1.0,
  }) : assert(automaticallyImplyLeading != null),
       assert(elevation != null),
       assert(primary != null),
       assert(titleSpacing != null),
       assert(toolbarOpacity != null),
       assert(bottomOpacity != null),
       preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
       super(key: key);
複製程式碼

automaticallyImplyLeading置為 false

參考連結

docs.flutter.io/flutter/wid…

部分演示圖片來自:medium.com/flutter-community/flutter-push-pop-push-1bb718b13c31

我的 Github:github.com/MeandNi

我的部落格:meandni.com/

相關文章