Bootstrap

Flutter 笔记 | Flutter 中的路由、包、资源、异常和调试

路由管理

Flutter中的路由通俗的讲就是页面跳转。在Flutter中通过Navigator组件管理路由导航。并提供了管理堆栈的方法。如:Navigator.pushNavigator.pop

Flutter中给我们提供了两种配置路由跳转的方式:1、基本路由2、命名路由

普通路由使用

比如我们现在想从HomePage组件跳转到SearchPage组件。

1、需要在HomPage中引入SearchPage.dart

import '../SearchPage.dart';

2、在HomePage中通过下面方法跳转

Center(
	child: ElevatedButton( 
		onPressed: () {
   
			Navigator.push(context,
				MaterialPageRoute(builder: (context) {
   
						return const SearchPage();
				}));
		},
		child: const Text("跳转到搜索页面"),
	),
)
MaterialPageRoute

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

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

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

1. Future push(BuildContext context, Route route)

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

2. bool pop(BuildContext context, [ result ])

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

Navigator 还有很多其他方法,如Navigator.replaceNavigator.popUntil等,详情请参考API文档或SDK 源码注释,在此不再赘述。

实例方法

Navigator类中每个第一个参数为context静态方法都对应一个相同功能的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)

普通路由跳转传值

路由跳转时,可以通过组件的构造函数直接传值,比如下面想从HomePage给SearchPage传参数

1、定义一个SearchPage接收传值

import 'package:flutter/material.dart';
class SearchPage extends StatefulWidget {
   
    final String title;
    const SearchPage({
   
        super.key, this.title = "Search Page"
    });
    
    State < SearchPage > createState() => _SearchPageState();
}
class _SearchPageState extends State < SearchPage > {
   
    
    Widget build(BuildContext context) {
   
        return Scaffold(
            appBar: AppBar(
                title: Text(widget.title),
                centerTitle: true,
            ),
            body: const Center(
                child: Text("组件居中"),
            ),
        );
    }
}

2、在跳转页面实现传值

Center(
  child: ElevatedButton(
      onPressed: () {
   
        Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
   
              return const SearchPage(title: "我是标题",);
            })
        );
      },
      child: const Text("跳转到搜索页面")
   ),
)

命名路由传值

官方文档:navigate-with-arguments

1、配置onGenerateRoute

import 'package:flutter/material.dart';
import './pages/tabs.dart';
import './pages/search.dart';
import './pages/form.dart';

void main() {
   
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
   
  MyApp({
   Key? key}) : super(key: key);
  
  // 1、配置路由, 定义Map类型的routes, Key为String类型,value为Function类型
  final Map<String, WidgetBuilder> routes = {
   
    '/':(context)=>const Tabs(), 
    '/search':(context,{
   arguments})=> SearchPage(arguments:arguments),
    '/login':(context)=>const LoginPage(), 
  };

  // 2. 固定写法 统一处理
  Route? onGenerateRoute(RouteSettings settings) {
   
    final String? name = settings.name;
    final Function? pageContentBuilder = routes[name];
    if (pageContentBuilder != null) {
   
      if (settings.arguments != null) {
   
        return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
      } else {
   
        return MaterialPageRoute(builder: (context) => pageContentBuilder(context));
      }
    }
    return null;
  }
  
  
  Widget build(BuildContext context) {
   
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue,),
      initialRoute: '/',
      //2、调用onGenerateRoute处理
      onGenerateRoute: onGenerateRoute,
    );
  }
}

2、定义页面接收arguments传参

import 'package:flutter/material.dart';

class SearchPage extends StatefulWidget {
   
  final Map arguments;
  const SearchPage({
   super.key, required this.arguments}); // 构造函数接受参数
  
  
  State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
   
  
  void initState() {
   
    super.initState();
    print(widget.arguments); // 打印接受到的参数
  }
  
  Widget build(BuildContext context) {
   
    return Scaffold(
      appBar: AppBar(
        title: const Text("我是搜索页面"),
      ),
    );
  }
}

3、在跳转页面实现传参

ElevatedButton(
   onPressed: () {
   
     Navigator.pushNamed(context, '/search', arguments: {
   
       "title": "搜索页面",
     });
   },
   child: const Text("打开搜索页面")
)

Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,可以自行查看API文档。

RouteSetting获取路由参数

也可以通过settings.arguments获取路由参数,组件构造函数无需添加额外参数

class EchoRoute extends StatelessWidget {
   

  
  Widget build(BuildContext context) {
   
    //获取路由参数  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略无关代码
  }
}

在打开路由时传递参数:

Navigator.of(context).pushNamed("new_page", arguments: "hi");
路由表

路由表的定义如下:

Map<String, WidgetBuilder> routes;

它是一个Mapkey为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

注册路由表

直接看代码:

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,),
  // home:Tabs(),
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  //注册路由表
  routes:{
   
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  } 
);

可以看到,如果想配置根路由页面,我们只需在路由表routes中注册一下MyHomePage路由,然后将其名字作为MaterialAppinitialRoute属性值即可,该属性决定应用的初始路由页是哪一个命名路由。这样就可以替代默认示例样板中的 home 参数来指定首页。

