likes
comments
collection
share

Flutter Route (路由) - 原生路由

作者站长头像
站长
· 阅读数 6

前言: Flutter 路由跳转有二种方法:一种是用系统原生的跳转方法。第二种是,用第三方框架...

原生路由

按官方文档原话,我们通常会用“屏”来表示应用的不同页面(界面)。比如,某个应用有一“屏”展示商品列表,当用户点击某个商品的图片,会跳到新的一“屏”展示商品的详细信息。

术语: 在 Flutter 中,屏 (screen)页面 (page) 都叫做 路由 (route),在下文中统称为 “路由 (route)”。

在 Android 开发中,Activity 相当于“路由”,在 iOS 开发中,ViewController 相当于“路由”。在 Flutter 中,“路由”也是一个 Widget。怎么样从一个“路由”跳转到新的“路由“呢?

MaterialPageRoute

MaterialPageRoute 继承自 PageRoute 类,PageRoute 类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material 组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

  • 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
  • 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • builder 是一个 WidgetBuilder 类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个 widget。我们通常要实现此回调,返回新路由的实例。
  • settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
  • maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置 maintainState 为 false。
  • fullscreenDialog 表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果 fullscreenDialogtrue,新页面将会从屏幕底部滑入(而不是水平方向)。

Navigator

Navigator 是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator 通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator 提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:

Future push(BuildContext context, Route route)

将给定的路由入栈(即打开新的页面),返回值是一个 Future 对象,用以接收新路由出栈(即关闭)时的返回数据。

bool pop(BuildContext context, [ result ])

将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。

Navigator 还有很多其它方法,如 Navigator.replaceNavigator.popUntil 等,详情请参考 API 文档或 SDK 源码注释,在此不再赘述。下面我们还需要介绍一下路由相关的另一个概念“命名路由”。

实例方法

Navigator 类中第一个参数为 context 的静态方法都对应一个 Navigator 的实例方法, 比如 Navigator.push(BuildContext context, Route route) 等价于 Navigator.of(context).push(Route route) ,下面命名路由相关的方法也是一样的。

导航到一个新页面和返回

  1. 创建两个路由

    首先,我们来创建两个路由。这是个最简单的例子,每个路由只包含一个按钮。点击第一个路由上的按钮会跳转到第二个路由,点击第二个路由上的按钮,会回退到第一个路由。

    class FirstRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('First Route'),
          ),
          body: Center(
            child: RaisedButton(
              child: Text('Open route'),
              onPressed: () {
                // Navigate to second route when tapped.
              },
            ),
          ),
        );
      }
    }
    
    class SecondRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Second Route"),
          ),
          body: Center(
            child: RaisedButton(
              onPressed: () {
                // Navigate back to first route when tapped.
              },
              child: Text('Go back!'),
            ),
          ),
        );
      }
    }
    
  2. 用 Navigator.push() 跳转到第二个路由

    使用 Navigator.push() 方法跳转到新的路由, push() 方法会添加一个 Route 对象到导航器的堆栈上。那么这个 Route 对象是从哪里来的呢?你可以自己实现一个,或者直接使用 MaterialPageRoute 类。使用 MaterialPageRoute 是非常方便的,框架已经为我们实现了和平台原生类似的切换动画。

    FirstRoute widget 的 build() 方法中,我们来修改 onPressed() 回调函数:

    // 位于 FirstRoute widget (Within the `FirstRoute` widget)
    onPressed: () {
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondRoute()),
      );
    }
    
  3. 用 Navigator.pop() 回退到第一个路由

    怎么关闭第二个路由回退到第一个呢? 使用 Navigator.pop() 方法, pop() 方法会从导航器堆栈上移除 Route 对象。

    我们来修改 SecondRoute widget 的 onPressed() 回调函数,实现返回第一个路由的功能:

    // 位于 SecondRoute widget (Within the SecondRoute widget)
    onPressed: () {
      Navigator.pop(context);
    }
    

传递数据到新页面

在开发的过程中,我们经常需要在跳转到新页面的时候,能同时传递一些数据。比如,传递用户点击的元素信息。

  1. 传递的模型数据类

    class Todo {
      final String title;
      final String description;
    
      Todo(this.title, this.description);
    }
    
  2. 跳转展示的新页面

    通过定义变量 Todo 来接收数据

    class DetailScreen extends StatelessWidget {
      // 声明一个成员变量来保存 Todo 对象
      final Todo todo;
    
      // 构造函数需要 Todo 对象
      DetailScreen({Key key, @required this.todo}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // 使用 Todo 对象构建 UI
        return Scaffold(
          appBar: AppBar(
            title: Text(todo.title),
          ),
          body: Padding(
            padding: EdgeInsets.all(16.0),
            child: Text(todo.description),
          ),
        );
      }
    }
    
  3. 原页面

    Navigator.push 跳转到新页面,并传入 Todo 模型。

    void main() {
      runApp(MaterialApp(
        title: 'Passing Data',
        home: TodosScreen(
          todos: List.generate(
            20,
            (i) => Todo(
              'Todo $i',
              'A description of what needs to be done for Todo $i',
            ),
          ),
        ),
      ));
    }
    
    class TodosScreen extends StatelessWidget {
      final List<Todo> todos;
    
      TodosScreen({Key key, @required this.todos}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Todos'),
          ),
          body: ListView.builder(
            itemCount: todos.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(todos[index].title),
                onTap: () {
         					// 当用户点击列表时,导航到 DetailScreen, 并当前的 todo 模型传递给它
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => DetailScreen(todo: todos[index]),
                    ),
                  );
                },
              );
            },
          ),
        );
      }
    }
    

