Flutter 在实际开发中遇到的一些问题及解决方案,作为笔记记录。
1、container width、height 100% 1 2 3 4 5 FractionallySizedBox( widthFactor: 1, heightFactor: 1, child: , )
1 2 double width = MediaQuery.of(context).size.width double height = MediaQuery.of(context).size.height
2、沉浸式背景图片 背景图片铺满 Appbar 及状态栏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Widget build(BuildContext context) { return new Stack( children: <Widget>[ Container( child: Image.network('https://www.bing.com/az/hprichbg/rb/Punakaiki_DE-DE0884339574_1920x1080.jpg'), color: Colors.lightGreen, ), Scaffold( backgroundColor: Colors.transparent, appBar: AppBar( backgroundColor: Colors.transparent, title: Text('Coin'), ), body: MyCoinPage(), ) ], ); }
3、APPBar no shadow AppBar 默认底部有阴影。去掉只需要配置参数即可:
1 2 3 AppBar( elevation: 0.0, )
4、自定义字体 1 2 3 4 5 6 7 8 9 10 flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true fonts: - family: Kphi fonts: - asset: assets/fonts/Komet-Pro-Heavy-Italic.otf
5、Tab 切换动画 Tab 切换需加入SingleTickerProviderStateMixin
通过 with SingleTickerProviderStateMixin
实现 Tab 页的切换动画效果。因为初始化animationController
的时候需要一个TickerProvider
类型的参数Vsync
参数,所以我们混入了TickerProvider
的子类SingleTickerProviderStateMixin
。
6、 逐帧动画 FrameAnimationImage
Flutter 逐帧动画 FrameAnimationImage
。 逐帧动画: 依次顺序循环播放多张图片组成一个动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 /// Flutter FrameAnimation 逐帧动画 import 'package:flutter/material.dart'; class FrameAnimationImage extends StatefulWidget { final List<String> assetList; final double width; final double height; final int interval; FrameAnimationImage( {this.assetList, this.width, this.height, this.interval = 200}); @override State<StatefulWidget> createState() { return _FrameAnimationImageState(); } } class _FrameAnimationImageState extends State<FrameAnimationImage> with SingleTickerProviderStateMixin { // 动画控制 Animation<double> _animation; AnimationController _controller; int interval = 200; @override void initState() { super.initState(); if (widget.interval != null) { interval = widget.interval; } final int imageCount = widget.assetList.length; final int maxTime = interval * imageCount; // 启动动画controller _controller = new AnimationController( duration: Duration(milliseconds: maxTime), vsync: this); _controller.addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { _controller.forward(from: 0.0); // 完成后重新开始 } }); _animation = new Tween<double>(begin: 0, end: imageCount.toDouble()) .animate(_controller) ..addListener(() { setState(() { // the state that has changed here is the animation object’s value }); }); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { int ix = _animation.value.floor() % widget.assetList.length; List<Widget> images = []; // 把所有图片都加载进内容,否则每一帧加载时会卡顿 for (int i = 0; i < widget.assetList.length; ++i) { if (i != ix) { images.add(Image.asset( widget.assetList[i], width: 0, height: 0, )); } } images.add(Image.asset( widget.assetList[ix], width: widget.width, height: widget.height, )); return Stack(alignment: AlignmentDirectional.center, children: images); } }
7、 时间格式化库 intl
8、 时间格式化库intl
和textDirection
冲突 报错如下:
1 The getter 'rtl' isn't defined for the class 'TextDirection'
参考: Stack Overflow
解决方案:
1 import 'package:intl/intl.dart' as intl;
9、 convert string to date dart 将时间字符串转为 date 库:
参考: Stack Overflow
1 2 var parsedDate = DateTime.parse('1974-03-20 00:00:00.000'); print('dateTime: $parsedDate');
10、 tabcontroller.addListener Tab 切换时,可以通过tabcontroller.addListener
监听切换事件。在实际开发中发现如下问题,可以根据需求选择不同的判断方式:
1 2 3 4 5 6 7 _tabController = new TabController(length: 2, vsync: this); _tabController.addListener(() { // _tabController.addListener... if (_tabController.indexIsChanging) { // _tabController.indexIsChanging... } });
当点击切换时:
_tabController.addListener 触发两次
_tabController.indexIsChanging 触发一次
当滑动切换时:
_tabController.addListener 触发一次
_tabController.indexIsChanging 不触发
参考: FutureBuilder 延迟加载组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @override Widget build(BuildContext context) { return new FutureBuilder( future: getTextFromFile(), initialData: "Loading text..", builder: (BuildContext context, AsyncSnapshot<String> text) { return new SingleChildScrollView( padding: new EdgeInsets.all(8.0), child: new Text( text.data, style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 19.0, ), )); }); } Future<String> getFileData(String path) async { return await new Future(() => "test text"); } Future<String> getTextFromFile() async { return getFileData("test.txt"); } }
12、获取页面生命周期 使用 with WidgetsBindingObserver
后,可以对页面的生命周期进行监听。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver { AppLifecycleState _lastLifecycleState; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print('current state: $state'); setState(() { _lastLifecycleState = state; }); } }
13、页面重构 1、TabView + pull_to_refresh 如何优雅共存 当页面中有 Tab 切换,且每个 Tab 页都是可以下拉刷新、上拉加载的组件时。如果每个TabView
使用的是同一个组件,只是根据不同的标识显示不同的内容。在实际开发中发现,这么做虽然可以减少部分代码量,但是出现的问题是,Tab 切换时每次都等于是重新渲染公共的组件,导致比较卡顿,且切换时会发生多个 TabView 间的数据混乱等一些列问题。而且多个 TabView 公用一个 scrollController 的话也会出很多问题。
正确的做法是,每个 TabView 应该是独立组件,包括互相独立的 scrollController。只不过其中的一些子组件是可以抽出来共用的,这是没问题的。
2、listView 显示占位图。
空列表的一般需要显示占位图,开始的做法是,判断列表为空时,直接返回一个占位图的 Container,代替 ListView。这样做之后,发现数据为空时就不能下拉刷新了,因为下拉刷新和 ListView 绑定在一起。于是改为下述方案,当数据为空时任然可以下拉刷新,实时更新数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 child: new SmartRefresher( headerConfig: const RefreshConfig(completeDuration: 300), enablePullDown: true, enablePullUp: true, onRefresh: _onRefresh, headerBuilder: LoadingIndicator.buildHeader, footerBuilder: LoadingIndicator.buildFooter, controller: _refreshController, child: (!loading && _payList.length == 0) ? ListView.builder( itemCount: 1, itemBuilder: (context, i) { return _emptyTip(); }, ) : _listView(), ),
14、网络图片加载组件 默认的图片加载组件 networkImage,当图片链接错误、为空时,会报 exception 导致 Crash。FadeInImage 同理。
参考: GitHub
试了好几款组件都没有做错误处理,最终找到一个可用的组件:Flutter_image ,同时配合FadeInImage
设置默认占位图,完美。
1 2 3 4 5 6 adeInImage( fit: fit, placeholder: AssetImage(placeHolderImage), image: NetworkImageWithRetry(imgUrl), ),
15、主题颜色 ThemeData
16、网络图片加载失败 Exception 使用 flutter_image
的 NetworkImageWithRetry
代替 NetworkImage
后,当图片 404 或者错误时,确实没有报 Exception 奔溃,但是安卓端却显示 crash。
换了很多其他的插件都不好使,最终查明,NetworkImageWithRetry
虽然可以 catch exception 不让应用奔溃,但是会触发 Flutter.onError,而安卓端在检测到 onError 之后,当做 crash 处理,直接将页面关闭….
方案: 修改组件源码,图片加载失败时,不要触发 Flutter.onError。
17、ActionSheet 点击隐藏 点击 ActionSheet 选择某项后,隐藏之。
1 Navigator.of(context, rootNavigator: true).pop("Discard");
18、阻止软键盘顶起页面 当软键盘弹起时,会将页面高度缩减,导致页面中的内容随着父级高度变化而变化。即软键盘弹起时将内容顶起,导致诸多问题。
解决方案:
1 2 3 4 5 6 return Scaffold( appBar: AppBar( title: new Text("通讯录"), ), resizeToAvoidBottomPadding: false, //输入框抵住键盘 内容不随键盘滚动 );
19、dart
正则 1 2 3 4 5 6 7 8 9 10 RegExp regExp = new RegExp( r'(ludis)', caseSensitive: false, // 是否大小写敏感 multiLine: false, // 待匹配文字是否是多行的 ); // 正则 var d = regExp.allMatches(text); d.forEach((f) => print(f.group(0))); print('match result, ${d.toString()}, ${d.toList()}');
20、键盘确认按钮样式 键盘的确认按钮会显示为“搜索”,点击后触发onSubmitted
事件。
1 2 3 4 5 TextFormField( ... textInputAction: TextInputAction.done, ... )
21、Map 类型错误 json 转换时报错如下:
1 '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'
原因: 类型为Map
的其实际类型为Map<dynamic, dynamic>
而非Map<String, dynamic>
。
解决方法: 进行转换
1 2 Map json; // Map<dynamic, dynamic> Map<String, dynamic> = new Map<String, dynamic>.from(json); // Map<String, dynamic>
22、GestureDetector 点击区域小 使用GestureDetector
包裹Container
,发现在Container
内容为空的区域点击时,捕捉不到onTap
点击事件。
参考: GitHub
解决方法:
1 2 3 4 5 GestureDetector( behavior: HitTestBehavior.opaque, onTap: (){}, child: Container() )
23、TextField 优化: TextField decoration
可使用以下两种:
使用 InputDecoration.collapsed()
。其高度不能和父级同高,导致可点击区域变小,用户体验不好。优势在于InputDecoration.collapsed()
为无边框的输入框。
使用InputDecoration
。可使输入框高度和父级同高,扩大可点击区域。去掉边框即可。
解决方法:
1 2 3 InputDecoration( border: InputBorder.none, // 去除输入框边框 )
24、Flutter hide keyboard 调用方法隐藏键盘。
1 FocusScope.of(context).requestFocus(new FocusNode());
25、去除 TextFiled 水纹效果 ripple effect 参考: how-to-disable-default-widget-splash-effect-in-flutter
解决方案:
使用自定义主体,覆盖默认主题
给组件包裹 Theme 容器,设置 splashFactory
为 Colors.transparent
。✅
1 2 3 4 5 child: new Theme( data: new ThemeData(splashFactory: Colors.transparent), child: new TextField(...), ),
Dart 数组、字符串常用方法: