Flutter 路由和导航

大部分应用程序都包含多个页面,并希望用户能从当前屏幕平滑过渡到另一个屏幕。移动应用程序通常通过被称为“屏幕”或“页面”的全屏元素来显示内容。在 Flutter 中,这些元素被称为路由(Route),它们由导航器(Navigator)控件管理。导航器管理着路由对象的堆栈并提供管理堆栈的方法,如 Navigator.pushNavigator.pop,通过路由对象的进出栈来使用户从一个页面跳转到另一个页面。

查看示例代码

基本用法

Navigator 的基本用法,从一个页面跳转到另一个页面,通过第二页面上的返回按钮回到第一个页面。

创建两个页面

首先创建两个页面,每个页面包含一个按钮。点击第一个页面上的按钮将导航到第二个页面。点击第二个页面上的按钮将返回到第一个页面。初始时显示第一个页面。

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

跳转到第二页面

为了导航到新的页面,我们需要调用 Navigator.push 方法。该方法将添加 Route 到路由栈中!

我们可以直接使用 MaterialPageRoute 创建路由,它是一种模态路由,可以通过平台自适应的过渡效果来切换屏幕。默认情况下,当一个模态路由被另一个替换时,上一个路由将保留在内存中,如果想释放所有资源,可以将 maintainState 设置为 false

给第一个页面上的按钮添加 onPressed 回调:

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

返回第一个页面

Scaffold 控件会自动在 AppBar 上添加一个返回按钮,点击该按钮会调用 Navigator.pop

现在希望点击第二个页面中间的按钮也能回到第一个页面,添加回调函数,调用 Navigator.pop

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

页面跳转传值

在进行页面切换时,通常还需要将一些数据传递给新页面,或是从新页面返回数据。考虑此场景:我们有一个文章列表页,点击每一项会跳转到对应的内容页。在内容页中,有喜欢和不喜欢两个按钮,点击任意按钮回到列表页并显示结果。

我会接着上面的例子继续编写。

定义 Article 类

首先我们创建一个 Article 类,拥有两个属性:标题、内容。

class Article {
  String title;
  String content;

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

创建列表页面和内容页面

列表页面中初始化 10 篇文章,然后使用 ListView 显示它们。
内容页面标题显示文章的标题,主体部分显示内容。

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

跳转到内容页并传递数据

接下来,当用户点击列表中的文章时将跳转到ContentScreen,并将 article 传递给 ContentScreen
为了实现这一点,我们将实现 ListTileonTap 回调。 在的 onTap 回调中,再次调用Navigator.push方法。

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

内容页返回数据

在内容页底部添加两个按钮,点击按钮时跳转会列表页面并传递参数。

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 列表项的 onTap 回调,处理内容页面返回的数据并显示。

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

定制路由

通常,我们可能需要定制路由以实现自定义的过渡效果等。定制路由有两种方式:

下面使用 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),
      ),
    );
  }
},

命名导航器路由

通常,移动应用管理着大量的路由,并且最容易的是使用名称来引用它们。路由名称通常使用路径结构:“/a/b/c”,主页默认为 “/”。

创建 MaterialApp 时可以指定 routes 参数,该参数是一个映射路由名称和构造器的 Map。MaterialApp 使用此映射为导航器的 onGenerateRoute 回调参数提供路由。

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

路由的跳转时调用 Navigator.pushNamed

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

这里有一个问题就是使用 Navigator.pushNamed 时无法直接给新页面传参数,目前官方还没有标准解决方案,我知道的方案是在 onGenerateRoute 回调中利用 URL 参数自行处理。

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

// 通过 URL 传递参数
Navigator.of(context).pushNamed('/new/xxx');

嵌套路由

一个 App 中可以有多个导航器,将一个导航器嵌套在另一个导航器下面可以创建一个内部的路由历史。例如:App 主页有底部导航栏,每个对应一个 Navigator,还有与主页处于同一级的全屏页面,如登录页面等。接下来,我们实现这样一个路由结构。

添加 Home 页面

添加 Home 页面,底部导航栏切换主页和我的页面。

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 页面组件使用 Navigator 代替,Navigator 中有两个路由页面:home 和 demo1。home 显示一个按钮,点击按钮调转到前面的 demo1 页面。

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

可以看到,点击按钮跳转到 demo1 页面后,底部的 tab 栏并没有消失,因为这是在子导航器中进行的跳转。要想显示全屏页面覆盖底栏,我们需要通过根导航器进行跳转,也就是 MaterialApp 内部的导航器。

我们在 Profile 页面中添加一个登出按钮,点击该按钮会跳转到登录页面。

// 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

至此,Flutter 路由和导航器的内容就总结完毕,接下来,学习 Flutter 中如何进行布局。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,968评论 3 119
  • 前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习 本人...
    珍此良辰阅读 7,274评论 33 15
  • 桌凳摆放 全员30分 1、桌椅凳出现乱刻、乱画,人为损坏,每项扣5分。 2、桌凳在预备零...
    哦是这个阅读 214评论 0 0
  • “我想应该静下来看一本书 我想应该静下来做这些事 我想静下来忘掉那些事 我只想静下来去反省自己” 雷子这首《静下来...
    阿狸的狸阅读 568评论 0 1
  • 孔子说,“不迁怒,不贰过。”“其恕乎,己所不欲勿施于人。”这两句话其实说的是情绪发生前和发生时,用英语里面时态表示...
    一个皮球的自述阅读 230评论 0 0