Flutter路由使用指北

路由管理

FLutter中的路由,和原生組件化的路由一樣,就是頁面之間的跳轉,也可以稱之為導航。app維護一個路由棧,路由入棧(push)操作對應打開一個新頁面,路由出棧(pop)操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。

MaterialPageRoute

MaterialPageRoute是一種模態路由,可以針對不同平臺自適應的過渡動畫替換整個屏幕頁面:

對于Android,打開新頁面時,新頁面從屏幕底部導入到頂部。關閉頁面的時候,會從頂部滑動到底部消失。

在iOS上,頁面從右側滑入并反向退出。

下面我們介紹一下MaterialPageRoute 構造函數的各個參數的意義:

  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })

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

基本使用

Flutter為我們提供了導航器Navigator。參數傳入當前的BuildContext和要導航的頁面即可。

  1. 調用Navigator.push導航到第二個頁面
     Navigator.push(
               context, new MaterialPageRoute(builder:       (context) => Page2()));
    
  1. 調用Navigator.pop返回前一個頁面
         Navigator.pop(context, result);
    
    
  1. 關閉頁面后獲取結果
    有時候我們需要上個頁面關閉時傳遞一個返回值,幸運的是,Navigator的調用方法都是Future,因此我們可以等待它們的結果:

    3.1. 等待Navigator運行
    3.2. 將返回值傳遞給Navigator.pop函數
    3.3. 等待完成后,獲取返回值

    在page1中,導航到page2,并且await到page2傳遞返回值并pop,根據返回值彈出不同的對話框:

    onPressed: () async {
          var navigationResult = await Navigator.push(
              context, new MaterialPageRoute(builder: (context) => Page2()));
    
          if (navigationResult == 'from_back') {
            showDialog(
                context: context,
                builder: (context) => AlertDialog(
                      title: Text('Navigation from back'),
                    ));
          } else if (navigationResult == 'from_button') {
            showDialog(
                context: context,
                builder: (context) => AlertDialog(
                      title: Text('Navigation from button'),
                    ));
          }
        },
    

    在page2中傳遞返回值并返回:

    Navigator.pop(context, 'from_button');
    
  2. 攔截返回鍵
    如果不想點擊返回鍵關閉當前頁面,可以使用WillPopScope小部件,用它放在最外層包括住腳手架。并向onWillPop返回false。false告訴系統當前頁面不處理返回。

       @override
       Widget build(BuildContext context) {
         return WillPopScope(
           onWillPop: () => Future.value(false),
           child: Scaffold(
             body: Container(
               child: Center(
                 child: Text('Page 2',
                     style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
               ),
             ),
           ),
         );
       }
如果想自定義處理返回鍵,可以在return false 之前自己處理,比如關閉    當前頁面并傳遞返回值:
      WillPopScope(
            onWillPop: () async {
                // You can await in the calling widget for my_value and handle when complete.
                Navigator.pop(context, 'my_value');
                return false;
              },
              ...
      );

命名路由

基本使用

上面代碼是在沒個需要導航的地方聲明路由,不能復用,我們可以先給路由起一個名字,再注冊路由表,然后就可以通過路由名字直接打開新的路由了,這為路由管理帶來了一種直觀、簡單的方式,并且可以復用。

MaterialApp的routes屬性,既是注冊路由表用的,它對應一個Map<String, WidgetBuilder>。

起名:

  static const String page1 = "/page1";
  static const String page2 = "/page2";

聲明路由表:

  Map<String, WidgetBuilder> routes = {
    page1: (context) => Page1(),
    page2: (context) => Page2(),
  };

注冊路由表:

 @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: routes,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Page1(),
    );
  }

然后在需要路由的地方使用命名路由調用:

Navigator.pushNamed(context, page2)

傳遞參數

給page3起名:

    page3: (context) => Page3(),

打開路由時候傳遞參數:

              Navigator.of(context).pushNamed(page3, arguments: "hi");

page3中接收參數:

class Page3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //獲取路由參數
    var args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
      body: Container(
        child: Center(
          child: Text('Page 3的參數是$args',
              style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
        ),
      ),
    );
  }
}

構造函數傳參

上面我們明明給page3傳遞了參數,但是并非傳遞到構造函數上。我們看構造函數,并不知道傳遞了什么參數,必須去看路由,并不是很好的做法。那怎么給構造函數傳參呢?

起名:

const String page4 = "/page4";

注冊路由:

    page4: (context) => Page4(text: ModalRoute.of(context).settings.arguments),

