Flutter開發之路由與導航

xiangzhihong發表於2019-12-10

基本概念

如果說構成檢視元素的基本單位是元件,那麼構成應用程式的基本單位就是頁面。對於擁有多個頁面的應用程式而言,如何從一個頁面平滑地過渡到另一個頁面,是技術框架需要考慮的問題。

在前端開發中,可以使用路由框架來統一管理頁面及它們之間的跳轉。在Android中路由指的是一個Activity,在iOS中指的是一個ViewController,可以通過startActivity或pushViewController來開啟一個新的路由。在Flutter中,路由的管理和導航借鑑了前端和客戶端的設計思路,需要使用Route和Navigator來進行統一管理。

其中,Route是頁面的抽象,主要負責建立介面、接收引數以及響應導航器Navigator的開啟與關閉。而Navigator則用於維護路由棧管理,Route開啟即入棧,Route關閉即出棧,當然還可以替換棧內的某一個Route。作為官方提供的路由管理元件,Navigator提供了一系列方法來管理路由棧,其中最常用的兩個方法是push()和pop(),它們的含義如下。

  • push():將給定的路由入棧,返回值是一個Future物件,用以接收路由出棧時的返回資料。
  • pop():將棧頂路由出棧,返回結果為頁面關閉時返回給上一個頁面的資料。

除了push()和pop()方法外,Navigator還提供了很多其它實用的方法,如replace()、removeRoute()和popUntil()等,可以根據使用場景合理的選取。

根據是否需要提前註冊頁面識別符號,Flutter中的路由管理可以分為基本路由和命名路由兩種。

  • 基本路由:無需提前註冊,在頁面切換時需要手動構造頁面的例項。
  • 命名路由:需要提前註冊頁面識別符號,在頁面切換時通過識別符號直接開啟新的路由。

下面就讓我們重點來看一下Flutter中的路由管理的基本路由和命名路由等相關知識。

基本路由

在Flutter開發中,基本路由的使用方式和原生Android、iOS開啟新頁面的方式非常類似。要開啟一個新的頁面,只需要建立一個MaterialPageRoute物件例項,然後呼叫Navigator.push()方法將新頁面壓到路由堆疊的頂部即可,如果要返回上一個頁面,則可以呼叫Navigator.pop()方法。

其中,MaterialPageRoute是一種路由模板,定義了路由建立以及路由切換過渡動畫的相關配置,該配置可以根據不同的平臺實現與平臺頁面切換動畫風格一致的路由切換動畫。下面是使用Navigator實現頁面跳轉的示例,程式碼如下。

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('第一個頁面'),
      ),
      body: Center(
        child: RaisedButton(
            child: Text('跳轉到第二個頁面'),
            onPressed: () => Navigator.push(context,
                MaterialPageRoute(builder: (context) => SecondPage()))),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('第二個頁面'),
      ),
      body: Center(
        child: RaisedButton(
            child: Text('返回上一個頁面'),
 onPressed: () => Navigator.pop(context)),
      ),
    );
  }
}

複製程式碼

在上面的示例中,我們建立了兩個頁面,每個頁面都包含一個按鈕。當點選第一個頁面上的按鈕時將導航到第二個頁面,點選第二個頁面上的按鈕將返回第一個頁面。執行上面的程式碼,效果如下圖所示。

在這裡插入圖片描述
可以發現,跳轉頁面使用的是Navigator.push()方法,該方法可以將一個新的路由新增到由Navigator管理的路由物件的棧頂。而建立新的路由物件使用的是MaterialPageRoute,MaterialPageRoute是PageRoute的子類,定義了路由建立及切換時過渡動畫的相關介面及屬性,並且自帶頁面切換動畫,Android平臺頁面進入動畫是向上滑動並淡出,退出是相反,iOS平臺頁面進入動畫是從右側滑入,退出則相反。

命名路由

基本路由的使用方式相對簡單靈活,適用於應用中頁面不多的場景。而對於應用中頁面比較多的情況下,如果再使用基本路由方式,那麼每次跳轉一個新的頁面都要手動建立MaterialPageRoute例項,然後再呼叫push()方法來開啟一個新的頁面,此時頁面的管理和跳轉就比較混亂。

為了避免頻繁的建立MaterialPageRoute例項,Flutter提供了另外一種方式來簡化路由管理,即命名路由。所謂命名路由,就是給頁面起一個別名,然後使用頁面的別名就可以開啟它,使用此種方式來管理路由,使得路由的管理更加清晰直觀。

