Flutter—— 路由(Route)和導航(Navigator),實現頁面管理

Flutter的頁面,怎么進行跳轉的呢?通過路由和導航呢。

一、路由和導航,初認識

言簡意賅!

  • 路由(Route) : route是一個屏幕或頁面的抽象(可以大概理解為安卓的Activity)
  • 導航(Navigator) : Navigator是管理route的Widget。導航器管理著路由對象的堆棧并提供管理堆棧的方法,如 Navigator.push入棧 和 Navigator.pop出棧

Navigator可以通過route入棧和出棧來實現頁面之間的跳轉。


一1、最簡單的頁面跳轉

一個頁面,在Flutter里面,被理解為一個路由。
多個路由,可以存在與同一個dart文件中的。

  • 使用Navigator.pushNamed方法首先需要在 MaterialApp 中定義routes。
import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(title:'導航頁面示例', home: new Demo()));
}

class Demo extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return new Scaffold(
      appBar: new AppBar(
        title: Text('導航頁面示例'),
      ),
      body: new Center(
          child:RaisedButton(
            child: Text('查看詳情頁面'),
            onPressed: (){
              Navigator.push(context, MaterialPageRoute(builder: (context)=>new SecondScreen()));
            },
          )
      ),
    );
  }
}

class SecondScreen extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('導航頁面第二屏'),
      ),
      body: new Center(
        child: new RaisedButton(
          onPressed: (){
            Navigator.pop(context);
          },
          child: new Text('返回頁面'),
        ),
      ),
    );
  }
}

.
.

從上面的例子,我們看到進入新頁面,用push(入棧),返回上個頁面,用pop(出棧)。

MaterialPageRoute

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

    • 對于Android,當打開新頁面時,新的頁面會從屏幕底部滑動到屏幕頂部;當關閉頁面時,當前頁面會從屏幕頂部滑動到屏幕底部后消失,同時上一個頁面會顯示到屏幕上。

    • 對于iOS,當打開頁面時,新的頁面會從屏幕右側邊緣一致滑動到屏幕左邊,直到新頁面全部顯示到屏幕上,而上一個頁面則會從當前屏幕滑動到屏幕左側而消失;當關閉頁面時,正好相反,當前頁面會從屏幕右側滑出,同時上一個頁面會從屏幕左側滑入。

  • 默認情況下,當一個模態路由被另一個替換時,上一個路由將保留在內存中,如果想釋放所有資源,可以將 maintainState 設置為 false。

MaterialPageRoute的構造函數

 MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • builder 是一個WidgetBuilder類型的回調函數,它的作用是構建路由頁面的具體內容,返回值是一個widget。我們通常要實現此回調,返回新路由的實例。

  • settings 包含路由的配置信息,如路由名稱、是否初始路由(首頁)。

  • maintainState:默認情況下,當入棧一個新路由時,原來的路由仍然會被保存在內存中,如果想在路由沒用的時候釋放其所占用的所有資源,可以設置maintainState為false。

  • fullscreenDialog表示新的路由頁面是否是一個全屏的模態對話框,在iOS中,如果fullscreenDialog為true,新頁面將會從屏幕底部滑入(而不是水平方向)。

.
.
示例:

1.gif

一.2、跳轉頁面時傳遞數據

  • 使用Navigator.push接收參數的重點在構造函數,通過在構造函數中接受參數進行傳遞

傳遞數據,其實也很簡單。P1跳轉到P2,P2的構造函數預留好參數,P1跳轉到P2的時候,傳遞一下即可。


import 'package:flutter/material.dart';

class Product {
  final String title;
  final String description;
  Product(this.title,this.description);
} //Product 類 屬性

void main(){
  runApp(new MaterialApp(
      title:'傳遞數據示例',
      home:new ProductList(
          products:new List.generate(20, (i)=>new Product('商品 $i', '這是一個商品的詳情 $i')) //父子傳值
      )
  ));
}


