Flutter 路由和導(dǎo)航

大部分應(yīng)用程序都包含多個(gè)頁(yè)面,并希望用戶能從當(dāng)前屏幕平滑過(guò)渡到另一個(gè)屏幕。移動(dòng)應(yīng)用程序通常通過(guò)被稱為“屏幕”或“頁(yè)面”的全屏元素來(lái)顯示內(nèi)容。在 Flutter 中,這些元素被稱為路由(Route),它們由導(dǎo)航器(Navigator)控件管理。導(dǎo)航器管理著路由對(duì)象的堆棧并提供管理堆棧的方法,如 Navigator.pushNavigator.pop,通過(guò)路由對(duì)象的進(jìn)出棧來(lái)使用戶從一個(gè)頁(yè)面跳轉(zhuǎn)到另一個(gè)頁(yè)面。

查看示例代碼

基本用法

Navigator 的基本用法,從一個(gè)頁(yè)面跳轉(zhuǎn)到另一個(gè)頁(yè)面,通過(guò)第二頁(yè)面上的返回按鈕回到第一個(gè)頁(yè)面。

創(chuàng)建兩個(gè)頁(yè)面

首先創(chuàng)建兩個(gè)頁(yè)面,每個(gè)頁(yè)面包含一個(gè)按鈕。點(diǎn)擊第一個(gè)頁(yè)面上的按鈕將導(dǎo)航到第二個(gè)頁(yè)面。點(diǎn)擊第二個(gè)頁(yè)面上的按鈕將返回到第一個(gè)頁(yè)面。初始時(shí)顯示第一個(gè)頁(yè)面。

// main.dart
void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Navigation',
      home: new FirstScreen(),
    );
  }
}

// demo1_navigation.dart
class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('First Screen'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Launch second screen'),
          onPressed: null,
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Second Screen'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Go back!'),
          onPressed: null,
        ),
      ),
    );
  }
}

跳轉(zhuǎn)到第二頁(yè)面

為了導(dǎo)航到新的頁(yè)面,我們需要調(diào)用 Navigator.push 方法。該方法將添加 Route 到路由棧中!

我們可以直接使用 MaterialPageRoute 創(chuàng)建路由,它是一種模態(tài)路由,可以通過(guò)平臺(tái)自適應(yīng)的過(guò)渡效果來(lái)切換屏幕。默認(rèn)情況下,當(dāng)一個(gè)模態(tài)路由被另一個(gè)替換時(shí),上一個(gè)路由將保留在內(nèi)存中,如果想釋放所有資源,可以將 maintainState 設(shè)置為 false

給第一個(gè)頁(yè)面上的按鈕添加 onPressed 回調(diào):

onPressed: () {
  Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new SecondScreen()),
  );
},

返回第一個(gè)頁(yè)面

Scaffold 控件會(huì)自動(dòng)在 AppBar 上添加一個(gè)返回按鈕,點(diǎn)擊該按鈕會(huì)調(diào)用 Navigator.pop

現(xiàn)在希望點(diǎn)擊第二個(gè)頁(yè)面中間的按鈕也能回到第一個(gè)頁(yè)面,添加回調(diào)函數(shù),調(diào)用 Navigator.pop

onPressed: () {
  Navigator.pop(context);
}

頁(yè)面跳轉(zhuǎn)傳值

在進(jìn)行頁(yè)面切換時(shí),通常還需要將一些數(shù)據(jù)傳遞給新頁(yè)面,或是從新頁(yè)面返回?cái)?shù)據(jù)。考慮此場(chǎng)景:我們有一個(gè)文章列表頁(yè),點(diǎn)擊每一項(xiàng)會(huì)跳轉(zhuǎn)到對(duì)應(yīng)的內(nèi)容頁(yè)。在內(nèi)容頁(yè)中,有喜歡和不喜歡兩個(gè)按鈕,點(diǎn)擊任意按鈕回到列表頁(yè)并顯示結(jié)果。

我會(huì)接著上面的例子繼續(xù)編寫。

定義 Article 類

首先我們創(chuàng)建一個(gè) Article 類,擁有兩個(gè)屬性:標(biāo)題、內(nèi)容。

class Article {
  String title;
  String content;

  Article({this.title, this.content});
}

創(chuàng)建列表頁(yè)面和內(nèi)容頁(yè)面

列表頁(yè)面中初始化 10 篇文章,然后使用 ListView 顯示它們。
內(nèi)容頁(yè)面標(biāo)題顯示文章的標(biāo)題,主體部分顯示內(nèi)容。