打開路由時傳遞參數:

              Navigator.of(context).pushNamed(page4, arguments: "hello");

動態路由

MaterialApp還為我們提供了一個onGenerateRoute參數,未在路由表里注冊的路由,會在這里尋找。RouteFactory有一個RouteSettings參數,并返回一個Route<dynamic>。這是我們將用來執行所有路由的功能。

Route<dynamic> Function(RouteSettings settings)

我們可以這樣使用:
先聲明路由表:

Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6());
        default:
            return MaterialPageRoute(builder: (context) => Page1());
    }

注冊:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: routes,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      onGenerateRoute: generateRoute,
      home: Page1(),
    );
  }

打開路由:

              Navigator.of(context).pushNamed(page5);

動態路由傳遞參數

上面說了,settings可以拿到參數,我們當然就可以傳遞參數了:

Route<dynamic> generateRoute(RouteSettings settings) {
    print('====${settings.name}');
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
        default:
            return MaterialPageRoute(builder: (context) => Page1());
    }
}

使用:

              Navigator.of(context).pushNamed(page6, arguments: "world");

so easy。

處理未定義的路線

有兩種處理未定義路由的方法。

  1. 利用generateRoute,找不到路由名的返回默認路由
Route<dynamic> generateRoute(RouteSettings settings) {
    print('====${settings.name}');
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
        default:
            return MaterialPageRoute(builder: (context) => NotFindPage());
    }
}
  1. 利用onUnknownRoute返回默認路由
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        routes: routes,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        onGenerateRoute: generateRoute,
        onUnknownRoute: (settings) =>
            MaterialPageRoute(builder: (context) => NotFindPage()));
  }

初始路由

打開應用第一屏的路由,也有2種方式,

  1. 可以設置initialRoute,指定路由表里注冊的路由名。
  2. 可以設置home,對應的page。
    initialRoute會覆蓋home。
 Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        routes: routes,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        initialRoute: root,
        home: Page2(),
        onGenerateRoute: generateRoute,
        onUnknownRoute: (settings) =>
            MaterialPageRoute(builder: (context) => NotFindPage()));
  }

不使用BuildContext的路由導航

很多情況是,我們已將UI代碼從業務邏輯中分離出來(類似于MVVM架構)。viewModel應處理所有邏輯,視圖應僅調用模型上的函數,然后在需要時使用新狀態重建自身。

我們知道Navigator需要BuildContext的參數,我們在進行實際業務邏輯決策的位置進行導航,而不是在widget里調用路由,如果在viewModel里導航,就要傳入context嗎?下面實現不要context的導航。

為了遵守MVVM原則,我們將把Navigation功能移動到可以從viewModel調用的服務中。在lib下創建一個名為services的新文件夾,并在其中創建一個名為navigation_service.dart的新文件。

先實現單利模式:

class NavigationService {
  factory NavigationService.getInstance() => _getInstance();

  NavigationService._internal();

  static NavigationService _instance;

  static NavigationService _getInstance() {
    if (_instance == null) {
      _instance = new NavigationService._internal();
    }
    return _instance;
  }
  }
然后利用navigatorKey實現:
 final GlobalKey<NavigatorState> navigatorKey =
      new GlobalKey<NavigatorState>();

  Future<dynamic> navigateTo(String routeName) {
    return navigatorKey.currentState.pushNamed(routeName);
  }

  void goBack() {
    return navigatorKey.currentState.pop();
  }

我們將NavigationService與應用程序鏈接的方式,通過navigatorKey提供給MaterialApp。轉到main.dart文件并設置navigatorKey:

 MaterialApp(
        title: 'Flutter Demo',
        navigatorKey: NavigationService().navigatorKey,
        ...
        )

然后寫一個viewModel,嘗試導航:

class ViewModel {
  final NavigationService _navigationService = NavigationService();

  Future goPage1() async{
    /// 模擬請求數據后調到首頁
      await Future.delayed(Duration(seconds: 1));
      _navigationService.navigateTo(page1);
  }

}

在page6里使用viewModel導航:

  onPressed: () {
          viewModel.goPage1();
        },

現在,將View文件的職責帶回到了“顯示UI”并將用戶操作傳遞給模型,而不是“顯示UI”將用戶操作傳遞給模型并進行導航。更符合MVVM職責的劃分。

這樣做的好處是,隨著導航邏輯的擴展,我們的UI將保持不變,并且模型將承載所有邏輯/狀態管理。

代碼鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。