前言
目前使用Flutter开发App已有两年时间,上线了两款App,App store者应用宝搜索脑学家可以下载体验。下面介绍一下我在开发中遇到的坑。
如何选择路由方式
Flutter中有命名路由和组件路由,最开始使用Flutter开发项目自带的路由都没有使用使用了一个第三方的路由fluro,这个路由的工作原理是在routes没有的情况下在onGenerateRoute获取到路由的名称进行跳转。相当于在routes没有找到对应的路由才会使用fluro声明的路由,两个可以结合使用。
命名路由
命名路由就是给每个页面一个名字我们可以使用这个名字跳转到对于的页面,下面介绍一下用法。
路由列表/lib/router/router_list.dart
定义了RouterUnit类,里面包含路由的名称、路由名称、需要构建的路由组件,里面包含了多个路由最后返回的是一个列表。
routeName使用的使用类名.routeName,我们将路由的名称定义到了组件中,只有这一个地方定义的路由的名称避免多处定义造成路由名称不相同的问题,使用时直接调用对于类就可以完成。
class NewView extends StatefulWidget {
final String? content;
const NewView({this.content});
static const String routeName = '/newView'; //路由名称
@override
_NewViewState createState() => _NewViewState();
}
复制代码
import 'package:dynamic_theme/containers/chat_list.dart';
import 'package:dynamic_theme/containers/detail.dart';
import 'package:dynamic_theme/containers/new_view.dart';
import 'package:dynamic_theme/containers/app.dart';
import 'package:dynamic_theme/router/router_unit.dart';
import 'package:flutter/widgets.dart';
List<RouterUnit> _buildRouter() {
final routerList = <RouterUnit>[
RouterUnit(
title: '首页',
routeName: NewView.routeName,
buildRoute: (BuildContext context) => const App(),
),
RouterUnit(
title: 'iOS跳转页面',
routeName: NewView.routeName,
buildRoute: (BuildContext context) => const NewView(),
),
RouterUnit(
title: '详情',
routeName: Detail.routeName,
buildRoute: (BuildContext context) => const Detail(),
),
RouterUnit(
title: '聊天信息',
routeName: ChatList.routeName,
buildRoute: (BuildContext context) => const ChatList(),
),
];
return routerList;
}
final List<RouterUnit> routerList = _buildRouter();
复制代码
配置路由
遍历路由将路由将我们的List路由变为Map类型
{
'/': (context) => const FirstScreen(),
'/second': (context) => const SecondScreen(),
}
复制代码
_buildRoutes方法遍历后会得到我们需要的类型,直接使用就可以,到这里我们就实现了原生的命名路由。
Map<String, WidgetBuilder> _buildRoutes() =>
{for (var data in routerList) data.routeName: data.buildRoute};
复制代码
@override
Widget build(BuildContext context) {
return MaterialApp(
...
routes: _buildRoutes(),
);
}
复制代码
路由跳转
两种方式都是命名路由的跳转方式,呈现的形式都是一样的。具体说明参考restorablePushNamed,参数的传递通过arguments传递,任何对象都可以作为arguments(例如 String、int或自定义MyRouteArguments类的实例)传递。通常使用Map用于传递键值对。
NewView就是一个自定义的类型传递。
Navigator.of(context).pushNamed(
NewView.routeName,
arguments: NewView(content: '网络搜索结果汉语- 维基百科,自由的百科全书'),
)
复制代码
或
Navigator.of(context).restorablePushNamed
(
NewView.routeName,
arguments: {content: '网络搜索结果汉语- 维基百科,自由的百科全书'},
)
复制代码
如何修改路由动画
使用命名路由的时候我们的路由动画在没有修改的情况下iOS的路由动画是从右到左,安卓的动画是从下到上。为了统一路由和主题我们都使用iOS路由动画,同时支持iOS的手势动画。在安卓手机上呈现的路由动画就和iOS是一样的。
修改theme和darkTheme的ThemeData platform为TargetPlatform.iOS。
darkTheme是暗黑模式主题。
ThemeData(
platform: TargetPlatform.iOS,
darkTheme: TargetPlatform.iOS,
pageTransitionsTheme: PageTransitionsTheme(builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
}),
...
)
复制代码
我们目前路由都是从右向左滑动但是我们也需要路由从下往上滑动应该如何实现?
介绍一下我最常用的方法,通过组件路由直接跳转页面,然后自定义动画方式。
跳转使用方法
Navigator.of(context).push(bottomPopRouter(Text('组件')))
复制代码
PageRouteBuilder可以自己去实现你需要的跳转方式,代码所示是底部弹出页面。你可以按自己的需求去实现各种方式的过渡页面。
// 底部弹出窗
Route bottomPopRouter(
Widget widget, {
opaque = false,
}) =>
PageRouteBuilder(
opaque: opaque as bool,
pageBuilder: (context, animation, secondaryAnimation) => widget,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(0.0, 1.0);
var end = Offset.zero;
var curve = Curves.ease;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
复制代码
// 对话框,放大缩小出现
Route showDialogRouter(Widget widget, {Color? barrierColor}) => PageRouteBuilder(
opaque: false,
barrierColor: barrierColor ?? Colors.black.withOpacity(0.5),
transitionDuration: Duration(milliseconds: 120),
pageBuilder: (context, animation, secondaryAnimation) => widget,
transitionsBuilder: (_, Animation<double> animation, __, Widget child) =>
FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1.0).animate(animation),
child: child,
),
),
);
复制代码
路由返回
Flutter的路由是栈的形式存在的最先push的路由在最底层我们使用Navigator.of(context).pop方法只能一次返回一个页面。
怎样返回多个页面?
不推荐方式
popUntil一次推出两个页面或者调用两次Navigator.of(context).pop方法,当然这不是最好的方法会出现意想不到的问题。
我在开发中遇到过黑屏问题以及页面跳转出错的问题,为什么会出现这种问题?
首先是黑屏的问题,pop栈的时候超出了栈里面的总路由数,就会把最上层的路由给清理掉没有页面了自然就黑屏了。
页面跳转出错问题发生原因,页面跳转后点击按钮发送请求会出现加载Loading提示,这个提示的现实也是push一个路由如果多个地方有调用或者失败不处理会导致最后不清楚目前路由数量pop的路由只是一个Loading提示框而不是我们的页面。
例子:
pop之前
['/', '/rute01']
Navigator.of(context).pop
pop一次之后
['/']
Navigator.of(context).pop
pop两次之后
[]
复制代码
count = 0;
Navigator.popUntil(context, (route) {
return count++ == 2;
});
或
Navigator.of(context).pop
Navigator.of(context).pop
复制代码
推荐方式
返回最顶层的路由
Navigator.of(context).popUntil((route) => route.isFirst);
复制代码
返回指定路由
例子:
['/', '/route01', '/route02', '/route03']
复制代码
返回到/route01
Navigator.of(context).popUntil(ModalRoute.withName('/route01'));
复制代码
注意使用这种方式跳转页面需要使用命名路由!!!使用这种方式我们不需要考虑推出路由太多黑屏的问题,你只要知道要返回哪个页面就好。
如何获取全局上下文(context)
刚刚我们讲到了路由跳转,路由跳转需要一个上下文(context)。我们来说一个应用场景,开发时我们将store和组件分开这样我们可以减少代码的耦合度也方便管理,相当于store里面都是逻辑代码比如请求数据处理。例如登录功能我们要在页面点击登录然后发送请求登录成功跳转页面错误给出提示信息,这里都需要context,我们可以在调用方法的时候传进来,当然这也是一种方法。
如果代码嵌套很深context需要一直传下去,这种方法可行,但是看上去很繁琐。
创建一个GlobalKey
项目的第一个组件创建
class App extends StatefulWidget {
const App();
static GlobalKey<NavigatorState> materialKey = GlobalKey();
static RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
@override
_DynamicThemeState createState() => _DynamicThemeState();
}
复制代码
MaterialApp的navigatorKey传入我们定义好的App.materialKey,意思是我们在App创建组件的时候将这个key和我们的组件绑定上,之后我们可以使用这个key找到context。
MaterialApp(
title: 'Dynamic Theme',
navigatorKey: App.materialKey,
theme: lightTheme.copyWith(platform: _options.platform),
darkTheme: darkTheme.copyWith(platform: _options.platform),
themeMode: _options.themeMode,
onGenerateRoute: (_) {
// 当通过Navigation.of(context).pushNamed跳转路由时,
// 在routes查找不到时,会调用该方法
return PageRouteBuilder(
pageBuilder: (BuildContext context, _, __) {
//这里为返回的Widget
return Material(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('404', style: Theme.of(context).textTheme.headline4),
CupertinoButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Back'),
)
],
),
);
},
opaque: false,
transitionDuration: Duration(milliseconds: 200),
transitionsBuilder:
(_, Animation<double> animation, __, Widget child) =>
FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
),
);
},
navigatorObservers: [App.routeObserver],
home: Entrance(
options: _options,
handleOptionsChanged: _handleOptionsChanged,
),
builder: (context, Widget? child) {
return _applyTextScaleFactor(
// Specifically use a blank Cupertino theme here and do not transfer
// over the Material primary color etc except the brightness to
// showcase standard iOS looks.
Builder(builder: (BuildContext context) {
return CupertinoTheme(
data: CupertinoThemeData(
brightness: Theme.of(context).brightness,
),
child: child ?? const Text('找不到模块'),
);
}),
);
},
routes: _buildRoutes(),
)
复制代码
获取context
App.materialKey.currentContext
复制代码
如何监听路由返回?
App的使用中我们肯定有跳转页面修改信息的需求,例如:修改用户信息以后需要返回页面我们重新获取新的数据。那么Flutter是如何实现监听的呢?
不推荐的方式
使用这种方式可以监听页面返回,但是会有问题。我在iOS手机上使用手势返回时是监听不到的而且每个页面跳转都需要去单独加会很麻烦。尽管写成公共方法也需要单独给每个页面跳转加上方法。
Navigator.of(context).pushNamed('routeName').then((value) => print('监听页面返回'));
或
await Navigator.of(context).pushNamed('routeName');
print('监听页面返回')
复制代码
推荐的方式
通过混入`RouteAware`监听路由状态来获取页面是否返回,通过这种方式我们只需要在`didPopNext`方法内请求数据即可,`iOS`手势侧滑也会触发监听。
复制代码
class _NewViewState extends State<NewView> with RouteAware {
@override
void didChangeDependencies() {
super.didChangeDependencies();
App.routeObserver
.subscribe(this, ModalRoute.of(context) as PageRoute<dynamic>);
}
@override
void didPopNext() {
// Covering route was popped off the navigator.
print('返回到当前页面');
}
@override
void didPush() {
// Route was pushed onto navigator and is now topmost route.
print('进入新的页面');
}
@override
void dispose() {
// 取消监听
App.routeObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
// 获取页面的参数
final param = ModalRoute.of(context)!.settings.arguments as NewView;
return Text('页面');
}
}
复制代码
颜色字体基础类
开发App的过程中设计会给到我们App的配色,原则上App的颜色不会很多我们可以做成一个公共类方便后续使用,如果后期需要修改颜色值我们只需要修改一个地方就可以(一劳永逸)。当然现在App有暗黑模式也是两套颜色我们也是可以很好的去适配。字体也是如此,我们会统一管理方便统一调整。
颜色公共类color.dart
使用类方法时我们需要使用传入context用于判断当前是否是暗黑模式,调用Theme.of(context).brightness进行判断根据不同的状态返回不同的颜色。如果我们想实现多彩的配色也可以使用这个方法,配置多种不同的颜色值。
import 'package:flutter/material.dart';
class ColorTheme {
late Color borderColor, cubeColor;
late Color activeNavColor;
late Color navBarColor;
late Color colorF3F3F6;
late Color color202326;
ColorTheme.of(BuildContext context) {
// 暗黑色
if (Theme.of(context).brightness == Brightness.dark) {
borderColor = const Color(0xfff16161);
cubeColor = Colors.white70;
activeNavColor = Colors.brown;
navBarColor = const Color(0xff161616);
colorF3F3F6 = const Color(0xff18191b);
color202326 = const Color(0xff4e5156);
return;
}
// 明亮色
borderColor = const Color(0xffdedede);
cubeColor = Colors.black38;
activeNavColor = Colors.amberAccent;
navBarColor = Colors.white;
colorF3F3F6 = const Color(0xffF3F3F6);
color202326 = const Color(0xff202326);
}
}
复制代码
字体公共类text_theme_style.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
@immutable
class TextThemeStyle with Diagnosticable {
final TextStyle? font17;
final TextStyle? fontBold17;
final TextStyle? font16;
final TextStyle? fontBold16;
final TextStyle? font14;
final TextStyle? font12;
const TextThemeStyle({
this.font17,
this.fontBold17,
this.font16,
this.fontBold16,
this.font14,
this.font12,
});
static TextThemeStyle of(BuildContext context) {
final _fontFamily = Platform.isIOS ? '.SF UI Text' : 'Roboto';
final _fontWeight = Platform.isIOS ? FontWeight.w500 : FontWeight.w600;
final _lineHeight = 1.2;
return TextThemeStyle(
font17: TextStyle(
fontSize: 17.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
),
fontBold17: TextStyle(
fontSize: 17.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
fontWeight: _fontWeight,
),
font16: TextStyle(
fontSize: 16.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
),
fontBold16: TextStyle(
fontSize: 16.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
fontWeight: _fontWeight,
),
font14: TextStyle(
fontSize: 14.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
),
font12: TextStyle(
fontSize: 12.0,
color: ColorTheme.of(context).color202326,
fontFamily: _fontFamily,
height: _lineHeight,
),
);
}
}
复制代码
项目地址dynamic_theme
环境说明
master是最新的Flutter2.2.x版本另外有flutter-1.17.x flutter-1.22.x flutter-2.0.x,根据你的flutter版本拉取分支,如果无法运行请检查版本。
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu不胜感激 !