class ProductList extends StatelessWidget{
  final List<Product> products;
  ProductList({Key key,@required this.products}):super(key:key);
  @override
  Widget build(BuildContext context){
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('商品列表'),
      ),
      body: new ListView.builder(
          itemCount:products.length,
          itemBuilder:(context,index){
            return new ListTile(
              title:new Text(products[index].title),
              onTap: (){
                Navigator.push(
                    context,
                    // 傳遞數據
                    new MaterialPageRoute(
                        builder: (context)=>new ProductDetail(product:products[index])
                    )
                );
              },
            );
          }
      ),
    );
  }
}

class ProductDetail extends StatelessWidget {
  final Product product;
  ProductDetail({Key key,@required this.product}):super(key:key);
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('${product.title}'),
      ),
      body: new Padding(
        padding: new EdgeInsets.all(16.0),
        child: new Text('${product.description}'),
      ),
    );
  }
}

.
.
效果:

2.gif

一3、頁面返回數據

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(title:'導航頁面示例', home: new ArticleListScreen()));
}

class Article {
  String title;
  String content;

  Article({this.title, this.content});
}
class ArticleListScreen extends StatelessWidget {
  final List<Article> articles = new List.generate(
    10,
        (i) => new Article(
      title: 'Article $i',
      content: '文章 $i: 你喜歡這個文章嗎親.',
    ),
  );

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Article List'),
      ),
      body: new ListView.builder(
        itemCount: articles.length,
        itemBuilder: (context, index) {
          // 當用戶點擊列表中的文章時將跳轉到ContentScreen,并將 article 傳遞給 ContentScreen。
          //為了實現這一點,我們將實現 ListTile 的 onTap 回調。 在的 onTap 回調中,再次調用Navigator.push方法。

          return new ListTile(
              title: new Text(articles[index].title),
              // 列表項的 onTap 回調,處理內容頁面返回的數據并顯示。
              /*onTap: () {
                Navigator.push(
                  context,
                  new MaterialPageRoute(
                    builder: (context) => new ContentScreen(articles[index]),
                  ),
                );
              },*/

              onTap: () async {
                // 接受頁面返回值
                String result = await Navigator.push(
                  context,
                  new MaterialPageRoute(
                    // 文章頁面跳轉到內容頁面,傳遞個值
                    builder: (context) => new ContentScreen(articles[index]),
                  ),
                );

                if (result != null) {
                  Scaffold.of(context).showSnackBar(
                    new SnackBar(
                      // 顯示一下從別人頁面傳遞過來的值,如果有值的話
                      content: new Text("$result"),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                }
              },
          );
        },
      ),
    );
  }
}



class ContentScreen extends StatelessWidget {
  final Article article;

  ContentScreen(this.article);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('${article.title}'),
      ),
      body: new Padding(
          padding: new EdgeInsets.all(15.0),
          child: new Column(
            children: <Widget>[
              new Text('${article.content}'),
              new Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  new RaisedButton(
                    onPressed: () {
                      // pop 第二個參數,就是表示給上個頁面傳遞
                      Navigator.pop(context, 'Like');
                    },
                    child: new Text('Like'),
                  ),
                  new RaisedButton(
                    onPressed: () {
                      // pop 第二個參數,就是表示給上個頁面傳遞
                      Navigator.pop(context, 'Unlike');
                    },
                    child: new Text('Unlike'),
                  ),
                ],
              )
            ],
          ),

      ),
    );
  }
}

  • 返回數據,主要就是pop的時候返回,然后想接受的,在push的時候,就接受下

.
.
效果

3.gif

二、定制路由

通常,我們可能需要定制路由以實現自定義的過渡效果等。定制路由有兩種方式:

  • 繼承路由子類,如:PopupRoute、ModalRoute 等。
  • 使用 PageRouteBuilder 類通過回調函數定義路由。
  • 下面使用 PageRouteBuilder 實現一個頁面旋轉淡出的效果。