要想通過別名來指定頁面切換,必須先給應用程式MaterialApp提供一個頁面名稱對映規則,即路由表。路由表是一個Map<String,WidgetBuilder>的結構,其中key對應頁面名字,value則是對應的頁面,如下所示。

MaterialApp(
    ...          //其他配置
    routes:{                     //註冊路由
      'first':(context)=>FirstPage(),
      'second':(context)=>SecondPage(),
},
initialRoute: 'first',            //初始路由頁面
);

複製程式碼

在路由表中註冊好頁面後,然後就可以通過Navigator.pushNamed()方法來開啟頁面,如下所示。

Navigator.pushNamed(context,"second ");    // second表示頁面別名
複製程式碼

不過,由於路由的註冊和使用都採用字串來標識,這就會帶來一個問題,即如果開啟一個不存在的路由頁面。對應這類問題,移動應用有一個通用的解決方案,即跳轉到一個統一的錯誤頁面。在註冊路由表時,Flutter提供了一個UnknownRoute屬性,用來對未知的路由識別符號進行統一的頁面跳轉處理,如下所示。

MaterialApp(
  …
routes:{},
   onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),       //錯誤路由處理,返回UnknownPage
);

class UnknownPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('錯誤路由'),
      ),
    );
  }
}

複製程式碼

路由巢狀

有時候,一個應用可能不止一個導航器,而是可能有多個導航器,將一個導航器巢狀在另一個導航器的行為稱為路由巢狀。路由巢狀在移動開發中是很常見的,比如,移動開發中經常會看到應用主頁有底部導航欄,每個底部導航欄又巢狀其他頁面的情況,效果如下圖所示。

在這裡插入圖片描述
要實現上面的示例效果,首先需要新建一個底部導航欄,然後再由底部導航欄去巢狀其他子路由。關於底部導航欄的實現,可以直接使用Scaffold佈局元件的bottomNavigationBar屬性實現,如下所示。

class MainPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MainPageState();
  }
}

class MainPageState extends State<MainPage> {
  int currentIndex = 0;       //底部導航欄索引
  final List<Widget> children = [
    HomePage(),          //首頁
    MinePage(),           //我的
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: children[currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        onTap: onTabTapped,
        currentIndex: currentIndex,
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('首頁')),
          BottomNavigationBarItem(icon: Icon(Icons.person), title: Text('我的')),
        ],
      ),
    );
  }

  void onTabTapped(int index) {
    setState(() {
      currentIndex = index;
    });
  }
}

複製程式碼

然後,每個底部導航欄會巢狀一個子路由,然後子路由再去管理對應的路由頁面。在Flutter中,建立子路由需要使用Navigator元件,並且子路由的攔截需要使用onGenerateRoute屬性,如下所示。

class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Navigator(
      initialRoute: 'first',
      onGenerateRoute: (RouteSettings settings) {
        WidgetBuilder builder;
        switch (settings.name) {
          case 'first':
            builder = (BuildContext context) => FirstPage();
            break;
          case 'second':
            builder = (BuildContext context) => SecondPage();
            break;
        }
        return new MaterialPageRoute(builder: builder, settings: settings);
      },
    );
  }
}

複製程式碼

執行上面的程式碼,當點選子路由頁面上的按鈕時,底部的導航欄欄並不會消失,這是因為子路由僅在自己的範圍內有效。要想跳轉到其他子路由管理的頁面,就需要在根導航器中進行註冊,也就是MaterialApp內部的導航器。

路由傳參

在移動應用開發中,頁面引數的傳遞也是一個比較常見的需求。為了滿足不同場景下頁面跳轉過程中引數傳遞的需求,Flutter提供了路由引數機制,可以在開啟路由時傳遞引數,然後在目標頁面通過RouteSettings來獲取頁面傳遞的引數,如下所示。

Navigator.of(context).pushNamed("second ", arguments: " from first page");

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
//取出路由引數
    String msg = ModalRoute.of(context).settings.arguments as String; 
     …  //資料處理
  }
}

複製程式碼

