了解Flutter的导航和路由

475 阅读7分钟

Flutter已经成为构建跨平台应用程序的流行工具包。它支持所有主要平台,包括安卓、iOS和网络。

导航对于任何应用程序都是非常重要的。它为各种平台提供的导航API提供了一个统一的抽象。Flutter为导航提供了两种类型的API:命令式和声明式。

在本教程中,我们将介绍Flutter 1.0中使用的命令式导航方法,以及Flutter 2.0中使用的声明式方法。

我们将讨论以下内容。

强制导航(Flutter 1.0

Flutter 1.0 对导航采取了一种强制性的方法。在Flutter中,导航由一个小部件的堆栈组成,其中小部件被推到上面,也从上面弹出。

FlutterNavigator

Navigator 类提供了 Flutter 应用程序中的所有导航功能。

Navigator 提供了通过向堆栈推送或从堆栈中弹出的方法来突变堆栈。Navigator.push 方法用于导航到一个较新的页面,Navigator.pop 用于从当前页面返回。

下面是一个关于poppush 的基本例子:push 方法把BuildContext 作为第一个参数,第二个参数是PageBuilder 。这个例子使用了MaterialPageRoute ,它提供了过渡动画并处理路线的变化。

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'My App',
    home: Main(),
  ));
}

class Main extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Route'),
      ),
      body: Center(
        child:RaisedButton(
          child: Text('Open route'),
          onPressed: () {
// pushing SecondRoute
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}

pop 方法只接受BuildContext ,并改变当前路线。

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
// Removing SecondRoute
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

Navigator 提供了更多的方法,包括*pushReplacement* ,它的参数与push 类似。它将替换当前的路由,所以不可能导航回旧的路由。

例如,在成功登录后,你想使用*pushReplacement* ,以防止用户返回到登录界面。

命名的路由

命名路由允许你通过使用字符串来改变路径,而不是提供组件类,这反过来又使你能够重复使用代码。

命名路由在MaterialApp 上被定义为一个地图。这些路由可以从应用程序的任何部分使用。

定义路由

路由是一个带有字符串键和值的地图,例如传递给routes 属性的建设者,在MaterialApp

void main() {
  runApp(MaterialApp(
    title: 'My App',
    home: Main(),
// Routes defined here
    routes: {
      "second":(context)=>SecondRoute()
    },
  ));
}

使用命名的路由

代替pushpushNamed ,用来改变到一个新的路由。类似地,*pushReplacementNamed* ,而不是pushReplacementpop 方法对所有的路由都是一样的。

class Main extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Route'),
      ),
      body: Center(
        child:RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.pushReplacementNamed(context, "second");
          },
        ),
      ),
    );
  }
}

声明式导航(Flutter 2.0

Flutter 2.0带有改进的导航,这在很大程度上得益于它对声明式方法的支持。这使得路由成为状态的一个函数--也就是说,页面在状态变化时发生变化。Flutter 2.0还对网络上的导航有更好的支持。

Flutter团队为宣布Flutter导航2.0和Router而公开分享的这张图,很好地描述了这一流程。

Flutter 2.0 Navigation Depicted As A Diagram

Flutter导航器

导航器接收一个页面列表,并显示最后一个页面。你可以通过添加或删除列表末尾的页面来改变其页面。

下面的例子演示了如何使用Navigator类与新的Flutter Navigator使用基于页面的导航。

_page 是由这个类管理的状态。对于导航来说,这个_page 是在setState 调用中操作的。

class _App extends State {
// Creating state for pages
  List<Page> _pages=[];

_page 被传递给Navigator类。导航器将根据_page 的值来改变当前页面。

当进行基于操作系统的导航时,如在Android上按下后退按钮等,onPopPage

   @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        onPopPage: (route,result){
// check if route removed
          if(route.didPop(result)){
// remove the last page
            _pages.removeLast();
            return true;
          }
          return false;
        },
        pages: _pages,
      ),
    );
  }
}

初始页面可以通过在initState 生命周期方法中添加一个页面来设置。

  @override
  void initState() {
    super.initState();
// setting intial page
    _pages=[_buildMain()];
  }

要创建一个新的材料页,请使用MaterialPage widget。MaterialPage 需要一个孩子和一个键。导航器使用key 来区分页面并检测页面变化。

在按下click 按钮时,一个新的页面被添加到_page 状态。setState 被调用以触发小组件的重建,Navigator 自动处理页面变化。

// This function creates a Page using MaterialPage  
Page _buildMain(){
    return MaterialPage(child: Scaffold(
      body: Center(
        child: ElevatedButton(
          child: Text("click"),
          onPressed: (){
// When clicked add a new Page to _page list
            _pages.add(_buildSecondPage());
// call setState to trigger rebuild for Widget
            setState(() {
// create a copy of array
              _pages=_pages.toList();
            });
          },
        ),
      ),
// This helps Navigator to distigush between different pages
    ),key: ValueKey("home"));
  }

这个页面的构建方式与_buildMain 相同,但它不是添加一个新的页面,而是删除一个并触发重建。

// This function perform same task as _buildMain  
Page _buildSecondPage(){
    return MaterialPage(child: Scaffold(
      body: Center(
        child: ElevatedButton(
          child: Text("back"),
          onPressed: (){
// This will take back to main
// remove the last page
            _pages.removeLast();
// call setState to trigger a rebuild
            setState(() {
// creating a copy of list
              _pages=_pages.toList();
            });
          },
        ),
      ),
    ),key: ValueKey("second"));
  }

