6.2.初識Flutter應用之路由管理

codersay發表於2019-07-23

路由管理

路由(Route)在移動開發中通常指頁面(Page),這跟web開發中單頁應用的Route概念意義是相同的,Route在Android中通常指一個Activity,在iOS中指一個ViewController。所謂路由管理,就是管理頁面之間如何跳轉,通常也可被稱為導航管理。這和原生開發類似,無論是Android還是iOS,導航管理都會維護一個路由棧,路由入棧(push)操作對應開啟一個新頁面,路由出棧(pop)操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。

我們在上一節“計數器”示例的基礎上,做如下修改:

1.建立一個新路由,命名“SecondPageRoute”

class SecondPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第二個路由頁面"),
      ),
    );
  }
}

新路由繼承自StatelessWidget,介面很簡單,在頁面中間顯示一句"這是第二個路由頁面"。

2.在_MyHomePageState.build方法中的Column的子widget中新增一個按鈕(FlatButton)

Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      ... //省略無關程式碼
            FlatButton(
              child: Text("點我開啟一個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.push(context,
                  new MaterialPageRoute(builder: (context) {
                    return SecondPageRoute();
                  })
                );
              },
            ),
       ],
 )

我們新增了一個開啟新路由的按鈕,並將按鈕文字顏色設定為藍色,點選該按鈕後就會開啟新的路由頁面。

avatar
avatar

完整程式碼

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '【全棧程式設計】- onajax.com',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: '【全棧程式設計】- onajax.com'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              '【全棧程式設計】- onajax.com提示:你已經點選了--',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            FlatButton(
              child: Text("點我開啟一個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.push(context,
                  new MaterialPageRoute(builder: (context) {
                    return SecondPageRoute();
                  })
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '【全棧程式設計】- onajax.com',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class SecondPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第二個路由頁面"),
      ),
    );
  }
}

MaterialPageRoute

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

MaterialPageRoute 是Material元件庫的一個Widget,它可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫:

對於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,新頁面將會從螢幕底部滑入(而不是水平方向)。

如果想自定義路由切換動畫,可以自己繼承PageRoute來實現,我們將在後面介紹動畫時,實現一個自定義的路由Widget。

Navigator是一個路由管理的widget,它通過一個棧來管理一個路由widget集合。通常當前螢幕顯示的頁面就是棧頂的路由。Navigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:

Future push(BuildContext context, Route route)

將給定的路由入棧(即開啟新的頁面),返回值是一個Future物件,用以接收新路由出棧(即關閉)時的返回資料。

bool pop(BuildContext context, [ result ])

將棧頂路由出棧,result為頁面關閉時返回給上一個頁面的資料。

Navigator 還有很多其它方法,如Navigator.replace、Navigator.popUntil等,詳情請參考API文件或SDK原始碼註釋,在此不再贅述。下面我們還需要介紹一下路由相關的另一個概念“命名路由”。

例項方法

Navigator類中第一個引數為context的靜態方法都對應一個Navigator的例項方法, 比如Navigator.push(BuildContext context, Route route)等價於Navigator.of(context).push(Route route) ,下面命名路由相關的方法也是一樣的。

命名路由

所謂命名路由(Named Route)即給路由起一個名字,然後可以通過路由名字直接開啟新的路由。這為路由管理帶來了一種直觀、簡單的方式。

路由表

要想使用命名路由,我們必須先提供並註冊一個路由表(routing table),這樣應用程式才知道哪個名稱與哪個路由Widget對應。路由表的定義如下:

Map<String, WidgetBuilder> routes;

它是一個Map, key 為路由的名稱,是個字串;value是個builder回撥函式,用於生成相應的路由Widget。我們在通過路由名稱入棧新路由時,應用會根據路由名稱在路由表中找到對應的WidgetBuilder回撥函式,然後呼叫該回撥函式生成路由widget並返回。

註冊路由表

我們需要先註冊路由表後,我們的Flutter應用才能正確處理命名路由的跳轉。註冊方式很簡單,我們回到之前“計數器”的示例,然後在MyApp類的build方法中找到MaterialApp,新增routes屬性,程式碼如下:

