Flutter 导航教程

原文:Flutter Navigation Tutorial
作者:Joe Howard
译者:kmyhy

比只有一屏的 app 更好的是什么?当然是有两屏的 app 了:]

导航是移动 app UX 的重要组成部分。由于手机屏幕资源有限,用户需要不停地在各个屏幕之间进行导航,例如,从一个表格导航到详情屏幕,从购物车导航到结算屏,从菜单导航到表单,等等。一个良好的导航能帮助用户不迷失方向并在宽广的 app 中进退自如。

iOS 的导航通常是哟 UINavigationController,这是一个栈式的屏幕切换方式。在 Android 中,则主要用 activity 栈为用户导航。在这些栈中,两个屏幕之间的动画是不一样的,从而导致 app 风格不一。

和原生 SDK 一样,跨平台开发框架也为 app 提供了屏幕切换方式。大部分情况下,你想让每个平台上的导航方式和用户预期的保持一致。

Flutter 是一个 Google 的跨平台开发 SDK,允许你基于同一套代码快速创建 iOS 和 Android app。如果你没有接触过 Flutter,请阅读我们的 Flutter 开始教程,以了解基本的 Flutter 使用。

在本教程中,你将了解 Flutter 如何在一个跨平台 app 中实现屏幕间的导航,包括:

  • 路由和导航
  • 从栈中弹出
  • 从路由中返回值
  • 自定义导航动画

开始

你可以在本文头部或底部下载开始项目。
本教程将使用安装了 Flutter 扩展的 VSCode。你也可以用 IntelliJ IDEA 或 Android Studio 或任意文本编辑工具并在命令行中使用 Flutter。

在 VSCode 中选择 File > Open 并找到开始项目解压后的根文件夹,打开开始项目:

Flutter 导航教程_第1张图片

VSCode 会提示下载项目要用到的包,请根据提示进行:

Flutter 导航教程_第2张图片

项目打开后,按 F5 ,build & run。如果 VSCode 提示你选择 app 执行环境,请选择 Dart & Flutter:

Flutter 导航教程_第3张图片

这是在 iOS 模拟器中运行项目:

Flutter 导航教程_第4张图片

这是在 Android 模拟器中运行项目:

Flutter 导航教程_第5张图片

“slow mode” 标志显示,表示你正在以 debug 模式运行 app。

开始 app 显示了一个 GitHub 组织的成员列表。在本教程中,我们将从这个第一屏导航到每个成员单独的屏幕。

第二屏

首先要创建针对每个成员的屏幕画面。每个 Flutter UI 的元素都是 UI widget,因此我们需要创建 member widget。

首先,右键点击项目中的 lib 文件夹,选择 New File,创建一个新文件,名为 memberwidget.dart:

Flutter 导航教程_第6张图片

添加导入语句,添加一个 StatefulWidget 子类,名为 MemberWidget:

import 'package:flutter/material.dart';

import 'member.dart';


class MemberWidget extends StatefulWidget {
  final Member member;

  MemberWidget(this.member)  {
    if (member == null) {
      throw new ArgumentError("member of MemberWidget cannot be null. "
          "Received: '$member'");
    }
  }

  @override
  createState() => new MemberState(member);
}

MemberWidget 用 MemberState 类来保存它的状态,并传递一个 Member 对象给 MemberState。在 widget 的构造函数中,确保 member 参数不为空。

在同一文件中 MemberWidget 的上面添加一个 MemberState 类:

class MemberState extends State<MemberWidget> {
  final Member member;

  MemberState(this.member);
}

这里声明了一个 Member 属性和一个构造函数。

每个 widget 都必须重写 build() 方法,因此在 MemberState 中重写该方法:

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      child: new Image.network(member.avatarUrl)
    )
  );
}

这里创建了一个 Scaffold,即材料设计的容器,它包含了一个 AppBar 和一个child 为成员头像 Image 的 Padding widget。

成员的屏幕已经写好,接下来可以进行导航了:]

路由

在 Flutter 中导航是基于路由概念的。

路由就好比 REST API 中的路由概念,每个路由都是相对于根的。app 中的 main() 方法所创建的 widget 就是根。

一种使用路由的方法就是 PageRoute 类。因为当前 app 是一个 Flutter Material App,你需要用的是 MaterialPageRoute 子类。

在 GHFlutterState 头部加入 import 语句,以便能够调用 member widget:

import 'memberwidget.dart';

然后在 ghflutterwidget.dart 中为 GHFlutterState 添加私有方法:

_pushMember(Member member) {
  Navigator.of(context).push(
    new MaterialPageRoute(
      builder: (context) => new MemberWidget(member)
    )
  );
}

这里用 Navigator 来 push 一个 MaterialPageRoute 到导航栈中,而这个 MaterialPageRoute 用你的 MemberWidget 进行构造。

现在的用户点击表格行时需要调用 _pushMemeber()。你需要修改 GHFlutterState 中的 _buildRow() 方法,在 ListTile 中添加一个 onTap 属性:

Widget _buildRow(int i) {
  return new Padding(
    padding: const EdgeInsets.all(16.0),
    child: new ListTile(
      title: new Text("${_members[i].login}", style: _biggerFont),
      leading: new CircleAvatar(
          backgroundColor: Colors.green,
          backgroundImage: new NetworkImage(_members[i].avatarUrl)
      ),
      // Add onTap here:
      onTap: () { _pushMember(_members[i]); },
    )
  );
}

当行被点击,_pushMember() 方法被调用,同时传递所选中的 member。

按 F5 build & run。点击某一行,你会看到成员详情屏显示:

这是在 iOS 中运行的效果:

注意,Android 中的返回按钮是 Android 风格的,而 iOS 中的返回按钮是 iOS 风格的,屏幕转换动画和对应平台保持一致。