或者使用 RouteSettings 传递参数

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

void main() {
  runApp(MaterialApp(
    title: 'Passing Data',
    home: TodosScreen(
      todos: List.generate(
        20,
        (i) => Todo(
          'Todo $i',
          'A description of what needs to be done for Todo $i',
        ),
      ),
    ),
  ));
}

class TodosScreen extends StatelessWidget {
  final List<Todo> todos;

  TodosScreen({Key key, @required this.todos}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(todos[index].title),
            onTap: () {
              // 当用户点击列表时,导航到 DetailScreen, 并当前的 todo 模型传递给它
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailScreen(),
                  // 作为路由的一部分传递参数。DetailScreen 从这些设置中读取参数。
                  settings: RouteSettings(
                    arguments: todos[index],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 取得路由传递参数
    final Todo todo = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),se
      ),
    );
  }
}

从一个页面回传数据

在某些场景下,我们需要在回退到上一屏时同时返回一些数据。比如,我们跳转到新的一屏,有两个选项让用户选择,当用户点击某个选项后会返回到第一屏,同时在第一屏可以知道用户选择的信息。可以使用 Navigator.pop()来进行。

First 页

Navigator.push 跳转时,用 await 接收回传的数据,并可以使用 setState 做 UI 更新

import 'package:defensor/views/home/second.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class First extends StatefulWidget {
  First({Key key}) : super(key: key);

  @override
  _FirstState createState() => _FirstState();
}

class _FirstState extends State<First> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: Container(
        child: Center(
            child: CupertinoButton(
                child: Text('跳转'),
                onPressed: () async {
                    var restult = await Navigator.push(context, MaterialPageRoute(builder: (context) => Second()));
                    // restult 就是回传的数据
                    print(restult);
                })),
      ),
    );
  }
}

Second 页

使用 Navigator.pop(context, data) 回传数据,回传放在第二个参数.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class Second extends StatefulWidget {
  Second({Key key}) : super(key: key);

  @override
  _SecondState createState() => _SecondState();
}

class _SecondState extends State<Second> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: Container(
        child: Center(
            child: CupertinoButton(
                child: Text('返回并传数据'),
                onPressed: () {
                      // data 传回
                      var data = '我是传回的数据';
                      Navigator.pop(context, data);
                })),
      ),
    );
  }
}

导航到对应名称的 routes (路由表) 里

导航到一个新页面和返回 一节中,我们通过创建一个新的路由并将它推到 Navigator类中学习到了如何导航到新的一个界面 (screen)。

然而,如果我们需要在应用的很多地方导航到同一界面,这样做就会导致代码重复。在这种情况下,定义 命名路由 (named route) 并使用它进行导航就会非常方便。

要使用命名路由,我们可以使用 Navigator.pushNamed() 方法。下面的例子展示如何使用 “命名路由” 来实现前一节中的功能。

创建两个界面

class FirstScreen extends StatelessWidget {
  /// 路由名
  static const routeName = '/';
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen when tapped.
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  /// 路由名
  static const routeName = '/second';
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first screen when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

定义路由

我们需要通过为 MaterialApp的构造函数额外的属性: initialRouteroutes 来定义我们的路由

initialRoute 属性定义了应用应该从哪个路由启动。 routes 属性定义了所有可用的命名路由,以及当我们跳转到这些路由时应该构建的 widgets。

MaterialApp(
  // 使用“/”命名路由来启动应用
  // 在这里,应用将从 FirstScreen Widget 启动
  initialRoute: '/',
  routes: {
    // 当我们跳转到“/”时,构建 FirstScreen Widget
    FirstScreen.routeName: (context) => FirstScreen(),
    // 当我们跳转到“/second”时,构建 SecondScreen Widget
    SecondScreen.routeName: (context) => SecondScreen(),
  },
);

请注意

当使用 initialRoute 时,需要确保你没有同时定义 home 属性。

跳转到第二个界面

准备好了 Widgets 和路由,我们就可以开始进行页面跳转。在这里,我们将使用 Navigator.pushNamed()函数。它会告诉 Flutter 去构建我们在 routes 表中定义的 widget 并启动该界面。

FirstScreen widget 的 build() 方法中,我们将更新 onPressed() 回调:

// 在 `FirstScreen` Widget中
onPressed: () {
  // 使用命名路由跳转到第二个界面
  Navigator.pushNamed(context, SecondScreen.routeName);
}

返回到第一个界面

为了能够跳转回第一个页面,我们可以使用 Navigator.pop() 方法。

// 在 SecondScreen Widget 中
onPressed: () {
  // 通过从堆栈弹出当前路由
  // 来返回到第一个界面
  Navigator.pop(context);
}

参考资料

Flutter 官方中文网 (flutter.cn/docs/get-st…)

Flutter 实战 (book.flutterchina.club/)