路由生成钩子

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制,如:

MaterialApp(
  ... //省略无关代码
  onGenerateRoute:(RouteSettings settings){
   
	  return MaterialPageRoute(builder: (context){
   
		   String routeName = settings.name;
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 引导用户登录;其他情况则正常打开路由。
     }
   );
  }
);

这个函数可以用来做页面拦截器、用户权限判断等。

注意,onGenerateRoute 只会对命名路由生效。

在单独文件中统一配置路由表

我们可以把路由表和路由钩子函数统一配置到一个独立的dart文件中,方便管理和使用。

1、新建routers/routers.dart 配置路由

import 'package:flutter/material.dart';

// 1.配置路由
final Map<String, WidgetBuilder> routes = {
   
  '/': (context) => const Tabs(),
  '/form': (context) => const FormPage(),
  '/product': (context) => const ProductPage(),
  '/productinfo': (context, {
   arguments}) => ProductInfoPage(arguments: arguments),
  '/search': (context, {
   arguments}) => SearchPage(arguments: arguments),
  '/login': (context) => const LoginPage(),
  '/registerFirst': (context) => const RegisterFirstPage(),
  '/registerSecond': (context) => const RegisterSecondPage(),
  '/registerThird': (context) => const RegisterThirdPage(),
};

// 2.onGenerateRoute
Route? onGenerateRoute(RouteSettings settings) {
   
  // 统一处理
  final String? name = settings.name;
  final Function? pageContentBuilder = routes[name];
  if (pageContentBuilder != null) {
   
    if (settings.arguments != null) {
   
      return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
    } else {
   
      return MaterialPageRoute(
          builder: (context) => pageContentBuilder(context));
    }
  } else {
   
    // 可以在这里添加全局跳转错误拦截处理页面
    print("路由不存在");
    return null;
  }
}

然后使用的时候就可以这样:

import 'package:flutter/material.dart';
import 'routes/Routes.dart';

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

class MyApp extends StatelessWidget {
   
  const MyApp({
   Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
   
    return const MaterialApp( 
        initialRoute: '/', //初始化的时候加载的路由
        onGenerateRoute: onGenerateRoute,
    );
  }
}

这是使用路由钩子的情况,如果不使用路由钩子,可以这样写:

MaterialApp( 
  // ...
  initialRoute: "/",   
  routes: routes
);

路由返回

Navigator.of(context).pop();

路由返回传值给上一个页面

首先,在启动页面,主要使用await/async来等待要打开的页面的返回结果,这是因为Navigator.pushNamed返回的是一个Future对象。

class RouterTestRoute extends StatelessWidget {
   
  
  Widget build(BuildContext context) {
   
    return Center(
      child: ElevatedButton(
        onPressed: () async {
   
          // 打开`TipRoute`,并等待返回结果
          var result = await Navigator.pushNamed(context, "tip_page", arguments: "初始参数");
          var result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
   
                return TipRoute(text: "我是提示xxxx"); // 路由参数
              },
            ),
          ); 
          print("路由返回结果: $result");
        },
        child: Text("打开提示页"),
      ),
    );
  }
}

// MaterialApp 配置
MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,), 
  initialRoute:"/",  
  routes:{
    
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), 
   "tip_page": (context) =>
            TipRoute(title: '${
     ModalRoute.of(context)?.settings.arguments}'),
  } 
);        

然后,在打开的路由页面中使用 Navigator.pop(context, result) 来返回值。

class TipRoute extends StatelessWidget {
   
  final String title;

  const TipRoute({
   Key? key, required this.title}) : super(key: key);

  
  Widget build(BuildContext context) {
   
    return Scaffold(
      appBar: AppBar(
        title: const Text("提示"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(title),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),
                child: const Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

替换路由

比如我们从用户中心页面跳转到了registerFirst页面,然后从registerFirst页面通过
pushReplacementNamed跳转到了registerSecond页面。这个时候当我们点击registerSecond的返回按钮的时候它会直接返回到用户中心。

Navigator.of(context).pushReplacementNamed('/registerSecond');

返回根路由

比如我们从用户中心跳转到registerFirst页面,然后从registerFirst页面跳转到registerSecond页面,然后从registerSecond跳转到了registerThird页面。这个时候我们想的是registerThird注册成功后返回到用户中心。 这个时候就用到了返回到根路由的方法。

Navigator.of(context).pushAndRemoveUntil(
        MaterialPageRoute(builder: (BuildContext context) {
   
          return const Tabs();
        }), (route) => false);

在这里插入图片描述

Android 和Ios使用同样风格的路由跳转

Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换 , CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件如果在Android上也想使用左右切换风格,可以使用CupertinoPageRoute。

1、routers.dart中引入cupertino.dart

import 'package:flutter/cupertino.dart';

2、MaterialPageRoute改为CupertinoPageRoute

import 'package:flutter/cupertino.dart';
import '../pages/tabs.dart';
import '../pages/shop.dart';
import '../pages/user/login.dart';
import '../pages/user/registerFirst.dart';
import '../pages/user/registerSecond.dart'
;