点击返回按钮,回到列表页,如果你想用在 app 中用你自己的按钮来触发返回该怎么做?

弹出

因为在 Flutter app 中的导航使用栈,同时你已经 push 了一个新的屏幕 widget 到栈中,那么为了返回上一屏,你必须对栈进行 pop 操作。

修改 MemberState 的 build() 方法,添加一个 IconButton,将 Image 替换成一个 Column widget:

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      // Add Column here:
      child: new Column(
        children: [
          new Image.network(member.avatarUrl),
          new IconButton(
            icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
            onPressed: () { Navigator.pop(context); }
            ),
        ]),
    )
  );
}

为了将 Image 和 IconButton 垂直布局,你添加了一个 Column。对于 IconButton,你将 onPressed 属性设置为调用 Navigator 来对栈进行 pop。

按 F5 build & run,你可以点击新加的返回箭头来回到成员列表:

返回值

路由也可以有返回值,就像 Android 使用 onActivityResult() 来读取结果一样。

来看个简单例子,在 MemberState 中添加下列私有的异步方法:

_showOKScreen(BuildContext context) async {
  // 1, 2
  bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>(
    builder: (BuildContext context) {
      return new Padding(
        padding: const EdgeInsets.all(32.0),
        // 3
        child: new Column(
        children: [
          new GestureDetector(
            child: new Text('OK'),
            // 4, 5
            onTap: () { Navigator.of(context).pop(true); }
          ),
          new GestureDetector(
            child: new Text('NOT OK'),
            // 4, 5
            onTap: () { Navigator.of(context).pop(false); }
          )
        ])
      );
    }
  ));
  // 6
  var alert = new AlertDialog(
    content: new Text((value != null && value) ? "OK was pressed" : "NOT OK or BACK was pressed"),
    actions: [
      new FlatButton(
        child: new Text('OK'),
        // 7
        onPressed: () { Navigator.of(context).pop(); }
        )
    ],
  );
  // 8
  showDialog(context: context, child: alert);
}

这个方法主要做了一下几个事情:

  1. push 一个新的 MaterialPageRout 到导航栈,这次增加了一个 bool 泛型参数。
  2. 在 push 新路由时,使用 await,这样它会一直等待直到这个路由被 pop 掉。
  3. 你所 push 的这个路由有一个 Column 用于显示两个带有手势检测器的 text widget。
  4. 点击 text widget 会触发 Navigator 将新路由 pop 出栈。
  5. 在调用 pop() 方法时,传递一个返回值,如果用户点击的是 OK 返回 true,否则返回 false。如果用户点击返回按钮,返回值为 null。
  6. 然后创建一个 AlertDialog,显示从路由返回的结果。
  7. 注意 AlertDialog 也必须 pop 出栈。
  8. 调用 showDialog() 显示这个 alert。

上面需要注意的是 MaterialPageRoute 中的类型参数 bool,你可以将它替换成你想从路由中返回的任意类型,同时你需要将返回值在调用 pop 时传入,例如 Navigator.of(context).pop(true)。

修改 MemberState 的 build() 方法,添加一个 RaisedButton 按钮来调用 _showOKScreen():

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(member.login),
    ),
    body: new Padding(
      padding: new EdgeInsets.all(16.0),
      child: new Column(
        children: [
          new Image.network(member.avatarUrl),
          new IconButton(
            icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
            onPressed: () { Navigator.pop(context); }
            ),
          // Add RaisedButton here:
          new RaisedButton(
            child: new Text('PRESS ME'),
            onPressed: () { _showOKScreen(context); }
            )
        ]),
    )
  );
}

这个 RaiseButton 用于显示新屏幕。

按 F5 开始 build & run。点击 PRESS ME 按钮,然后点 OK 或者 NOT OK 或者返回按钮。你会从新屏幕中看到用户点击的结果:

Flutter 导航教程_第7张图片

自定义动画

为了给你的 app 的导航添加一点独特的味道,你可以自定义转换动画。你可以扩展 PageRoute 类,也可以用 PageRouteBuilder 之类的类通过回调方式自定义路由。

修改 GHFlutterState 的 _pushMember 方法,push 一个 PageRouteBuilder 入栈:

_pushMember(Member member) {
  // 1
  Navigator.of(context).push(new PageRouteBuilder(
    opaque: true,
    // 2
    transitionDuration: const Duration(milliseconds: 1000),
    // 3
    pageBuilder: (BuildContext context, _, __) {
      return new MemberWidget(member);
    },
    // 4
    transitionsBuilder: (_, Animation animation, __, Widget child) {
      return new FadeTransition(
        opacity: animation,
        child: new RotationTransition(
          turns: new Tween(begin: 0.0, end: 1.0).animate(animation),
          child: child,
        ),
      );
    }
  ));
}

这里,你:

  1. push 一个 PageRouteBuild 入栈。
  2. 用 transitionDuration 指定时长。
  3. 用 pageBuilder 创建 MemberWidget 屏。
  4. 用 transitionsBuilder 属性创建新路由呈现时的渐入和旋转动画。

按 F5 进行 build & run,看一下新动画的样子:

噢,这真让我有点头晕!:]

接下来去哪里?

你可以从本教程头部或底部下载完整项目。

通过访问下列网址,你可以学习更多 Flutter 导航的知识:

  • Flutter 文档中的路由及导航。
  • Navigator API 文档。

在阅读文档时,请尤其注意阅读如何创建命名路由,这样你就可以调用 Navigator 的 pushNamed() 来调用路由了。

请继续关注更多的 Flutter 教程和屏播!

请在论坛或评论中提问,分享你的心得。希望你学得愉快!

Download Materials

你可能感兴趣的:(跨平台开发)