当前位置:主页 > 查看内容

Flutter 开发中的一些 Tips

发布时间:2021-08-01 00:00| 位朋友查看

简介:学习Flutter也有一阵子了。闲着没事,用了公司一个已经凉凉的App设计图来练手。当然了接口不可能用的了,所以都是些死数据,实现效果可以说是很***了(得到了设计的认可。。。)。当然自己也是边查边写,也借鉴了许多Github上优秀的Flutter项目。现在开源出来(……

学习Flutter也有一阵子了。闲着没事,用了公司一个已经凉凉的App设计图来练手。当然了接口不可能用的了,所以都是些死数据,实现效果可以说是很***了(得到了设计的认可。。。)。当然自己也是边查边写,也借鉴了许多Github上优秀的Flutter项目。现在开源出来(附带设计图),供大家交流学习。希望多多Star、Fork支持,有问题可以Issue。附上链接:github.com/simplezhli/…

本篇主要分享一下自己在此项目中遇到的问题及心得,希望对你有所帮助!

1.部件溢出

异常大致如下:

  1. A RenderFlex overflowed by 22 pixels on the bottom. 

导致的原因就是在水平或者垂直方向上的内容超过了父部件的大小。一般来说我们的页面不存在这样的问题,因为根据页面的设计,事先可以预料到是否超出。不过要注意到有输入法弹出的页面。比如我下面的这个例子:

Flutter 开发中的一些 Tips

Flutter 开发中的一些 Tips

可以看到底部溢出了22个像素,可能在18:9的手机以上不太会出现这种问题,因为屏幕的高度足够。但是这种16:9的手机可能会暴露出来。解决的方法有两种:

包一层SingleChildScrollView,让你的页面可以滑动起来。

在Scaffold中设置resizeToAvoidBottomInset为false。默认为ture,防止部件被遮挡。如果使用了这个方法,如果底部有输入框,则会造成遮挡。

大家可以根据实际需求选择。

2.输入框的遮挡

页面如下:

Flutter 开发中的一些 Tips

底部有输入框,同时“提交”的按钮固定在底部。一开始觉得既然固定在底部,那就使用Stack配合Positioned来实现,然而就导致输入法弹出时,发生遮挡。

Flutter 开发中的一些 Tips

上图中,我选中了***一个输入框,但因为输入法默认都是在输入框的下方弹出,然而上面盖着这个“提交”按钮,发生了遮挡。

最终我的解决方法就是使用Column配合Expanded来实现。修复后如下:

Flutter 开发中的一些 Tips

3.SafeArea

一旦有部件固定在顶部或者底部(严谨点的话可以说是在屏幕的四边)。那我我们***使用SafeArea来包一下。因为Android 和 IOS都有状态栏,甚至IOS还有叫做“HomeIndicator”的横条。所以一不留神就会出现适配问题。

我们在Flutter中常使用的BottomNavigationBar 和 AppBar 其实就在内部处理了此类问题。以 AppBar源码为例:

  1. class _AppBarState extends State { 
  2.  
  3.   @override 
  4.   Widget build(BuildContext context) { 
  5.      
  6.     if (widget.primary) { 
  7.       appBar = SafeArea(  // <--- 1 
  8.         toptrue
  9.         child: appBar, 
  10.       ); 
  11.     } 
  12.      
  13.     return Semantics( 
  14.       container: true
  15.       child: AnnotatedRegion( 
  16.         value: overlayStyle, 
  17.         child: Material( // <--- 2 
  18.           color: widget.backgroundColor 
  19.             ?? appBarTheme.color 
  20.             ?? themeData.primaryColor, 
  21.           child: Semantics( 
  22.             explicitChildNodes: true
  23.             child: appBar, 
  24.           ), 
  25.         ), 
  26.       ), 
  27.     ); 
  28.   } 

所以使用方法为:

Material( // 需要颜色填充到边界区域可以使用

  1. Material( // 需要颜色填充到边界区域可以使用 
  2.   color: Colors.white, 
  3.   child: SafeArea( 
  4.     child: Container(), 
  5.   ), 

还是上面的页面,我们对比一下处理前后的效果:

Flutter 开发中的一些 Tips

Flutter 开发中的一些 Tips

4.善用Theme

Flutter 在开发中,让人诟病的就是大量的嵌套,而我们只能尽量避免。比如将一些部件、属性进行封装,避免重复的书写。不过封装也讲究使用场景。如果这种样式的部件仅仅只是某一两处使用,封装显得有点小题大做。并且封装的大而全也会增加使用的复杂度。那么这时就可以使用Theme这种办法。

举一个例子,在下图中圈起来的部分有三个按钮,它们的高度相同,文字、圆角大小也相同。如果每一个都去设定这些属性,未免太过麻烦。

Flutter 开发中的一些 Tips

这时我们使用Theme去统一修改它们的样式,就会很方便了。

  1. Theme(  
  2.               data: Theme.of(context).copyWith( 
  3.                 buttonTheme: ButtonThemeData( 
  4.                   padding: const EdgeInsets.symmetric(horizontal: 16.0), 
  5.                   minWidth: 64.0, 
  6.                   height: 30.0, 
  7.                   materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 
  8.                   shape:RoundedRectangleBorder( 
  9.                     borderRadius: BorderRadius.circular(4.0), 
  10.                   ) 
  11.                 ), 
  12.                 textTheme: TextTheme( 
  13.                   button: TextStyle( 
  14.                     fontSize: 14.0, 
  15.                   ) 
  16.                 ) 
  17.               ), 
  18.               child: Row( 
  19.                 children: [ 
  20.                   FlatButton( 
  21.                     color: Color(0xFFF6F6F6), 
  22.                     onPressed: (){}, 
  23.                     child: Text("联系客户"), 
  24.                   ), 
  25.                   ...... 
  26.                   FlatButton( 
  27.                     color: Color(0xFFF6F6F6), 
  28.                     onPressed: (){}, 
  29.                     child: Text("拒单"), 
  30.                   ) 
  31.                 ], 
  32.               ), 
  33.             ) 

同时使用Theme还可以修改许多默认的设置,比如FlatButton的默认宽度为88,高度为36,但是FlatButton中没有直接修改的属性,网上好多的方法都是通过包一层Container去修改,不仅增加的嵌套,有些需求还不能达到。所以善用Theme可以让你省时省力,不过缺点就是你需要去翻翻源码,寻找使用这些Theme的地方。

5.注意平台差异

注意部分组件在Android与IOS平台之间的差异。

(1)Scaffold的 AppBar,AppBar中默认的title在Android中靠左显示,IOS中居中显示。如果需要两个平台效果统一,需要设置在AppBar中主动设置centerTitle属性。同时AppBar的返回箭头图标也不相同,统一的话需要自定义leading。

Flutter 开发中的一些 Tips

(2)页面跳转如果使用MaterialPageRoute来做过渡效果,注意Android中新的页面会从屏幕底部滑动到屏幕顶部,IOS中新的页面会从屏幕右侧滑动到屏幕左侧。

如果需要两个平台效果统一,我们不使用自带效果,可以自定义一个。

  1. Navigator.push(context, PageRouteBuilder(transitionDuration: Duration(milliseconds: 300), 
  2.   pageBuilder: (context, animation, secondaryAnimation){ 
  3.     return new FadeTransition( //使用渐隐渐入过渡, 
  4.       opacity: animation, 
  5.       child: TestPage(), 
  6.     ); 
  7.   }) 
  8. ); 

要么修改Theme,统一两平台的实现。:

  1. class MyApp extends StatelessWidget { 
  2.  
  3.   static const Map _defaultBuilders = { 
  4.     TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), 
  5.     TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(), 
  6.   }; 
  7.    
  8.   @override 
  9.   Widget build(BuildContext context) { 
  10.      
  11.     return MaterialApp( 
  12.       theme: ThemeData( 
  13.         pageTransitionsTheme: PageTransitionsTheme( 
  14.           builders: _defaultBuilders 
  15.         ) 
  16.       ), 
  17.       ... 
  18.     ); 
  19.   } 

(3)ScrollPhysics效果,可以滑动的部件都有一个physics属性。滑动到边界时,Android平台为边缘阴影的效果ClampingScrollPhysics,IOS为回弹效果BouncingScrollPhysics。如果需要统一,可以指定physics属性。

(4)状态栏方面,Android平台默认是半透明的效果,IOS则是透明效果。比如Android要实现IOS的效果,可以设置状态栏为透明。不过IOS要实现Android的效果则不行。。。,难道只能自定义?有知道方法的可以分享一下。

  1. void main(){ 
  2.   runApp(MyApp()); 
  3.   // 透明状态栏 
  4.   if (Platform.isAndroid) { 
  5.     SystemUiOverlayStyle systemUiOverlayStyle =  
  6.     SystemUiOverlayStyle(statusBarColor: Colors.transparent); 
  7.     SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 
  8.   } 

5.输入键盘

当TextField的keyboardType属性设置为TextInputType.phone 或TextInputType.number时,IOS系统弹出的数字输入键盘没有"完成"按钮,导致输入法无法关闭。当然了Android不存在这个问题。

比较成熟有效的方案是在键盘弹出的上方悬浮一个按钮,点击可以关闭键盘。当然了,这种问题也有对应的库可以解决,我使用的是flutter_keyboard_actions来解决了这个问题。因为在Android端我发现了部分输入法的兼容问题,所以只针对IOS做了处理。大家可以看一下前后对比图,具体实现代码可以参考flutter_keyboard_actions的文档和我的项目代码:

Flutter 开发中的一些 Tips

Flutter 开发中的一些 Tips

当然平台差异不仅仅是这么多,比如IOS自带侧滑返回等。具体我们可以去查看调用TargetPlatform枚举类的代码。

如果你觉得这样真麻烦,我给你支个大招,修改ThemeData的platform,指定一个平台。

  1. class MyApp extends StatelessWidget { 
  2.    
  3.   @override 
  4.   Widget build(BuildContext context) { 
  5.      
  6.     return MaterialApp( 
  7.       theme: ThemeData( 
  8.         platform: TargetPlatform.android 
  9.       ), 
  10.       ... 
  11.     ); 
  12.   } 

其次就是使用TextInputType.number在IOS中弹起的键盘没有小数点符号。在输入金额类型数据时,需要将keyboardType属性设置为TextInputType.numberWithOptions(decimal: true)。

6.keyboardType

keyboardType属性主要含义为弹起的键盘类型,并不代表输入数据的类型。

而在Android开发中,在EditText中设置android:inputType不仅可以指定弹起的键盘类型,同时也确定了输入数据的类型,也就是内置了数据的格式校验。Flutter中并没有后者,所以可能一开始你是TextInputType.number,但是在输入法中切换成中文键盘,一样可以输入中文字符。所以数据的校验需要我们使用inputFormatters自己处理。

比如TextInputType.phone时可以使用WhitelistingTextInputFormatter 白名单校验,只允许输入0~9:

  1. TextField( 
  2.       keyboardType: TextInputType.phone, 
  3.       inputFormatters: [WhitelistingTextInputFormatter(RegExp("[0-9]"))] 
  4.     ) 

输入密码时可以使用BlacklistingTextInputFormatter 黑名单校验,除去中文字符:

  1. TextField( 
  2.       keyboardType: TextInputType.text, 
  3.       inputFormatters: [BlacklistingTextInputFormatter(RegExp("[\u4e00-\u9fa5]"))] 
  4.     ) 

输入小数时,可以自定义TextInputFormatter来限制输入小数格式:

  1. TextField( 
  2.       keyboardType: TextInputType.numberWithOptions(decimaltrue), 
  3.       inputFormatters: [UsNumberTextInputFormatter()] 
  4.     ) 
  5. //来源:https://www.cnblogs.com/yangyxd/p/9639588.html 
  6. class UsNumberTextInputFormatter extends TextInputFormatter { 
  7.   static const defaultDouble = 0.001; 
  8.   static double strToFloat(String str, [double defaultValue = defaultDouble]) { 
  9.     try { 
  10.       return double.parse(str); 
  11.     } catch (e) { 
  12.       return defaultValue; 
  13.     } 
  14.   } 
  15.   @override 
  16.   TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { 
  17.     String value = newValue.text; 
  18.     int selectionIndex = newValue.selection.end
  19.     if (value == ".") { 
  20.       value = "0."
  21.       selectionIndex++; 
  22.     } else if (value != "" && value != defaultDouble.toString() && strToFloat(value, defaultDouble) == defaultDouble) { 
  23.       value = oldValue.text; 
  24.       selectionIndex = oldValue.selection.end
  25.     } 
  26.     return new TextEditingValue( 
  27.       text: value, 
  28.       selection: new TextSelection.collapsed(offset: selectionIndex), 
  29.     ); 
  30.   } 

7.InkWell

InkWell有的叫溅墨效果,有的叫水波纹效果。使用场景是给一些无点击事件的部件添加点击事件时使用(也支持长按、双击等事件),同时你也可以去修改它的颜色和形状。

  1. InkWell( 
  2.   borderRadius: BorderRadius.circular(8.0), // 圆角 
  3.   splashColor: Colors.transparent, // 溅墨色(波纹色) 
  4.   highlightColor: Colors.transparent, // 点击时的背景色(高亮色) 
  5.   onTap: () {},// 点击事件 
  6.   child: Container(), 
  7. ); 

不过有时你会发现并不是包一层InkWell就一定会有溅墨效果。主要原因是溅墨效果是在一个背景效果,并不是覆盖的前景效果。所以InkWell中的child一旦有设置背景图或背景色,那么就会遮住这个溅墨效果。如果你需要这个溅墨效果,有两种方式实现。

(1)包一层 Material,将背景色设置在 Material中的color里。

  1. Material( 
  2.   color: Colors.white, 
  3.   child: InkWell(), 

(2)使用Stack布局,将InkWell放置在上层。这种适用于给图片添加点击效果,比如Banner图的点击。

  1. Stack( 
  2.             children: [ 
  3.               Positioned.fill( 
  4.                 child: Image(), 
  5.               ), 
  6.               Positioned.fill( 
  7.                 child: Material( 
  8.                   color: Colors.transparent, 
  9.                   child: InkWell( 
  10.                     splashColor: Color(0X40FFFFFF), 
  11.                     highlightColor: Colors.transparent, 
  12.                     onTap: () {}, 
  13.                   ), 
  14.                 ), 
  15.               ) 
  16.             ], 
  17.           ) 

8.保持页面状态

比如点击导航栏来回切换页面,默认情况下会丢失原页面状态,也就是每次切换都会重新初始化页面。这种情况解决方法就是PageView与BottomNavigationBar结合使用,同时子页面State中继承AutomaticKeepAliveClientMixin并重写wantKeepAlive为true。代码大致如下:

  1. class _TestState extends State with AutomaticKeepAliveClientMixin{ 
  2.  
  3.   @override 
  4.   Widget build(BuildContext context) { 
  5.     super.build(context); 
  6.     return Container(); 
  7.   } 
  8.    
  9.   @override 
  10.   bool get wantKeepAlive => true

9.依赖版本问题

首先这里建议凡是Flutter的插件在填写版本号时不要使用^符号。

Flutter 开发中的一些 Tips

^符号意味着你可以使用此插件的***版本(大于等于当前版本)。这会导致什么问题呢?可能你前一天代码还能跑起来,今天就编译出错了。因为这些插件中包括Android、IOS的所用依赖环境配置,常见的就是新版本使用了AndroidX的依赖,但是还有些插件并没有使用AndroidX,导致了两者的冲突。

我之前在看flutter-go的代码时,就是因为webview的插件突然升级了,导致了安装失败。具体问题可以看这里。所以在代码稳定的情况下不建议使用^符号。

发生了这种问题,有以下几个解决方法:

  • 使用非AndroidX的版本插件。(优点就是见效快。缺点就是此插件后续的更新无法使用)
  • 手动修改插件的冲突,因为Flutter插件的代码是可以直接修改的,所以你可以手动修改掉这些冲突,统一插件的版本(优点就是可以使用***的版本。缺点就是这种方法首先麻烦,其次不利于团队开发使用)

我偏好使用第二种,只要做好修改的相关记录就行,算是一劳永逸。

10.Flutter Android 打包

打包本身流程没有问题,配置好签名文件,执行flutter build apk命令。但是发现打包后没有将插件中的AndroidManifest.xml文件合并。比如我有使用image_picker插件,它的AndroidManifest.xml文件如下:

Flutter 开发中的一些 Tips

可以看到有权限的及Android 7.0FileProvider的声明。诸如此类的信息没有打包进去(但是引用xml中的flutter_image_picker_file_paths文件却在),导致我实际使用这些功能时没有反应,但是在平时的调试过程中却是好的。

中间我发现打包后的App名称也是之前的,怀疑是缓存问题,所以我手动删除了项目根目录的build与.gradle文件夹,重新打包就好了。所以打包后***检查一下AndroidManifest.xml文件,避免此类缓存造成的问题。

11.其他

  1. Container 功能强大,设置宽高、padding、margin、背景色、背景图、圆角、阴影等都可以使用它。
  2. 有些widget 自带padding 属性,所以不必多套一层Padding部件。(比如ListView、GridView、Container、ScrollView、Button )
  3. 尽量使用const来定义常量。比如padding、color、style 这些地方:
  1. class Colours { 
  2.   static const Color text_dark = Color(0xFF333333); 
  3.  
  4. Padding( 
  5.   padding: const EdgeInsets.all(8.0), 
  6.   child: Text( 
  7.   "Test"
  8.   style: TextStyle( 
  9.       fontSize: 26.0, 
  10.       color: Colours.text_dark 
  11.     ) 
  12.   ) 

4.Dart2中的new 关键字可选,所以就不要选了,哈哈!!

其实我在这中间遇到的小问题还有很多,有的暂时还没有找到好的方法去解决。不过这才刚刚开始,希望Flutter越来越好。

篇幅有限,那么先分享以上11条Tips,如果本篇对你有所帮助,可以点赞支持!***再次奉上Github地址:github.com/simplezhli/…


本文转载自网络,原文链接:https://juejin.im/post/5d00fbfd51882570ec017660
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:你必须要掌握的Android冷启动优化 下一篇:没有了

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