Flutter入门:路由、包和资源

Posted by alonealice on 2021-01-05

路由

路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter中的路由管理和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

距离:

1
2
3
4
5
6
7
8
9
10
11
12
13
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("New route"),
),
body: Center(
child: Text("This is new route"),
),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
FlatButton(
child: Text("open new route"),
textColor: Colors.blue,
onPressed: () {
//导航到新路由
Navigator.push( context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}));
},
),

MaterialPageRoute

MaterialPageRoute可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

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

Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。

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

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

Navigator 还有很多其它方法,如Navigator.replaceNavigator.popUntil

Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return Center(
child: RaisedButton(
onPressed: () async {
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "我是提示xxxx",
);
},
),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");
},
child: Text("打开提示页"),
),
);
1
2
3
4
5
TipRoute({
Key key,
@required this.text, // 接收一个text参数
}) : super(key: key);
final String text;

TipRoute会传入一个text值。

命名路由

所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。

注册路由表

路由表的注册方式很简单,然后在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
MaterialApp(
title: 'Flutter Demo',
initialRoute:"/", //名为"/"的路由作为应用的home(首页)
theme: ThemeData(
primarySwatch: Colors.blue,
),
//注册路由表
routes:{
"new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);

注册首页只需在路由表中注册一下首页路由,然后将其名字作为MaterialAppinitialRoute属性值即可,该属性决定应用的初始路由页是哪一个命名路由。

通过路由名打开新路由页

要通过路由名称来打开新路由,可以使用NavigatorpushNamed方法:

1
Future pushNamed(BuildContext context, String routeName,{Object arguments})
1
2
3
onPressed: () {
Navigator.pushNamed(context, "new_page");
},
命名路由参数传递

在路由页通过RouteSetting对象获取路由参数:

1
2
3
4
5
6
7
8
9
class EchoRoute extends StatelessWidget {

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

在打开路由时传递参数

1
Navigator.of(context).pushNamed("new_page", arguments: "hi");
适配

如果路由注册时需要参数:

1
2
3
4
5
6
7
8
MaterialApp(
... //省略无关代码
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context).settings.arguments);
},
},
);
路由生成钩子

Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

1
Route<dynamic> Function(RouteSettings settings)

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

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

这种方式可以在启动每个路由时都统一处理操作。

包管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: flutter_in_action
description: First Flutter application.

version: 1.0.0+1

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2

dev_dependencies:
flutter_test:
sdk: flutter

flutter:
uses-material-design: true

各个字段的意义:

  • name:应用或包名称。
  • description: 应用或包的描述、简介。
  • version:应用或包的版本号。
  • dependencies:应用或包依赖的其它包或插件。
  • dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
  • flutter:flutter相关的配置选项。

Pub仓库

Pub是Google官方的Dart Packages仓库

添加到依赖项列表:

1
2
3
4
5
6
7
dependencies:
flutter:
sdk: flutter

cupertino_icons: ^0.1.0
# 新添加的依赖
english_words: ^3.1.3

添加完之后一般需要下载:单击右上角的 Packages get

引入包:

1
import 'package:english_words/english_words.dart';

使用包:

1
final wordPair = new WordPair.random();

其它依赖方式

依赖本地包
1
2
3
dependencies:
pkg1:
path: ../../code/pkg1

路径可以是相对的,也可以是绝对的。

依赖Git
1
2
3
4
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git

上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:

1
2
3
4
5
dependencies:
package1:
git:
url: git://github.com/flutter/packages.git
path: packages/package1

资源管理

Flutter APP安装包中会包含代码和 assets(资源)两部分。Assets是会打包到程序安装包中的,可在运行时访问。常见类型的assets包括静态数据(例如JSON文件)、配置文件、图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)等。

指定 assets

Flutter也使用pubspec.yaml文件来管理应用程序所需的资源,举个例子:

1
2
3
4
flutter:
assets:
- assets/my_icon.png
- assets/background.png

assets指定应包含在应用程序中的文件,asset的实际目录可以是任意文件夹

Asset 变体

pubspec.yaml的assets部分中指定asset路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的asset一起被包含在asset bundle中。

例如,如果应用程序目录中有以下文件:

  • …/pubspec.yaml
  • …/graphics/my_icon.png
  • …/graphics/background.png
  • …/graphics/dark/background.png
  • …etc.

然后pubspec.yaml文件中只需包含:

1
2
3
flutter:
assets:
- graphics/background.png

那么这两个graphics/background.pnggraphics/dark/background.png 都将包含在您的asset bundle中。前者被认为是main asset (主资源),后者被认为是一种变体(variant)。在选择匹配当前设备分辨率的图片时,Flutter会使用到asset变体。

加载 assets

加载文本assets
  • 通过rootBundle 对象加载:每个Flutter应用程序都有一个rootBundle对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。
  • 通过 DefaultAssetBundle 加载:建议使用 DefaultAssetBundle 来获取当前BuildContext的AssetBundle。 这种方法不是使用应用程序构建的默认asset bundle,而是使父级widget在运行时动态替换的不同的AssetBundle,这对于本地化或测试场景很有用。

通常,可以使用DefaultAssetBundle.of()在应用运行时来间接加载asset(例如JSON文件),而在widget上下文之外,或其它AssetBundle句柄不可用时,可以使用rootBundle直接加载这些asset,例如:

1
2
3
4
5
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
加载图片

AssetImage 可以将asset的请求逻辑映射到最接近当前设备像素比例(dpi)的asset。为了使这种映射起作用,必须根据特定的目录结构来保存asset:

  • …/image.png
  • …/Mx/image.png
  • …/Nx/image.png
  • …etc.

其中M和N是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。

主资源默认对应于1.0倍的分辨率图片。看一个例子:

  • …/my_icon.png
  • …/2.0x/my_icon.png
  • …/3.0x/my_icon.png

1.8的设备上,会选择2.0x,2.7的设备上会选择3.0x

加载示例:

1
2
3
4
5
6
7
8
9
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('graphics/background.png'),
),
),
);
}

可以使用Image.asset()方法:

1
2
3
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}

依赖包中的资源图片

要加载依赖包中的图像,必须给AssetImage提供package参数。

例如,假设您的应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:

  • …/pubspec.yaml
  • …/icons/heart.png
  • …/icons/1.5x/heart.png
  • …/icons/2.0x/heart.png
  • …etc.
1
new AssetImage('icons/heart.png', package: 'my_icons')

1
new Image.asset('icons/heart.png', package: 'my_icons')

打包包中的 assets

如果在pubspec.yaml文件中声明了期望的资源,它将会打包到相应的package中。特别是,包本身使用的资源必须在pubspec.yaml中指定。

包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml中指定包含哪些图像。 例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:

  • …/lib/backgrounds/background1.png
  • …/lib/backgrounds/background2.png
  • …/lib/backgrounds/background3.png

要包含第一张图像,必须在pubspec.yaml的assets部分中声明它:

1
2
3
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png

特定平台 assets

设置APP图标

更新Flutter应用程序启动图标的方式与在本机Android或iOS应用程序中更新启动图标的方式相同。

更新启动页

与在本机Android或iOS应用程序中更新启动图标的方式相同。