Flutter路由管理指北

艾維碼發表於2020-05-21

路由管理

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()));
    複製程式碼
  2. 呼叫Navigator.pop返回前一個頁面

     Navigator.pop(context, result);
    
    複製程式碼
  3. 關閉頁面後獲取結果 有時候我們需要上個頁面關閉時傳遞一個返回值,幸運的是,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');
    複製程式碼
  4. 攔截返回鍵 如果不想點選返回鍵關閉當前頁面,可以使用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。這是我們將用來執行所有路由的功能。

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將保持不變,並且模型將承載所有邏輯/狀態管理。

程式碼連結

相關文章