class ArticleListScreen extends StatelessWidget {
  final List<Article> articles = new List.generate(
    10,
    (i) => new Article(
          title: 'Article $i',
          content: 'Article $i: The quick brown fox jumps over the lazy dog.',
        ),
  );

  @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) {
          return new ListTile(
            title: new Text(articles[index].title),
          );
        },
      ),
    );
  }
}

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 Text('${article.content}'),
      ),
    );
  }
}

跳轉(zhuǎn)到內(nèi)容頁(yè)并傳遞數(shù)據(jù)

接下來(lái),當(dāng)用戶點(diǎn)擊列表中的文章時(shí)將跳轉(zhuǎn)到ContentScreen,并將 article 傳遞給 ContentScreen
為了實(shí)現(xiàn)這一點(diǎn),我們將實(shí)現(xiàn) ListTileonTap 回調(diào)。 在的 onTap 回調(diào)中,再次調(diào)用Navigator.push方法。

return new ListTile(
  title: new Text(articles[index].title),
  onTap: () {
    Navigator.push(
      context,
      new MaterialPageRoute(
        builder: (context) => new ContentScreen(articles[index]),
      ),
    );
  },
);

內(nèi)容頁(yè)返回?cái)?shù)據(jù)

在內(nèi)容頁(yè)底部添加兩個(gè)按鈕,點(diǎn)擊按鈕時(shí)跳轉(zhuǎn)會(huì)列表頁(yè)面并傳遞參數(shù)。

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: () {
                  Navigator.pop(context, 'Like');
                },
                child: new Text('Like'),
              ),
              new RaisedButton(
                onPressed: () {
                  Navigator.pop(context, 'Unlike');
                },
                child: new Text('Unlike'),
              ),
            ],
          )
        ],
      ),
    ),
  );
}

修改 ArticleListScreen 列表項(xiàng)的 onTap 回調(diào),處理內(nèi)容頁(yè)面返回的數(shù)據(jù)并顯示。

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),
      ),
    );
  }
},

定制路由

通常,我們可能需要定制路由以實(shí)現(xiàn)自定義的過(guò)渡效果等。定制路由有兩種方式:

下面使用 PageRouteBuilder 實(shí)現(xiàn)一個(gè)頁(yè)面旋轉(zhuǎn)淡出的效果。

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),
      ),
    );
  }
},

命名導(dǎo)航器路由

通常,移動(dòng)應(yīng)用管理著大量的路由,并且最容易的是使用名稱來(lái)引用它們。路由名稱通常使用路徑結(jié)構(gòu):“/a/b/c”,主頁(yè)默認(rèn)為 “/”。

創(chuàng)建 MaterialApp 時(shí)可以指定 routes 參數(shù),該參數(shù)是一個(gè)映射路由名稱和構(gòu)造器的 Map。MaterialApp 使用此映射為導(dǎo)航器的 onGenerateRoute 回調(diào)參數(shù)提供路由。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Navigation',
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => new ArticleListScreen(),
        '/new': (BuildContext context) => new NewArticle(),
      },
    );
  }
}

路由的跳轉(zhuǎn)時(shí)調(diào)用 Navigator.pushNamed

Navigator.of(context).pushNamed('/new');

這里有一個(gè)問(wèn)題就是使用 Navigator.pushNamed 時(shí)無(wú)法直接給新頁(yè)面?zhèn)鲄?shù),目前官方還沒有標(biāo)準(zhǔn)解決方案,我知道的方案是在 onGenerateRoute 回調(diào)中利用 URL 參數(shù)自行處理。

onGenerateRoute: (RouteSettings settings) {
  WidgetBuilder builder;
  if (settings.name == '/') {
    builder = (BuildContext context) => new ArticleListScreen();
  } else {
    String param = settings.name.split('/')[2];
    builder = (BuildContext context) => new NewArticle(param);
  }

  return new MaterialPageRoute(builder: builder, settings: settings);
},

// 通過(guò) URL 傳遞參數(shù)
Navigator.of(context).pushNamed('/new/xxx');

嵌套路由

一個(gè) App 中可以有多個(gè)導(dǎo)航器,將一個(gè)導(dǎo)航器嵌套在另一個(gè)導(dǎo)航器下面可以創(chuàng)建一個(gè)內(nèi)部的路由歷史。例如:App 主頁(yè)有底部導(dǎo)航欄,每個(gè)對(duì)應(yīng)一個(gè) Navigator,還有與主頁(yè)處于同一級(jí)的全屏頁(yè)面,如登錄頁(yè)面等。接下來(lái),我們實(shí)現(xiàn)這樣一個(gè)路由結(jié)構(gòu)。

添加 Home 頁(yè)面