除了使用_pages 列表作为状态外,你还可以使用任何其他状态来执行导航。下面是另一个例子。

class _App extends State {
  String _selected="main";

  Page _buildMain(){
    return MaterialPage(child: Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          child: Text("click"),
          onPressed: (){
            setState(() {
// add a new page
              _selected="second";
            });
          },
        ),
      ),
    ),key: ValueKey("home"));
  }

  Page _buildSecondPage(){
    return MaterialPage(child: Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          child: Text("back"),
          onPressed: (){
            setState(() {
// change back state to main
             _selected="main";
            });
          },
        ),
      ),
    ),key: ValueKey("second"));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        onPopPage: (route,result){
          if(route.didPop(result)){
            _selected="main";
            return true;
          }
          return false;
        },
        pages: [
           _buildMain(),
// only show select if state has second selected
          if (_selected=="second") _buildSecondPage()
        ],
      ),
    );
  }
}

使用RouterDelegate

RouterDelegate 是一个由Router 使用的核心部件。它对引擎的路由推送和路由弹出的意图做出响应。新的导航允许创建RouterDelegate ,以便更好地控制导航。

一个RouterDelegate 是通过扩展RouterDelegate<AppRouteState> 类与PopNavigatorRouterDelegateMixin,ChangeNotifier 混合来创建的。

_selected 追踪当前的路线。这与前面例子中使用的状态类似。

class AppRouter extends RouterDelegate<AppRouteState> with PopNavigatorRouterDelegateMixin,ChangeNotifier {
  String _selected="main";

这被路由器用来获取路由器的最新状态并改变地址栏中的URL。

// get correct state of router  
@override
  AppRouteState get currentConfiguration => AppRouteState(_selected);

导航键是用来支持旧的导航。

// This for older navigation support. 
 final _navigation= GlobalKey<NavigatorState>();
  @override
  GlobalKey<NavigatorState> get navigatorKey => _navigation;

notifyListeners 被用来代替setState 来触发重建。_selected 被改变来改变路由。

  Page _buildMain(){
    return MaterialPage(child: Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          child: Text("click"),
          onPressed: (){
            _selected="second";
// notify route changes
           notifyListeners();
          },
        ),
      ),
    ),key: ValueKey("home"));
  }

这类似于_buildMain

  Page _buildSecondPage(){
    return MaterialPage(child: Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ElevatedButton(
          child: Text("back"),
          onPressed: (){
              _selected="main";
// notify route changes
          notifyListeners();
          },
        ),
      ),
    ),key: ValueKey("second"));
  }

build 函数返回导航器部件,该部件用于布置其他页面。这个函数类似于前面函数中的build 。代替setStatenotifyListeners ,用于触发重建。

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        key: _navigation,
        onPopPage: (route,result){
          if(!route.didPop(result)){
            return false;
          }
          _selected="main";
// notify route changes
          notifyListeners();
          return true;

        },
        pages: [
          _buildMain(),
// if Route is second show SecondPage
          if (_selected=="second") _buildSecondPage()
        ],
      ),
    );
  }

这个函数使用路由器传递的信息来改变路由。当引擎传递路由推送或弹出的意图时,这个函数被调用来改变路由。这里传递的信息是由一个不同的类来解析的,我们将在后面讨论。

  @override
  Future<void> setNewRoutePath(configuration) async {
// update page based on 
    _selected=configuration.selected;
  }
}

RouteInformationParser

setNewRoutePath 从路由器接收配置。这个配置是由RouteInformationParser 解析的。

对于操作系统、引擎等传递的解析状态,应该有一个类来扩展RouteInformationParserrestoreRouteInformationcurrentConfiguration 中获取返回值并将其转换为RouteInformation

parseRouteInformation 返回路由器状态,并将其传递给setNewRoutePath

class AppRouteInformationParser extends RouteInformationParser<AppRouteState>{
  // This converts route state to route information.
  @override
  RouteInformation restoreRouteInformation(configuration) {
    if(configuration.selected=="main") {
      return RouteInformation(location:"/main");
    } else {
      return RouteInformation(location: "/second");
    }

  }

// This converts route info to router state
  @override
  Future<AppRouteState> parseRouteInformation(RouteInformation routeInformation)async{
    var url=Uri.parse(routeInformation.location);
    print(url.path);
    if (url.path == "/") return AppRouteState("main");
    return AppRouteState(url.path.replaceAll("/", ""));
  }

}

把它放在一起

MaterialApp 现在有一个新命名的构造函数,它实现了一个以DelegateInformationParser 为参数的路由器。

class _App extends State {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(routeInformationParser: AppRouteInformationParser(), routerDelegate: AppRouter());
  }
}

总结

在这个Flutter导航教程中,我们向您介绍了如何根据Flutter 1.0中使用的命令式方法和Flutter 2.0中引入的新的声明式导航在Flutter应用中实现导航。

根据您的Flutter项目的性质,这两种类型的导航都可能是合适的,但都不是一蹴而就的。您应该始终选择最适合您需求的方法,即使这意味着采用两者的组合。

要开始使用Flutter中的导航,我推荐你查看FluroVoyager包。

The postUnderstanding Flutter navigation and routingappeared first onLogRocket Blog.