除此之外,對於某些特定的頁面,還需要在其關閉時回傳頁面處理的處理結果。這與Android提供的startActivityForResult()方法監聽目標頁面返回處理結果的場景類似,Flutter也提供了頁面返回的引數機制。具體來說,就是在使用push()方法開啟目標頁面時,可以設定目標頁面關閉時監聽函式來獲取返回引數,當目標頁面關閉路由時使用pop()方法回傳引數即可。例如,下面是兩個頁面之間引數值傳遞和引數值回傳,程式碼如下。

class FirstPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return FirstPageState();
  }
}

class FirstPageState extends State<FirstPage> {
  
String result = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Column(
        children: <Widget>[
          Text('from seconde page: ' + result, style: TextStyle(fontSize: 20)),
          RaisedButton(
              child: Text('跳轉'),
              //使用then()獲取目標頁面返回引數
              onPressed: () => Navigator.of(context)
                  .pushNamed("second", arguments: "from first page")
                  .then((msg) => setState(() => result = msg)))
        ],
      )),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    String msg = ModalRoute.of(context).settings.arguments as String;

    return Scaffold(
        body: Center(
          child: Column(children: [
            Text('from first screen: ' + msg, style: TextStyle(fontSize: 20)),
            RaisedButton(
                child: Text('返回'),
                onPressed: () => Navigator.pop(context, "from second page"))
          ]),
        ));
  }
}

複製程式碼

執行上面的程式碼,可以看到,當SecondPage頁面被關閉重新回到FirstPage頁面時,FirstPage會把回傳的引數值展示出來,最終效果如下圖所示。

在這裡插入圖片描述

MaterialPageRoute

在使用路由過程中,經過會使用到MaterialPageRoute類。MaterialPageRoute繼承自PageRoute類,PageRoute類是一個抽象類,表示佔有整個螢幕空間的一個模態路由頁面,它還定義了路由構建及切換時過渡動畫的相關介面及屬性。

MaterialPageRoute 是Material元件庫提供的元件,它可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫:當開啟頁面時,新的頁面會從螢幕右側邊緣一致滑動到螢幕左邊,直到新頁面全部顯示到螢幕上,而上一個頁面則會從當前螢幕滑動到螢幕左側而消失;當關閉頁面時,正好相反,當前頁面會從螢幕右側滑出,同時上一個頁面會從螢幕左側滑入。

MaterialPageRoute 建構函式和各個引數的意義如下:

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

它們的具體含義如下:

  • builder :是一個WidgetBuilder型別的回撥函式,它的作用是構建路由頁面的具體內容,返回值是一個widget。我們通常要實現此回撥,返回新路由的例項。
  • settings: 包含路由的配置資訊,如路由名稱、是否初始路由(首頁)。
  • maintainState:預設情況下,當入棧一個新路由時,原來的路由仍然會被儲存在記憶體中,如果想在路由沒用的時候釋放其所佔用的所有資源,可以設定maintainState為false。
  • fullscreenDialog:表示新的路由頁面是否是一個全屏的模態對話方塊,在iOS中,如果fullscreenDialog為true,新頁面將會從螢幕底部滑入(而不是水平方向)。

總結

Flutter 提供了基本路由和命名路由兩種方式,來管理頁面間的跳轉。其中,基本路由需要自己手動建立頁面例項,通過 Navigator.push 完成頁面跳轉;而命名路由需要提前註冊頁面識別符號和頁面建立方法,通過 Navigator.pushNamed 傳入識別符號實現頁面跳轉。

對於命名路由,如果我們需要響應錯誤路由識別符號,還需要一併註冊 UnknownRoute。為了精細化控制路由切換,Flutter 提供了頁面開啟與頁面關閉的引數機制,我們可以在頁面建立和目標頁面關閉時,取出相應的引數。可以看到,關於路由導航,Flutter 綜合了 Android、iOS 和 React 的特點,簡潔而不失強大。

在中大型應用中,通常還會使用命名路由來管理頁面間的切換。命名路由的最重要作用,就是建立了字串識別符號與各個頁面之間的對映關係,使得各個頁面之間完全解耦,應用內頁面的切換隻需要通過一個字串識別符號就可以搞定,為後期模組化打好基礎。

除此之外,巢狀路由和路由傳參也是路由框架中比較核心的內容。本篇只是Flutter路由與導航的基本知識,後面將會從pushReplacementNamed 、 popAndPushNamed、pushNamedAndRemoveUntil和popUntil,以及第三方導航庫和原始碼分析等方面來深入介紹Flutter的路由開發與導航。

相關文章