onTap: () async {
  String result = await Navigator.push(
      context,
      new PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 1000),
        pageBuilder: (context, _, __) =>
            new ContentScreen(articles[index]),
        transitionsBuilder:
            (_, Animation<double> animation, __, Widget child) =>
                new FadeTransition(
                  opacity: animation,
                  child: new RotationTransition(
                    turns: new Tween<double>(begin: 0.0, end: 1.0)
                        .animate(animation),
                    child: child,
                  ),
                ),
      ));

  if (result != null) {
    Scaffold.of(context).showSnackBar(
      new SnackBar(
        content: new Text("$result"),
        duration: const Duration(seconds: 1),
      ),
    );
  }
},

三、命名路由

在Flutter最初的版本中,命名路由是不能傳遞參數的,后來才支持了參數.

當使用 initialRoute 時,需要確保你沒有同時定義 home 屬性。

通常,移動應用管理著大量的路由,并且最容易的是使用名稱來引用它們。路由名稱通常使用路徑結構:“/a/b/c”,主頁默認為 “/”

MaterialApp(
  // Start the app with the "/" named route. In this case, the app starts
  // on the FirstScreen widget.
  
  // 使用“/”命名路由來啟動應用(Start the app with the "/" named route. In our case, the app will start)
  // 在這里,應用將從 FirstScreen Widget 啟動(on the FirstScreen Widget)
  
  initialRoute: '/',
  routes: {
    // When navigating to the "/" route, build the FirstScreen widget.
    // 當我們跳轉到“/”時,構建 FirstScreen Widget(When we navigate to the "/" route, build the FirstScreen Widget)
    '/': (context) => FirstScreen(),
    // When navigating to the "/second" route, build the SecondScreen widget.
    // 當我們跳轉到“/second”時,構建 SecondScreen Widget(When we navigate to the "/second" route, build the SecondScreen Widget)
    '/second': (context) => SecondScreen(),
  },
);

創建 MaterialApp 時可以指定 routes 參數,該參數是一個映射路由名稱和構造器的 Map。MaterialApp 使用此映射為導航器的 onGenerateRoute 回調參數提供路由。

注冊路由

      routes: {
        // 注冊路由
        "parameters_page":(context)=>ParametersRoute(),
      },

在路由頁通過RouteSetting對象獲取路由參數:

class ParametersRoute extends StatelessWidget {

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

在打開路由時傳遞參數

onPressed: () {
    //導航到一個新的路由頁面
    //Navigator.pushNamed(context, "third_page");
    Navigator.of(context).pushNamed("parameters_page",arguments:"命名路由傳遞的參數");
},

指定給 parameters_page路由 傳遞參數 "命名路由傳遞的參數"

更加常見的,是弄個路由表

本部分和示例無關。

  • 提供一個路由表,這是一個Map,是字符串和WidgetBuilder的對應關系。比如:
/// 路由表
final Map<String, WidgetBuilder> routeTable = {
  '/' : (content) => Home(),
  '/page1' : (content) => Page1(),
  '/page2' : (content) => Page2(),
  '/page3' : (content) => Page3(),
};
  • 把這個路由表放在MaterialApp的routes參數中
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: routeTable,
    );
  }
}
  • 使用的例子如下,比如跳轉到Page1頁面:
onPressed: (){
    Navigator.of(context).pushNamed('/page1');
}

一個示例代碼

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '路由測試',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
        // 注冊路由
        "home": (context) => MyHomePage(),
        "parameters_page": (context) => ParametersRoute(),
      },
      home: new MyHomePage(title: '路由測試'),
    );
  }
}

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>[
            FlatButton(
              child: Text(
                "攜帶參數打開新頁面",
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              textColor: Colors.blue,
              onPressed: ()  {
                //導航到一個新的路由頁面
//                Navigator.pushNamed(context, "third_page");
                 Navigator.of(context)
                    .pushNamed("parameters_page", arguments: "命名路由傳遞的參數");
              },
            )
          ],
        ),
      ),
    );
  }
}

