目录
1. 路由管理(页面跳转)
2. 资源管理
1. 路由管理(页面跳转)
路由Route
在移动开发中通常指页面(iOS的UIViewController、Android的Activity)。
导航器Navigator(路由管理)
管理路由之间如何跳转。
维护一个路由栈,路由入栈操作(push)会打开新页面,路由出栈操作(pop)会关闭页面,路由管理主要是指如何管理路由栈。
/*
嵌套导航器
比如Tabbar,每个Tab内都有一个独立的导航器。
应用里的所有导航器为树状结构,有一个根导航器,同一节点下的各兄弟并行导航。
*/
1. 跳转到下一页面(Navigator.push)
// push方法返回值类型为Future。
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new HelloWidget()), // 需要一个Route
);
等价(第一个参数是context都可以这样等价替换)
Navigator.of(context).push(
new MaterialPageRoute(builder: (context) => new HelloWidget()),
);
2. 返回(Navigator.pop)
返回到上一页面
Navigator.pop(context);
返回到根页面
Navigator.of(context,rootNavigator:true).pop();
示例(在模版计数器示例中,做如下修改)
1. 创建一个新路由
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("New route"),
),
body: new Center(
child: new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: new Text('Go back!'),
),
),
);
}
}
2. 在_MyHomePageState.build方法中添加一个按钮:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
... // 省略无关代码
FlatButton(
child: Text("open new route"),
textColor: Colors.blue,
onPressed: () {
// 导航到新路由
Navigator.push( context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}));
},
),
],
)
- Navigator导航器
通过栈来管理路由,提供了打开和退出路由的方法。
通常当前屏幕显示的页面是栈顶路由。
1. Future push(BuildContext context, Route route)
将指定路由入栈(打开新页面)。
返回值是一个Future对象,用以接收新路由出栈(页面关闭)时的返回数据。
2. bool pop(BuildContext context, [result])
将栈顶路由出栈(关闭当前页)。
result为页面关闭时返回给上一页面的数据。
3. 其他方法
Navigator.replace、Navigator.popUntil
- MaterialPageRoute (继承自PageRoute抽象类)
MaterialPageRoute({
// 构建路由页,返回值是一个widget。
WidgetBuilder builder,
// 路由页的配置信息(路由页名称、是否首页)
RouteSettings settings,
// 设置为false后,当入栈一个新路由后会释放内存中原来的路由页。
bool maintainState = true,
// 是否全屏
// 在iOS中,如果为true新页面会从屏幕底部滑入(而不是水平方向)。
bool fullscreenDialog = false,
})
/*
Android当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
iOS当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
可以针对不同平台,实现动画风格一致的路由切换动画。
*/
PageRoute类
占有整个屏幕空间的一个模态路由页面,定义了路由构建及切换时过渡动画的相关接口及属性。
如果想自定义路由切换动画,可以继承PageRoute来实现。
- 路由传值(跳转时通常需要传递数据)
例如:打开商品详情页时需要一个商品id展示对应商品详情;填写订单时需要选择收货地址,打开地址选择页并选择地址后,将所选地址返回给订单页。
示例(双向传递数据)
class RouterTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
onPressed: () async {
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由参数
text: "hello world",
);
},
),
);
// 点击返回按钮,命令行输出 路由返回值: 我是返回值。点击箭头,命令行输出 路由返回值: null。
print("路由返回值: $result");
},
child: Text("打开提示页"),
),
);
}
}
// 接受一个文本参数用来展示。点击“返回”按钮后返回上一个路由时 传递了文本数据。
class TipRoute extends StatelessWidget {
TipRoute({this.text});
final String text;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("提示"),
),
body: Padding(
padding: EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(text),
RaisedButton(
onPressed: () => Navigator.pop(context, "我是返回值"),
child: Text("返回"),
)
],
),
),
),
);
}
}
- 命名路由(有名字的路由)
给路由起名字后,可以通过路由名来打开新路由。
通过路由名打开新路由时,应用会根据路由名在【路由表】中查找并调用对应的WidgetBuilder回调函数生成并返回路由。
路由表的定义如下:
// MaterialApp的一个属性。
// key为路由名,value是个builder回调函数(用于生成并返回相应的路由)。
Map<String, WidgetBuilder> routes;
好处:
1. 语义化更明确。
2. 维护更方便。
如果使用匿名路由:不仅需要import新路由页的dart文件,而且代码非常分散不方便管理。还需要知道页面的构建方式,且变化后需要涉及该页面的跳转都需要修改,代码耦合性严重。
3. 可以通过onGenerateRoute(路由拦截器)做一些全局的路由跳转前的处理逻辑。
使用:
1. 首先注册一个路由表,提供对应关系(路由名--->路由)
在MaterialApp中添加routes属性:
MaterialApp(
title: ProjectConfig.packageInfo.appName,
theme: ProjectTheme.theme,
routes: {
'/':(context)=>BootstrapPage(),
'/login':(context)=>LoginPage(),
'/register':(context)=>RegisterPage(),
'/tab':(context)=>TabPage(),
},
// navigatorKey: // GlobalKey<NavigatorState>对象,全局存储导航的状态,
),
2. 通过路由名打开新路由页
// 第三个参数可选,用于传递值。
// 在导航栏上会显示返回按钮
Navigator.pushNamed(context, "/new_page");
3. 在新路由页可以
使用替换路由打开新页面:
// 点击返回按钮或者pop后会返回到“上上页”,即调用Navigator.pushNamed的页面
Navigator.pushReplacementNamed(context, "/new_page");
返回到上一页面:
Navigator.pop(context);
返回根路由:
Navigator.of(context,rootNavigator:true).pop();
返回根路由:
Navigator.pushAndRemoveUntil(
context,
new MaterialPageRoute(builder: (context)=>new HomeRoute()),
(route)=>route==null
);
4. 路由生成钩子(onGenerateRoute)
1. MaterialApp的一个属性,只会对命名路由生效。
2. 可用来统一处理跳转逻辑(参数传递、无效路由、权限访问)。
3. 当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。
5. 命名路由参数传递(早期版本并不支持)
1. 注册路由表
routes:{
"new_page":(context) => NewRoute(),
} ,
2. 传递值
Navigator.of(context).pushNamed("new_page", arguments: "hi");
3. 获取值
方式1:(NewRoute的build方法中)
Widget build(BuildContext context) {
// 获取路由参数
var args=ModalRoute.of(context).settings.arguments;
//...省略无关代码
}
方式2:
如果NewRoute接受一个text 参数,在不改变NewRoute源码的前提下,使用路由名来打开它需要:
routes: {
"new_page": (context){
return TipRoute(text: ModalRoute.of(context).settings.arguments);
},
},
/*
看一下pushNamed方法和pop方法的实现:
Future<T?> pushNamed<T extends Object?>(
String routeName, {
Object? arguments,
}) {
return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
}
void pop<T extends Object?>([ T? result ]) {
...
}
*/
示例(创建了一个路由管理类来集中管理Route。使用onGenerateRoute来统一处理跳转)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../app.dart';
import '../login.dart';
import '../not_found.dart';
import '../splash.dart';
class RouterTable {
static String splashPath = 'splash';
static String loginPath = 'login';
static String homePath = '/';
static String notFoundPath = '404';
static Map<String, WidgetBuilder> routeTables = {
// 404页面
notFoundPath: (context) => NotFound(),
// 启动页
splashPath: (context) => Splash(),
// 登录
loginPath: (context) => LoginPage(),
// 首页
homePath: (context) => AppHomePage(),
};
// 路由拦截
static Route onGenerateRoute<T extends Object>(RouteSettings settings) {
return CupertinoPageRoute<T>(
settings: settings,
builder: (context) {
String name = settings.name;
if (routeTables[name] == null) {
name = notFoundPath;
}
Widget widget = routeTables[name](context);
return widget;
},
);
}
}
====================
使用
import 'package:flutter/material.dart';
import 'routers/router_table.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final GlobalKey navigationKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigationKey,
onGenerateRoute: RouterTable.onGenerateRoute,
initialRoute: RouterTable.splashPath,
);
}
}
示例(routes注册路由表、initialRoute首页)
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 注册路由表
routes:{
"new_page":(context) => NewRoute(),
... // 省略其它路由注册信息
} ,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
修改FlatButton的onPressed,修改跳转逻辑为:
onPressed: () {
Navigator.pushNamed(context, "/new_page");
//Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return NewRoute();
//}));
},
/*
也可以将首页注册为命名路由
MaterialApp(
title: 'Flutter Demo',
// 设置初始路由:将名为"/"的路由作为应用的首页(默认就是‘/’,不用再次赋值)。
// 设置initialRoute后不再需要设置home。
initialRoute:"/",
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 注册路由表
routes:{
"/new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
}
);
*/
示例(onGenerateRoute统一设置权限)
MaterialApp(
// 在该回调中进行统一的权限控制
// Route<dynamic> Function(RouteSettings settings)
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其它情况则正常打开路由。
}
);
}
);
示例(onGenerateRoute统一处理传参)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// 通常放在单独的文件中
final routes={
'/page1':(context)=>HomePageWidget(),
'/page2':(context,{arguments})=>MyPageWidget(arguments:arguments),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.lightBlue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute:"/page1",
// 通常将该方法抽离和routes位于单独的文件中
onGenerateRoute: (RouteSettings settings){
final String name = settings.name; // 路由名
final Function pageContentBuilder = this.routes[name];
if(settings.arguments!=null){
final Route route=MaterialPageRoute(
builder: (context)=>pageContentBuilder(context,arguments:settings.arguments),
);
return route;
}else{
final Route route=MaterialPageRoute(
builder: (context)=>pageContentBuilder(context),
);
return route;
}
},
);
}
}
class HomePageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('hello'),
),
body: new Text('hello'),
floatingActionButton: FloatingActionButton(
onPressed: _pushNewPage,
child: Icon(Icons.add),
),
);
}
void _pushNewPage() {
Navigator.of(context).pushNamed("/page2", arguments: {
"id":110,
});
}
}
class MyPageWidget extends StatelessWidget {
final arguments;
MyPageWidget({this.arguments});
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('MyPageWidget'),
),
body: new Text('${arguments!=null?arguments['id']:'hello'}'),
);
}
}
/*
class MyPageWidget extends StatefulWidget {
final arguments;
MyPageWidget({Key key,this.arguments}):super(key:key);
@override
_MyPageWidgetState createState()=>_MyPageWidgetState(arguments:arguments);
}
class _MyPageWidgetState extends State<MyPageWidget> {
final arguments;
_MyPageWidgetState({this.arguments});
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('MyPageWidget'),
),
body: new Text('${arguments!=null?arguments['id']:'hello'}'),
);
}
}
*/
示例(onGenerateRoute)
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return HelloPageWidget();
})
);
class HelloPageWidget extends StatelessWidget{
final _navigatorKey=GlobalKey<NavigatorState>();
Future bool _onWillPopFunc() async{
final maybePop=await _navigatorKey.currentState.maybePop();
return Future.value(maybePop);
}
@override
Widget build(BuildContext context) {
return WillPopScope( // 使用WillPopScope来使内层导航器响应Android实体键返回。
onWillPop: _onWillPopFunc,
child: Navigator(
key: _navigatorKey,
initialRoute: '/helloPage',
onGenerateRoute: (settings){
WidgetBuilder builder:{
switch(settings.name){ // 路由名
case '/register':
builder=(_)=>RegisterPage();
default:
throw Exception('error');
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
}
},
),
);
}
}
- fluro路由库
使用
第1步. 创建FluroRouter路由实例
class RouterManager {
static String splashPath = '/';
static String loginPath = '/login';
static String homePath = '/home';
static String dynamicPath = '/dynamic';
static String dynamicDetailPath = '$dynamicPath/:id';
//
static FluroRouter router;
static void initRouter() {
if (router == null) {
router = FluroRouter();
defineRoutes();
}
}
static var loginHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return LoginPage();
});
static var dynamicDetailHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return DynamicDetailPage(params['id'][0]);
});
static var splashHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return Splash();
});
static var homeHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return AppHomePage();
});
static var notFoundHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return NotFound();
});
// Fluro的路由匹配次序是按照定义路由的先后次序进行匹配的,因此需要把更具体的路由放置在范围匹配的前面
static void defineRoutes() {
router.define(splashPath, handler: splashHandler);
router.define(homePath, handler: homeHandler);
router.define(loginPath, handler: loginHandler);
router.define(dynamicDetailPath, handler: dynamicDetailHandler);
// 路由不存在时,设置错误路由处理器
router.notFoundHandler = notFoundHandler;
}
}
/*
看一下Handler类的定义:
class Handler {
Handler({this.type = HandlerType.route, required this.handlerFunc});
// route(默认)、function。
final HandlerType type;
// 返回值为将要跳转的页面。parameters为路由参数。
// typedef Widget? HandlerFunc(BuildContext? context, Map<String, List<String>> parameters);
final HandlerFunc handlerFunc;
}
*/
第2步. 根Widget中
在build方法开始位置加上:
RouterManager.initRouter();
在MaterialApp中把onGenerateRoute设置为RouterManager.router.generator。
第3步. 跳转
1. RouterManager.router.navigateTo(context, RouterManager.loginPath);
直接跳转(无参数)
2. RouterManager.router.navigateTo(context, '${RouterManager.dynamicPath}/$id?event=a&event=b')
跳转(带参数)
3. RouterManager.router.navigateTo(context, RouterManager.homePath, clearStack: true);
清除路由堆栈跳转:即跳转后的页面作为根页面(没有返回按钮)。
设置转场动画
enum TransitionType { // 转场动画类型
native, // 原生形式,和原生的保持一致(默认)
nativeModal, // 原生模态跳转
inFromLeft, // 从左滑入
inFromTop, // 从顶部滑入
inFromRight, // 从右滑入
inFromBottom,// 从底部滑入
fadeIn, // 渐现
custom, // 自定义,需要配合 transitionBuilder 使用
material, // 安卓风格跳转
materialFullScreenDialog, // 安卓风格全屏对话框(左上角带有关闭按钮)
cupertino, // iOS 风格跳转
cupertinoFullScreenDialog,// iOS风格全屏对话框(左上角带有关闭按钮)
none, // 无转场动画
}
2种设置方式
1. 定义路由处理器Handler时设置transitionType参数。
router.define(transitionPath,handler: transitionHandler,transitionType: TransitionType.inFromBottom);
2. 使用navigateTo跳转时设置transition参数。
RouterManager.router.navigateTo(context, RouterManager.transitionPath,transition: TransitionType.inFromRight,transitionDuration: Duration(milliseconds: 1000));
自定义转场动画
先看看fluro转场部分源码是怎么实现转场动画的:
// 根据不同的枚举类型返回不同的动画。
// TransitionType.fadeIn使用的是Flutter自带的FadeTransition。上下左右滑入使用的是Flutter自带的SlideTransition。
RouteTransitionsBuilder _standardTransitionsBuilder(
TransitionType? transitionType) {
return (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (transitionType == TransitionType.fadeIn) {
return FadeTransition(opacity: animation, child: child);
} else {
const Offset topLeft = const Offset(0.0, 0.0);
const Offset topRight = const Offset(1.0, 0.0);
const Offset bottomLeft = const Offset(0.0, 1.0);
Offset startOffset = bottomLeft;
Offset endOffset = topLeft;
if (transitionType == TransitionType.inFromLeft) {
startOffset = const Offset(-1.0, 0.0);
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromRight) {
startOffset = topRight;
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromBottom) {
startOffset = bottomLeft;
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromTop) {
startOffset = Offset(0.0, -1.0);
endOffset = topLeft;
}
return SlideTransition(
position: Tween<Offset>(
begin: startOffset,
end: endOffset,
).animate(animation),
child: child,
);
}
};
}
自定义转场动画只需要transition设置为TransitionType.custom,然后transitionBuilder返回相应动画就可以了。
/*
Flutter常用转场动画(继承自AnimatedWidget)
FadeTransition(透明)
SlideTransition(上下左右滑入)
RotationTransition(旋转)
ScaleTransition(缩放)
看一下RotationTransition的定义:
RotationTransition({
Key? key,
// 旋转角度。推荐的起始值0.2至0.3之间,结束值为0表示回到正常位置。起始值如果为负,则是顺时针;如果为正则是逆时针。
required Animation<double> turns,
this.alignment = Alignment.center, // 旋转中心点
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
*/
路由拦截
fluro没有提供类似onGenerateRoute方法来在每次跳转时进行路由拦截。
路由拦截(两种方式)
1. 在定义路由时,对于未授权的路由地址跳转到403未授权页面。
2. 继承FluroRouter类,重写navigateTo跳转方法拦截路由。
示例(自定义转场动画:逆时针围绕中心旋转)
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return RotationTransition(
turns: Tween<double>(
begin: 0.25,
end: 0.0,
).animate(animation),
child: child,
);
},
);
示例(自定义转场动画:缩放)
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(
begin: 0.5,
end: 1.0,
).animate(animation),
child: child,
);
},
);
示例(自定义转场动画:通过继承AnimatedWidget自定义动画)
class SkewTransition extends AnimatedWidget {
const SkewTransition({
Key key,
Animation<double> turns,
this.alignment = Alignment.center,
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
Animation<double> get turns => listenable as Animation<double>;
final Alignment alignment;
final Widget child;
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform =
Matrix4.skew(turnsValue * pi * 2.0, turnsValue * pi * 2.0);
// 返回一个 Transform 对象
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return SkewTransition(
turns: Tween<double>(
begin: -0.05,
end: 0.0,
).animate(animation),
child: child,
);
},
);
示例(定义路由时拦截)
为了保证路由拦截有效,必须在初始化路由前就通过登录人信息拿到路由白名单。
为了改善用户体验,可以预先明确哪些页面不涉及权限管控(如闪屏页,首页,登录页)。
// 完整路由表
static final routeTable = {
loginPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return LoginPage();
}),
dynamicDetailPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return DynamicDetailPage(params['id'][0]);
}),
splashPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return Splash();
}),
transitionPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return TransitionPage();
}),
homePath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return AppHomePage();
}),
};
// 未授权页面处理器
static final permissionDeniedHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return PermissionDenied();
});
// 定义路由
// 添加路由时,将路由路径与白名单进行比对,若不在白名单内,则使用未授权路由处理器。
static void defineRoutes({List<String> whiteList}) {
routeTable.forEach((path, handler) {
if (whiteList == null || whiteList.contains(path)) {
router.define(path, handler: handler);
} else {
router.define(path,handler: permissionDeniedHandler,transitionType: TransitionType.material);
}
});
router.notFoundHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return NotFound();
});
}
示例(继承FluroRouter类,重写navigateTo方法拦截路由)
如果首页不涉及授权,可以在 App 启动后再获取授权白名单,而不需要在启动时获取,可以降低启动时的任务,加快启动速度和提高用户体验。
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
class PermissionRouter extends FluroRouter {
List<String> _whiteList;
set whiteList(value) => _whiteList = value;
String _permissionDeniedPath;
set permissionDeniedPath(value) => _permissionDeniedPath = value;
@override
Future navigateTo(
BuildContext context,
String path, {
bool replace = false,
bool clearStack = false,
bool maintainState = true,
bool rootNavigator = false,
TransitionType transition,
Duration transitionDuration,
transitionBuilder,
RouteSettings routeSettings,
}) {
String pathToNavigate = path;
// 如果匹配成功,则返回匹配的路由对象AppRouteMatch,如果没有匹配到则返回 null。
AppRouteMatch routeMatched = this.match(path);
// 获取匹配到的路径
String routePathMatched = routeMatched?.route?.route;
if (routePathMatched != null) {
// 设置了白名单且当前路由不在白名单内,更改路由路径到授权被拒绝页面
if (_whiteList != null && !_whiteList.contains(routePathMatched)) {
pathToNavigate = _permissionDeniedPath;
}
}
return super.navigateTo(context, pathToNavigate,
replace: replace,
clearStack: clearStack,
maintainState: maintainState,
rootNavigator: rootNavigator,
transition: transition,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
routeSettings: routeSettings);
}
}
/*
match方法匹配成功返回AppRouteMatch对象(有一个AppRoute类型的route属性)
class AppRoute {
String route; // 匹配到的路由路径
dynamic handler;
TransitionType? transitionType;
Duration? transitionDuration;
RouteTransitionsBuilder? transitionBuilder;
AppRoute(this.route, this.handler,
{this.transitionType, this.transitionDuration, this.transitionBuilder});
}
*/
- 路由2.0
为了满足
1. Web端复杂路由的需要
2. 状态驱动界面的设计理念。界面与行为进行分离,通过更改状态来驱动界面完成既定行为。
因此,路由2.0最关键的地方就是之前的Navigator.push或Navigator.pop方法不见了,界面只是响应用户操作去更改数据状态,而页面路由跳转统一交给了RougterDelegate来完成。
优点
1. 路由管理和路由解析分离,可以自己定义路由解析类和路由参数配置类,更为灵活。
2. 路由页面可以动态生成,因此实现动态路由更为简单。
3. 页面无需管理跳转逻辑,将页面和路由分离解耦,保持状态驱动界面的一致性。
4. 可以引入状态管理组件来管理整个 App 的路由状态,扩展性更强。
新加入了如下内容:
1. Page
设置Navigator导航器历史堆栈的不可变对象。
2. Router
设置Navigator导航器要显示的页面列表。
3. RouteInformationParser
从路由信息提供者(RouteInformationProvider)获取路由信息并将信息解析转换为用户定义的数据类型。
4. RouterDelegate
定义路由如何获知App状态改变的行为,以及如何响应这些行为。它的职责就是监听 RouteInformationParser 和 App 状态,然后构建当前页面列表的导航器。
5. BackButtonDispatcher
向路由通知返回按钮点击事件。
流程
用户点击跳转,RouteInformationParser解析路径为对应的类,RouterDelegate会调用其setNewRoutePath方法(传入解析的类,设置状态,调用notifyListeners),当 notifyListeners 被调用后会通知Router重建RouterDelegate(调用build,返回一个新导航器),最新页面。
示例
1. 在根widget的build方法中使用MaterialApp.router构建,设置路由委托routerDelegate和路由信息解析器routeInformationParser。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: '2.0路由',
routerDelegate: AppRouterDelegate(),
routeInformationParser: AppRouterInformationParser(),
// 省略其他代码
);
}
}
2. 路由解析类 app_router_path.dart。
import 'package:flutter/cupertino.dart';
// 路由枚举,不同枚举对应不同页面
enum RouterPaths { splash, dynamicList, dynamicDetail, notFound }
class AppRouterConfiguration { // 路由配置类
final RouterPaths path; // 当前路由path路径
final dynamic state; // 状态数据state(用于将数据传递到新的页面)
AppRouterConfiguration(this.path, this.state);
}
// 路由信息解析类,继承自RouteInformationParser<AppRouterConfiguration>
// 当进行路由跳转时就会调用路由解析方法,获取对应的路由配置对象。
class AppRouterInformationParser
extends RouteInformationParser<AppRouterConfiguration> {
@override
Future<AppRouterConfiguration> parseRouteInformation(
RouteInformation routeInformation) async {
final String routeName = routeInformation.location;
switch (routeName) {
case '/':
return AppRouterConfiguration(
RouterPaths.splash, routeInformation.state);
case '/home':
return AppRouterConfiguration(
RouterPaths.dynamicList, routeInformation.state);
case '/dynamicDetail':
return AppRouterConfiguration(
RouterPaths.dynamicDetail, routeInformation.state);
default:
return AppRouterConfiguration(
RouterPaths.notFound, routeInformation.state);
}
}
// 不同的路由枚举返回不同的路由信息对象。
@override
RouteInformation restoreRouteInformation(
AppRouterConfiguration configuration) {
switch (configuration.path) {
case RouterPaths.splash:
return RouteInformation(location: '/', state: configuration.state);
case RouterPaths.dynamicList:
return RouteInformation(location: '/home', state: configuration.state);
case RouterPaths.dynamicDetail:
return RouteInformation(
location: '/dynamicDetail', state: configuration.state);
default:
return RouteInformation(location: '/404', state: configuration.state);
}
}
}
3. 路由委托实现类 router_delegate.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:home_framework/dynamic_detail.dart';
import 'package:home_framework/models/dynamic_entity.dart';
import 'package:home_framework/not_found.dart';
import 'package:home_framework/routers/app_router_path.dart';
import 'package:home_framework/splash.dart';
import '../dynamic.dart';
// AppRouterDelegate继承自RouterDelegate<AppRouterConfiguration>,实现了ChangeNotifier、PopNavigatorRouterDelegateMixin。
// ChangeNotifier用于增加状态更改监听对象(由底层完成)和通知监听对象进行动作,当有状态改变时应当调用 notifyListeners方法通知所有监听者。
// PopNavigatorRouterDelegateMixin用于管理返回事件,只有一个方法,可以覆盖来自定义返回事件。
class AppRouterDelegate extends RouterDelegate<AppRouterConfiguration>
with
ChangeNotifier,
PopNavigatorRouterDelegateMixin<AppRouterConfiguration> {
// 用于存储导航器状态的GlobalKey,可以全局获知导航器的当前状态。
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// 存储当前路由,发生改变后会触发通知进行路由跳转。
RouterPaths _routerPath;
get routerPath => _routerPath;
set routerPath(RouterPaths value) {
if (_routerPath == value) return;
_routerPath = value;
notifyListeners();
}
// 路由状态对象(即路由参数)。
dynamic _state;
get state => _state;
// 启动页是否完成,有启动页时首页是启动页,用于在启动完成后将启动页移除路由表,以便显示实际的首页。
bool _splashFinished = false;
get splashFinished => _splashFinished;
set splashFinished(bool value) {
if (_splashFinished == value) return;
_splashFinished = value;
notifyListeners();
}
// 构建路由,通过一个 Navigator 包裹全部路由页面。
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: _buildPages(),
onPopPage: _handlePopPage,
);
}
// 用于返回 build 方法所需要的pages参数。
List<Page<void>> _buildPages() {
if (_splashFinished) { // 启动页加载完成
return [
MaterialPage(
// 根据路由枚举匹配对应页面,同时指定自定义处理方法(该页面有几种跳转就需要几个)。
key: ValueKey('home'), // 第一个路由是首页。
child: DynamicPage(_handleDynamicItemChanged)), // 对应的跳转页面
if (_routerPath == RouterPaths.splash)
MaterialPage(
key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
if (_routerPath == RouterPaths.dynamicDetail)
MaterialPage(
key: ValueKey('dynamicDetail'), child: DynamicDetail(state)),
if (_routerPath == RouterPaths.notFound)
MaterialPage(key: ValueKey('notFound'), child: NotFound()),
];
} else { //
return [
MaterialPage(
key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
];
}
}
// 一些自定义处理方法
void _handleSplashFinished() {
_routerPath = RouterPaths.dynamicList;
_splashFinished = true;
notifyListeners();
}
void _handleDynamicItemChanged(DynamicEntity dynamicEntity) {
_routerPath = RouterPaths.dynamicDetail;
_state = dynamicEntity;
notifyListeners();
}
// 覆写了PopNavigatorRouterDelegateMixin的方法
@override
Future<bool> popRoute() async {
return true;
}
// 传入路由配置对象进行跳转。
@override
Future<void> setNewRoutePath(AppRouterConfiguration configuration) async {
_routerPath = configuration.path;
_state = configuration.state;
}
// 返回处理方法(build方法用到)
bool _handlePopPage(Route<dynamic> route, dynamic result) {
final bool success = route.didPop(result);
return success;
}
// 通过_routerPath和_state构建当前的路由配置对象并返回
@override
AppRouterConfiguration get currentConfiguration =>
AppRouterConfiguration(routerPath, state);
}
4. 由于代码不能再使用 push 和 pop 跳转和返回,因此涉及到这些都需要变更。
启动页
class Splash extends StatefulWidget {
final Function onFinished; // 具体实现在AppRouterDelegate类中
Splash(this.onFinished, {Key key}) : super(key: key);
@override
_SplashState createState() => _SplashState(onFinished);
}
class _SplashState extends State<Splash> {
final Function onFinished;
_SplashState(this.onFinished);
bool _initialized = false;
//省略其他代码
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
Timer(const Duration(milliseconds: 2000), () {
onFinished(); // 跳转
});
}
}
}
动态列表页
需要接收一个onItemTapped方法来响应每行元素的点击事件,并把点击的元素作为参数传递。
这种方式暴露了业务的实现,破坏了封装性,而且如果父子元素嵌套过深会导致传递链路过长。
需要使用类似Redux的状态管理器来解耦。
2. 资源管理
Flutter APP安装包中会包含 代码 和 资源(assets) 两部分。
常见assets类型:
1. 静态数据(json文件)
2. 配置文件
3. 图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)
指定assets
和包管理一样,Flutter使用pubspec.yaml来管理资源。
1. assets指定应包含在应用程序中的文件。
2. 每个asset都通过相对于pubspec.yaml文件所在的文件系统路径来标识自身的路径。
3. asset的声明顺序是无关紧要的。
4. asset的实际目录可以是任意文件夹。
5. Asset变体(适配各机型)
在选择匹配当前设备分辨率的图片时,Flutter会使用到asset变体。未来会将这种机制扩展到本地化、阅读提示等方面。
构建过程支持“asset变体”:不同版本的asset可能会显示在不同的上下文中。 在pubspec.yaml的assets部分中指定asset路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的asset一起被包含在asset bundle中。
6. 在构建期间,Flutter将asset放置到称为asset bundle中,应用程序可以在运行时读取它们(但不能修改)。
示例
flutter:
assets:
- assets/my_icon.png
- assets/background.png
这里的图片文件夹名是assets,也可是imgs(imgs/my_icon.png)
示例2(Asset变体)
如果应用程序目录中有以下文件:
…/graphics/background.png
…/graphics/dark/background.png
pubspec.yaml文件中只需包含:
flutter:
assets:
- graphics/background.png
graphics/background.png和graphics/dark/background.png 都将会包含在asset bundle中。
前者被认为是main asset (主资源),后者被认为是一种变体(variant)。
- 文本(json文件、)
加载文本(2种方法)
1. 通过rootBundle对象加载
每个Flutter应用程序都有一个rootBundle对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset。
2. 通过DefaultAssetBundle加载(建议)
使用DefaultAssetBundle.of(context) 来获取当前BuildContext的AssetBundle。
获取的不是应用程序构建的默认asset bundle,而是使父级widget在运行时动态替换的不同的AssetBundle,这对于本地化或测试场景很有用。
示例(通过rootBundle对象加载)
通常使用DefaultAssetBundle方式来加载,但在widget上下文之外或其它AssetBundle句柄不可用时,则使用rootBundle加载:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
- 图片
1. 在pubspec.yaml文件指定asset
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/my_icon.png 将被选择。对于2.7的设备像素比率,.../3.0x/my_icon.png将被选择。
/*
如果未在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。即,如果.../my_icon.png是72px乘72px,那么.../3.0x/my_icon.png应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。
pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。
*/
2. 使用AssetImage类加载图片
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('graphics/background.png'),
),
),
);
}
AssetImage 并非是一个widget, 它实际上是一个ImageProvider,有些时候可能期望直接得到一个显示图片的widget,那么可以使用Image.asset()方法,如:
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}
3. 如果要加载依赖包中的图像,必须给AssetImage提供package参数。
示例:
假设应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:
…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png
…etc.
然后加载图像,使用:
new AssetImage('icons/heart.png', package: 'my_icons')
或
new Image.asset('icons/heart.png', package: 'my_icons')
包在使用本身的资源时也必须在pubspec.yaml文件中加上package参数来获取。
包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:
…/lib/backgrounds/background1.png
…/lib/backgrounds/background2.png
…/lib/backgrounds/background3.png
如果要包含第一张图像,则必须在pubspec.yaml的assets部分中声明它:
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
lib/是隐含的,所以它不应该包含在资产路径中。
上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用。
如果要给应用设置APP图标或者添加启动图,那必须使用特定平台的assets。
特定平台 assets
iOS
Assets.xcassets中设置App图标、启动图。直接拖入图片。
Android
App图标:.../android/app/src/main/res目录,里面包含了各种资源文件夹,替换。
启动图:.../android/app/src/main目录。也可以在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面。
注意:如果重命名了.png文件,则必须在AndroidManifest.xml的<application>标签的android:icon属性中更新名称。
在Flutter框架加载时,Flutter会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。这意味着如果不在应用程序的main()方法中调用runApp 函数 (更具体地说,如果不调用window.render去响应window.onDrawFrame)的话, 启动屏幕将永远持续显示。
- 字体
默认字体
在iOS上:
中文字体:PingFang SC
英文字体:.SF UI Text 、.SF UI Display
在Android 上:
中文字体:Source Han Sans / Noto
英文字体:Roboto
/*
iOS使用.SF的好处:
SF Text 的字距及字母的半封闭空间,比如 "a"! 上半部分会更大,因其可读性更好,适用于更小的字体;
SF Display 则适用于偏大的字体。
字体小于 20pt 时用 Text ,大于等于 20pt 时用 Display 。
由于 SF 属于动态字体,系统自动根据字体的大小匹配这两种显示模式。
*/
自定义字体
1. 在pubspec.yaml中声明(以确保字体会打包到应用程序中)。
flutter:
fonts:
- family: Raleway // family设置字体名, 用于TextStyle的fontFamily属性中使用。
fonts:
- asset: assets/fonts/Raleway-Regular.ttf // asset 是相对于 pubspec.yaml 文件的字体路径
- asset: assets/fonts/Raleway-Medium.ttf
weight: 500 // 指定字体的粗细,取值范围是100到900之间的整百数(100的倍数). 对应TextStyle的FontWeight属性
style: italic // 指定字体是倾斜还是正常,对应的值为italic和 normal. 对应TextStyle的 fontStyle TextStyle 属性
- asset: assets/fonts/Raleway-SemiBold.ttf
weight: 600
- family: AbrilFatface
fonts:
- asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
2. 通过style(TextStyle)使用字体。
// 声明文本样式
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
// 使用文本样式
var buttonText = const Text(
"Use the font for this text",
style: textStyle,
);
Package中的字体
1. 要使用Package中定义的字体,必须提供package参数。
如果在package包内部使用它自己定义的字体,也应该在创建文本样式时指定package参数。
const textStyle = const TextStyle(
fontFamily: 'Raleway',
package: 'my_package', // 指定包名
);
2. 一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该存放在包的lib/文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件:lib/fonts/Raleway-Medium.ttf
然后,应用程序可以声明一个字体,如下面的示例所示(lib/是隐含的,所以它不应该包含在asset路径中。):
flutter:
fonts:
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: packages/my_package/fonts/Raleway-Medium.ttf
weight: 500
在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle时可以不指定package参数:
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
示例(自定义字体)
1. 在pubsec.yaml中声明字体
name: my_application
description: A new Flutter project.
dependencies:
flutter:
sdk: flutter
flutter:
# Include the Material Design fonts.
uses-material-design: true
fonts:
- family: Rock Salt
fonts:
# https://fonts.google.com/specimen/Rock+Salt
- asset: fonts/RockSalt-Regular.ttf
- family: VT323
fonts:
# https://fonts.google.com/specimen/VT323
- asset: fonts/VT323-Regular.ttf
- family: Ewert
fonts:
# https://fonts.google.com/specimen/Ewert
- asset: fonts/Ewert-Regular.ttf
2. 使用字体
import 'package:flutter/material.dart';
const String words1 = "Almost before we knew it, we had left the ground.";
const String words2 = "A shining crescent far beneath the flying vessel.";
const String words3 = "A red flair silhouetted the jagged edge of a wing.";
const String words4 = "Mist enveloped the ship three hours out from port.";
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Fonts',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FontsPage(),
);
}
}
class FontsPage extends StatefulWidget {
@override
_FontsPageState createState() => new _FontsPageState();
}
class _FontsPageState extends State<FontsPage> {
@override
Widget build(BuildContext context) {
// Rock Salt - https://fonts.google.com/specimen/Rock+Salt
var rockSaltContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Rock Salt",
),
new Text(
words2,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "Rock Salt",
fontSize: 17.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// VT323 - https://fonts.google.com/specimen/VT323
var v2t323Container = new Container(
child: new Column(
children: <Widget>[
new Text(
"VT323",
),
new Text(
words3,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "VT323",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// https://fonts.google.com/specimen/Ewert
var ewertContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Ewert",
),
new Text(
words4,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "Ewert",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// Material Icons font - included with Material Design
String icons = "";
// https://material.io/icons/#ic_accessible
// accessible:  or 0xE914 or E914
icons += "\u{E914}";
// https://material.io/icons/#ic_error
// error:  or 0xE000 or E000
icons += "\u{E000}";
// https://material.io/icons/#ic_fingerprint
// fingerprint:  or 0xE90D or E90D
icons += "\u{E90D}";
// https://material.io/icons/#ic_camera
// camera:  or 0xE3AF or E3AF
icons += "\u{E3AF}";
// https://material.io/icons/#ic_palette
// palette:  or 0xE40A or E40A
icons += "\u{E40A}";
// https://material.io/icons/#ic_tag_faces
// tag faces:  or 0xE420 or E420
icons += "\u{E420}";
// https://material.io/icons/#ic_directions_bike
// directions bike:  or 0xE52F or E52F
icons += "\u{E52F}";
// https://material.io/icons/#ic_airline_seat_recline_extra
// airline seat recline extra:  or 0xE636 or E636
icons += "\u{E636}";
// https://material.io/icons/#ic_beach_access
// beach access:  or 0xEB3E or EB3E
icons += "\u{EB3E}";
// https://material.io/icons/#ic_public
// public:  or 0xE80B or E80B
icons += "\u{E80B}";
// https://material.io/icons/#ic_star
// star:  or 0xE838 or E838
icons += "\u{E838}";
var materialIconsContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Material Icons",
),
new Text(
icons,
textAlign: TextAlign.center,
style: new TextStyle(
inherit: false,
fontFamily: "MaterialIcons",
color: Colors.black,
fontStyle: FontStyle.normal,
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text("Fonts"),
),
body: new ListView(
children: <Widget>[
rockSaltContainer,
v2t323Container,
ewertContainer,
materialIconsContainer,
],
),
);
}
}