添加 Home 頁(yè)面,底部導(dǎo)航欄切換主頁(yè)和我的頁(yè)面。

import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _HomeState();
  }
}

class _HomeState extends State<Home> {
  int _currentIndex = 0;
  final List<Widget> _children = [
    new PlaceholderWidget('Home'),
    new PlaceholderWidget('Profile'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _children[_currentIndex],
      bottomNavigationBar: new BottomNavigationBar(
        onTap: onTabTapped,
        currentIndex: _currentIndex,
        items: [
          new BottomNavigationBarItem(
            icon: new Icon(Icons.home),
            title: new Text('Home'),
          ),
          new BottomNavigationBarItem(
            icon: new Icon(Icons.person),
            title: new Text('Profile'),
          ),
        ],
      ),
    );
  }

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

class PlaceholderWidget extends StatelessWidget {
  final String text;

  PlaceholderWidget(this.text);

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Text(text),
    );
  }
}

效果如下:

step1.gif

然后我們將 Home 頁(yè)面組件使用 Navigator 代替,Navigator 中有兩個(gè)路由頁(yè)面:home 和 demo1。home 顯示一個(gè)按鈕,點(diǎn)擊按鈕調(diào)轉(zhuǎn)到前面的 demo1 頁(yè)面。

import 'package:flutter/material.dart';

import './demo1_navigation.dart';

class HomeNavigator extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Navigator(
      initialRoute: 'home',
      onGenerateRoute: (RouteSettings settings) {
        WidgetBuilder builder;
        switch (settings.name) {
          case 'home':
            builder = (BuildContext context) => new HomePage();
            break;
          case 'demo1':
            builder = (BuildContext context) => new ArticleListScreen();
            break;
          default:
            throw new Exception('Invalid route: ${settings.name}');
        }

        return new MaterialPageRoute(builder: builder, settings: settings);
      },
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Home'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('demo1'),
          onPressed: () {
            Navigator.of(context).pushNamed('demo1');
          },
        ),
      ),
    );
  }
}

效果如下圖:

step2.gif

可以看到,點(diǎn)擊按鈕跳轉(zhuǎn)到 demo1 頁(yè)面后,底部的 tab 欄并沒有消失,因?yàn)檫@是在子導(dǎo)航器中進(jìn)行的跳轉(zhuǎn)。要想顯示全屏頁(yè)面覆蓋底欄,我們需要通過(guò)根導(dǎo)航器進(jìn)行跳轉(zhuǎn),也就是 MaterialApp 內(nèi)部的導(dǎo)航器。

我們?cè)?Profile 頁(yè)面中添加一個(gè)登出按鈕,點(diǎn)擊該按鈕會(huì)跳轉(zhuǎn)到登錄頁(yè)面。

// profile.dart

class Profile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Profile'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Log Out'),
          onPressed: () {
            Navigator.of(context).pushNamed('/login');
          },
        ),
      ),
    );
  }
}

// main.dart

import './home.dart';
import './login.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demos',
      routes: {
        '/': (BuildContext context) => new Home(),
        '/login': (BuildContext context) => new Login()
      },
    );
  }
}

最后效果如下:

step3.gif

至此,F(xiàn)lutter 路由和導(dǎo)航器的內(nèi)容就總結(jié)完畢,接下來(lái),學(xué)習(xí) Flutter 中如何進(jìn)行布局。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明AGI閱讀 16,012評(píng)論 3 119
  • 前言 學(xué)習(xí)本系列內(nèi)容需要具備一定 HTML 開發(fā)基礎(chǔ),沒有基礎(chǔ)的朋友可以先轉(zhuǎn)至 HTML快速入門(一) 學(xué)習(xí) 本人...
    珍此良辰閱讀 7,309評(píng)論 33 15
  • 桌凳擺放 全員30分 1、桌椅凳出現(xiàn)亂刻、亂畫,人為損壞,每項(xiàng)扣5分。 2、桌凳在預(yù)備零...
    哦是這個(gè)閱讀 240評(píng)論 0 0
  • “我想應(yīng)該靜下來(lái)看一本書 我想應(yīng)該靜下來(lái)做這些事 我想靜下來(lái)忘掉那些事 我只想靜下來(lái)去反省自己” 雷子這首《靜下來(lái)...
    阿貍的貍閱讀 585評(píng)論 0 1
  • 孔子說(shuō),“不遷怒,不貳過(guò)。”“其恕乎,己所不欲勿施于人。”這兩句話其實(shí)說(shuō)的是情緒發(fā)生前和發(fā)生時(shí),用英語(yǔ)里面時(shí)態(tài)表示...
    一個(gè)皮球的自述閱讀 260評(píng)論 0 0