class ParametersRoute extends StatelessWidget {
  final Topic = Text("路由測試");

  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    var args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
      appBar: AppBar(
        title: Text("路由測試"),
      ),
      body: new ListView(

        children: <Widget>[
          
          Center(
            child: Text("路由到此頁面獲取到的數據為:\n\n" + args),
          ),
        ],

      )
    );
  }
}

.
.

4.gif

四、onGenerateRoute 攔截器

假設我們要開發一個電商APP,當用戶沒有登錄時可以看店鋪、商品等信息.
但交易記錄、購物車、用戶個人信息等頁面需要登錄后才能看。為了實現上述功能,我們需要在打開每一個路由頁前判斷用戶登錄狀態!如果每次打開路由前我們都需要去判斷一下將會非常麻煩,那有什么更好的辦法嗎?答案是有! —— onGenerateRoute

  • onGenerateRoute 可以做攔截器
  • onGenerateRoute可以變相的接受參數
  • 可以在onGenerateRoute()函數中提取參數并將它們傳遞給widget,而不是直接在窗口小部件中提取參數。
  • 注意,onGenerateRoute只會對命名路由生效

.
.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 提供處理命名路由的函數。使用此功能可識別要推送的命名路徑,并創建正確的頁面。
      onGenerateRoute: (settings) {
        // 如果您要 打開 PassArguments 路由
        if (settings.name == PassArgumentsScreen.routeName) {
          // 將參數轉換為正確的類型:ScreenArguments。
          final ScreenArguments args = settings.arguments;

          // 從參數中提取所需數據并將數據傳遞到正確的屏幕。
          return MaterialPageRoute(
            builder: (context) {
              return PassArgumentsScreen(
                title: args.title,
                message: args.message,
              );
            },
          );
        }
      },
      title: 'Navigation with Arguments',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // A button that navigates to a named route that. The named route
            // extracts the arguments by itself.
            RaisedButton(
              child: Text("帶參數 push 方式"),
              onPressed: () {
                // When the user taps the button, navigate to the specific route
                // and provide the arguments as part of the RouteSettings.
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ExtractArgumentsScreen(),
                    // Pass the arguments as part of the RouteSettings. The
                    // ExtractArgumentScreen reads the arguments from these
                    // settings.
                    settings: RouteSettings(
                      arguments: ScreenArguments(
                        'tag1  來自HomeScreen的 push',
                        'tag1  這個消息將被  build 方法 提取',
                      ),
                    ),
                  ),
                );
              },
            ),
            // A button that navigates to a named route. For this route, extract
            // the arguments in the onGenerateRoute function and pass them
            // to the screen.
            RaisedButton(
              child: Text("帶參 pushNamed onGenerateRoute 方式"),
              onPressed: () {
                // When the user taps the button, navigate to a named route
                // and provide the arguments as an optional parameter.
                Navigator.pushNamed(
                  context,
                  PassArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'tag2  來自HomeScreen的 pushNamed',
                    'tag2  這個消息來自 將被 onGenerateRoute 函數提取',
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// 一個Widget,它從ModalRoute中提取必要的參數。
class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    // 從當前ModalRoute設置中提取參數并將其轉換為ScreenArguments。
    final ScreenArguments args = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(args.title),
      ),
      body: Center(
        child: Text(args.message),
      ),
    );
  }
}

// Widget,通過構造函數接受必要的參數。
class PassArgumentsScreen extends StatelessWidget {
  static const routeName = '/passArguments';

  final String title;
  final String message;

  // 此Widget接受參數作為構造函數參數。它不從ModalRoute中提取參數。
  //
  // 參數由提供給MaterialApp小部件的onGenerateRoute函數提取。
  const PassArgumentsScreen({
    Key key,
    @required this.title,
    @required this.message,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}

// 您可以將任何對象傳遞給arguments參數。在此示例中,創建一個包含可自定義標題和消息的類。
class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}

這是官方的例子,稍微改了點文字

5.gif

.
.
END

.
.
.

參數:
Flutter (十五) 路由及導航

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

Flutter 路由和導航

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

推薦閱讀更多精彩內容