return new MaterialApp(
  title: '【全棧程式設計】- onajax.com',
  theme: new ThemeData(
    primarySwatch: Colors.blue,
  ),

  routes: {
    "third_page":(context)=>ThirdPageRoute(),
  },

  home: new MyHomePage(title: '【全棧程式設計】- onajax.com'),
);

//同時新增第三個頁面的程式碼
class ThirdPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第三個路由頁面"),
      ),
    );
  }
}

現在我們就完成了路由表的註冊。

通過路由名開啟新路由頁

要通過路由名稱來開啟新路由,可以使用:

Future pushNamed(BuildContext context, String routeName,{Object arguments})
Navigator 除了pushNamed方法,還有pushReplacementNamed等其他管理命名路由的方法,讀者可以自行檢視API文件。

接下來我們通過路由名來開啟新的路由頁,再新增一個FlatButton的onPressed回撥程式碼,改為:

FlatButton(
  child: Text("點我開啟第三個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
  textColor: Colors.green,
  onPressed: () {
    //導航到一個新的路由頁面
    Navigator.pushNamed(context, "third_page");
  },
)

熱過載應用,再次點選“點我開啟第三個新頁面”按鈕,依然可以開啟新的路由頁。

avatar
avatar
avatar
avatar
avatar

完整程式碼

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '【全棧程式設計】- onajax.com',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),

      routes: {
        "third_page":(context)=>ThirdPageRoute(),
      },

      home: new MyHomePage(title: '【全棧程式設計】- onajax.com'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              '【全棧程式設計】- onajax.com提示:你已經點選了--',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            FlatButton(
              child: Text("點我開啟第二個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.push(context,
                  new MaterialPageRoute(builder: (context) {
                    return SecondPageRoute();
                  })
                );

              },
            ),
            FlatButton(
              child: Text("點我開啟第三個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.pushNamed(context, "third_page");
              },
            )
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '【全棧程式設計】- onajax.com',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class SecondPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第二個路由頁面"),
      ),
    );
  }
}

class ThirdPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第三個路由頁面"),
      ),
    );
  }
}

命名路由引數

在Flutter最初的版本中,命名路由是不能傳遞引數的,後來才支援了引數;下面展示命名路由如何傳遞並獲取路由引數:

我們先註冊一個路由:

 routes:{
   "parameters_page":(context)=>ParametersRoute(),
  } ,
  

在路由頁通過RouteSetting物件獲取路由引數:

class ParametersRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //獲取路由引數  
    var args = ModalRoute.of(context).settings.arguments
    //...省略無關程式碼
  }
}
  

在開啟路由時傳遞引數

Navigator.of(context).pushNamed("parameters_page", arguments: "【全棧程式設計】- onajax.com");

avatar
avatar

完整程式碼

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '【全棧程式設計】- onajax.com',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),

      routes: {
        "third_page":(context)=>ThirdPageRoute(),
        "parameters_page":(context)=>ParametersRoute(),
      },

      home: new MyHomePage(title: '【全棧程式設計】- onajax.com'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              '【全棧程式設計】- onajax.com提示:你已經點選了--',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            FlatButton(
              child: Text("點我開啟第二個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.push(context,
                  new MaterialPageRoute(builder: (context) {
                    return SecondPageRoute();
                  })
                );

              },
            ),
            FlatButton(
              child: Text("點我開啟第三個新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
                Navigator.pushNamed(context, "third_page");
              },
            ),
            FlatButton(
              child: Text("攜帶引數開啟新頁面",style: TextStyle(fontWeight: FontWeight.bold),),
              textColor: Colors.green,
              onPressed: () {
                //導航到一個新的路由頁面
//                Navigator.pushNamed(context, "third_page");
                Navigator.of(context).pushNamed("parameters_page",arguments:"【全棧程式設計】- onajax.com");
              },
            )
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '【全棧程式設計】- onajax.com',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class SecondPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第二個路由頁面"),
      ),
    );
  }
}

class ThirdPageRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("這是第三個路由頁面"),
      ),
    );
  }
}

class ParametersRoute extends StatelessWidget {

  final Topic = Text("【全棧程式設計】- onajax.com");
  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    var args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
      appBar: AppBar(
        title: Text("【全棧程式設計】- onajax.com"),
      ),
      body: Center(
        child: Text("路由到此頁面獲取到的資料為:\n" + args),
      ),
    );
  }
}

參照

相關文章