目前使用Flutter
开发App已有两年时间,上线了两款App,App store
者应用宝
搜索脑学家
可以下载体验。下面介绍一下我在开发中遇到的坑。
Flutter中有命名路由
和组件路由
,最开始使用Flutter开发项目自带的路由都没有使用使用了一个第三方的路由fluro
,这个路由的工作原理是在routes
没有的情况下在onGenerateRoute
获取到路由的名称进行跳转。相当于在routes
没有找到对应的路由才会使用fluro
声明的路由,两个可以结合使用。
命名路由就是给每个页面一个名字我们可以使用这个名字跳转到对于的页面,下面介绍一下用法。
定义了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 _buildRouter() { final routerList = [ 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 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 materialKey = GlobalKey(); static RouteObserver routeObserver = RouteObserver(); @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: [ 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不胜感激 !