作为系列文章的第十七篇,本篇再一次带来 Flutter 开发过程中的实用技巧,让你继续弯道超车,全篇均为个人的日常干货总结,以实用填坑为主,让你少走弯路狂飙车。

文章汇总地址:

Flutter 完整实战实战系列文章专栏

Flutter 番外的世界系列文章专栏

十七、实用技巧与填坑二 - 图1

1、Package get git 失败

Flutter 项目在引用第三库时,一般都是直接引用 pub 上的第三方插件,但是有时候我们为了安全和私密,会选择使用 git 引用,如:

  1. photo_view:
  2. git:
  3. url: https://github.com/CarSmallGuo/photo_view.git
  4. ref: master

这时候在执行 flutter packages get 过程中,如果出现失败后,再次执行 flutter packages get 可能会遇到如下图所示的问题:

十七、实用技巧与填坑二 - 图2)

flutter packages get 提示 git 失败的原因,主要是:

在下载包的过程中出现问题,下次再拉包的时候,.pub_cache 内的 git 目录下会检测到已经存在目录,但是可能是空目录等等,导致 flutter packages get 的时候异常。

所以你需要清除掉 .pub_cache 内的 git 的异常目录,然后最好清除掉项目下的 pubspec.lock ,之后重新执行 flutter packages get

win 一般是在 C:\Users\xxxxx\AppData\Roaming\Pub\Cache 路径下有 git 目录。

mac 目录在 ~/.pub-cache

2、TextEditingController

image.png

如上代码所示,红线部分表示,如果 controller 为空,就赋值一个 TextEditingController ,这样的写法会导致如下图所示问题:

十七、实用技巧与填坑二 - 图4

弹出键盘时输入成功后,收起键盘时输入的内容消失了! 这是因为键盘的弹出和收起都会触发页面 build ,而在 controllernull 时,每次赋值的 TextEditingController 会导致 TextFieldTextEditingValue 重置。

image.png

如上图所示,因为当 TextFieldcontroller 不为空时,update 时是不会执行 value 的拷贝,所以为了避免这类问题,如下图所示, 需要先在全局构建 TextEditingController 再赋值,如果 controller 为空直接给 null 即可,避免 build 时每次重构 TextEditingController

十七、实用技巧与填坑二 - 图6

3、Scrollable

十七、实用技巧与填坑二 - 图7

如上图所示,在之前第七篇的时候分析过,滑动列表内一般都会有 Scrollable 的存在,而 Scrollable 恰好是一个 InheritedWidget ,这就给我们在 children 中调用 Scrollable 相关方法提供了便利。

如下代码所依,通过 Scrollable.of(context) 我们可以更解耦的在 ListView/GridViewchildren 对其进行控制。

  1. ScrollableState state = Scrollable.of(context)
  2. ///获取 _scrollable 内 viewport 的 renderObject
  3. RenderObject renderObject = state.context.findRenderObject();
  4. ///监听位置更新
  5. state.position.addListener((){});
  6. ///通知位置更新
  7. state.position.notifyListeners();
  8. ///滚动到指定位置
  9. state.position.jumpTo(1000);
  10. ····

4、图片高斯模糊

十七、实用技巧与填坑二 - 图8

在 Flutter 中,提供了 BackdropFilterImageFilter 实现了高斯模糊的支持,如下代码所示,可以快速实现上图的高斯模糊效果。

  1. class BlurDemoPage extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. body: new Container(
  6. child: Stack(
  7. children: <Widget>[
  8. Positioned(
  9. top: 0,
  10. bottom: 0,
  11. left: 0,
  12. right: 0,
  13. child: new Image.asset(
  14. "static/gsy_cat.png",
  15. fit: BoxFit.cover,
  16. width: MediaQuery.of(context).size.width,
  17. height: MediaQuery.of(context).size.height,
  18. )),
  19. new Center(
  20. child: new Container(
  21. width: 200,
  22. height: 200,
  23. child: ClipRRect(
  24. borderRadius: BorderRadius.circular(15.0),
  25. child: BackdropFilter(
  26. filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
  27. child: new Row(
  28. mainAxisSize: MainAxisSize.max,
  29. crossAxisAlignment: CrossAxisAlignment.center,
  30. mainAxisAlignment: MainAxisAlignment.center,
  31. children: <Widget>[
  32. new Icon(Icons.ac_unit),
  33. new Text("哇!!")
  34. ],
  35. )))))
  36. ],
  37. )));
  38. }
  39. }

5、滚动到指定位置

因为目前 Flutter 并没有直接提供滚动到指定 Item 的方法,在每个 Item 大小不一的情况下,折中利用如图下所示代码,可以快速实现滚动到指定 Item 的效果:

十七、实用技巧与填坑二 - 图9

上图为部分代码,完整代码可见 scroll_to_index_demo_page2.dart ,这里主要是给每个 item 都赋予了一个 GlobalKey , 利用 findRenderObject 找到所需 itemRenderBox ,然后使用 localToGlobal 获取 itemViewPort 这个 ancestor 中的偏移量进行滚动:

十七、实用技巧与填坑二 - 图10

当然还有另外一种实现方式,具体可见 scroll_to_index_demo_page.dart

6、findRenderObject

在 Flutter 中是存在 容器 Widget渲染Widget 的区别的,一般情况下:

  • TextSliverListTile 等都是属于渲染 Widget ,其内部主要是 RenderObjectElement
  • StatelessWidget / StatefulWidget 等属于容器 Widget ,其内部使用的是 ComponentElementComponentElement 本身是不存在 RenderObject 的。

结合前面篇章我们说过 BuildContext 的实现就是 Element,所以 context.findRenderObject() 这个操作其实就是 ElementfindRenderObject()

十七、实用技巧与填坑二 - 图11

那么如上图所示,findRenderObject 的实现最终就是获取 renderObject,在 ElementrenderObject 的获取逻辑就很清晰了,在遇到 ComponentElement 时,执行的是 element.visitChildren(visit); , 递归直到找到 RenderObjectElement

所以如下代码所示,print("${globalKey.currentContext.findRenderObject()}"); 最终输出了 SizedBoxRenderObject

十七、实用技巧与填坑二 - 图12

7、行间距

十七、实用技巧与填坑二 - 图13

在 Flutter 中,是没有直接设置 Text 行间距的方法的, Text 显示的效果是如下图所示的逻辑组成:

十七、实用技巧与填坑二 - 图14

那么我们应该如何处理行间距呢?如下图所示,通过设置 StrutStyleleading , 然后利用 Transform 做计算翻方向位置偏移,因为 leading 是上下均衡的,所以计算后就可以得到我们所需要的行间距大小。 (虽然无法保证一定 100%像素准确,你是否还知道其他方法?)

十七、实用技巧与填坑二 - 图15

这里额外提一点,可以通过父节点使用 DefaultTextStyle 来实现局部样式的共享哦。

8、Builder

十七、实用技巧与填坑二 - 图16

在 Flutter 中存在 Builder 这样一个 Widget,看源码发现它其实就是 StatelessWidget 的简单封装,那为什么还需要它的存在呢?

如下图所示,相信一些 Flutter 开发者在使用 Scaffold.of(context).showSnackBar(snackbar) 时,可能 遇到过如下错误,这是因为传入的 context 属于错误节点导致的,因为此处传入的 context 并不能找到页面所在的 Scaffold 节点。

十七、实用技巧与填坑二 - 图17

所以这时候 Builder 的作用就体现了,如下所示,通过 builder 方法返回赋予的 context ,在向上查找 Scaffold 的时候,就可以顺利找到父节点的 Scaffold 了,这也一定程度上体现了 ComponentElement 的作用之一。

十七、实用技巧与填坑二 - 图18

9、快速实现动画切换效果

十七、实用技巧与填坑二 - 图19

要实现如上图所示动画效果,在 Flutter 中提供了 AnimatedSwitcher 封装简易实现。

如下图所示,通过嵌套 AnimatedSwitcher ,指定 transitionBuilder 动画效果,然后在数据改变时,同时改变需要执行动画的 key 值,即可达到动画切换的效果。

十七、实用技巧与填坑二 - 图20

10、多语言显示异常

在官方的 https://github.com/flutter/flutter/issues/36527 issue 中可以发现,Flutter 在韩语/日语 与中文同时显示,会导致 iOS 下出现文字渲染异常的问题 ,如下图所示,左边为异常情况。

十七、实用技巧与填坑二 - 图21

改问题解决方案暂时有两种:

  • 增加字体 ttf ,全局指定改字体显示。

  • 修改主题下所有 TextThemefontFamilyFallback

  1. getThemeData() {
  2. var themeData = ThemeData(
  3. primarySwatch: primarySwatch
  4. );
  5. var result = themeData.copyWith(
  6. textTheme: confirmTextTheme(themeData.textTheme),
  7. accentTextTheme: confirmTextTheme(themeData.accentTextTheme),
  8. primaryTextTheme: confirmTextTheme(themeData.primaryTextTheme),
  9. );
  10. return result;
  11. }
  12. /// 处理 ios 上,同页面出现韩文和简体中文,导致的显示字体异常
  13. confirmTextTheme(TextTheme textTheme) {
  14. getCopyTextStyle(TextStyle textStyle) {
  15. return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
  16. }
  17. return textTheme.copyWith(
  18. display4: getCopyTextStyle(textTheme.display4),
  19. display3: getCopyTextStyle(textTheme.display3),
  20. display2: getCopyTextStyle(textTheme.display2),
  21. display1: getCopyTextStyle(textTheme.display1),
  22. headline: getCopyTextStyle(textTheme.headline),
  23. title: getCopyTextStyle(textTheme.title),
  24. subhead: getCopyTextStyle(textTheme.subhead),
  25. body2: getCopyTextStyle(textTheme.body2),
  26. body1: getCopyTextStyle(textTheme.body1),
  27. caption: getCopyTextStyle(textTheme.caption),
  28. button: getCopyTextStyle(textTheme.button),
  29. subtitle: getCopyTextStyle(textTheme.subtitle),
  30. overline: getCopyTextStyle(textTheme.overline),
  31. );
  32. }

ps :通过WidgetsBinding.instance.window.locale; 可以获取到手机平台本身的当前语言情况,不需要 context ,也不是你设置后的 Locale

11、长按输入框导致异常的情况

如果项目存在多语言和主题切换的场景,可能会遇到长按输入框导致异常的场景,目前可推荐两种解放方法:

  • 1、可以给你的自定义 ThemeData 强制指定固定一个平台,但是该方式会导致平台复制粘贴弹出框没有了平台特性:
  1. ///防止输入框长按崩溃问题
  2. platform: TargetPlatform.android
  • 2、增加一个自定义的 LocalizationsDelegate , 实现多语言环境下的自定义支持:
  1. class FallbackCupertinoLocalisationsDelegate
  2. extends LocalizationsDelegate<CupertinoLocalizations> {
  3. const FallbackCupertinoLocalisationsDelegate();
  4. @override
  5. bool isSupported(Locale locale) => true;
  6. @override
  7. Future<CupertinoLocalizations> load(Locale locale) => loadCupertinoLocalizations(locale);
  8. @override
  9. bool shouldReload(FallbackCupertinoLocalisationsDelegate old) => false;
  10. }
  11. class CustomZhCupertinoLocalizations extends DefaultCupertinoLocalizations {
  12. const CustomZhCupertinoLocalizations();
  13. @override
  14. String datePickerMinuteSemanticsLabel(int minute) {
  15. if (minute == 1) return '1 分钟';
  16. return minute.toString() + ' 分钟';
  17. }
  18. @override
  19. String get anteMeridiemAbbreviation => '上午';
  20. @override
  21. String get postMeridiemAbbreviation => '下午';
  22. @override
  23. String get alertDialogLabel => '警告';
  24. @override
  25. String timerPickerHourLabel(int hour) => '小时';
  26. @override
  27. String timerPickerMinuteLabel(int minute) => '分';
  28. @override
  29. String timerPickerSecond(int second) => '秒';
  30. @override
  31. String get cutButtonLabel => '裁剪';
  32. @override
  33. String get copyButtonLabel => '复制';
  34. @override
  35. String get pasteButtonLabel => '粘贴';
  36. @override
  37. String get selectAllButtonLabel => '全选';
  38. }
  39. class CustomTCCupertinoLocalizations extends DefaultCupertinoLocalizations {
  40. const CustomTCCupertinoLocalizations();
  41. @override
  42. String datePickerMinuteSemanticsLabel(int minute) {
  43. if (minute == 1) return '1 分鐘';
  44. return minute.toString() + ' 分鐘';
  45. }
  46. @override
  47. String get anteMeridiemAbbreviation => '上午';
  48. @override
  49. String get postMeridiemAbbreviation => '下午';
  50. @override
  51. String get alertDialogLabel => '警告';
  52. @override
  53. String timerPickerHourLabel(int hour) => '小时';
  54. @override
  55. String timerPickerMinuteLabel(int minute) => '分';
  56. @override
  57. String timerPickerSecond(int second) => '秒';
  58. @override
  59. String get cutButtonLabel => '裁剪';
  60. @override
  61. String get copyButtonLabel => '復制';
  62. @override
  63. String get pasteButtonLabel => '粘貼';
  64. @override
  65. String get selectAllButtonLabel => '全選';
  66. }
  67. Future<CupertinoLocalizations> loadCupertinoLocalizations(Locale locale) {
  68. CupertinoLocalizations localizations;
  69. if (locale.languageCode == "zh") {
  70. switch (locale.countryCode) {
  71. case 'HK':
  72. case 'TW':
  73. localizations = CustomTCCupertinoLocalizations();
  74. break;
  75. default:
  76. localizations = CustomZhCupertinoLocalizations();
  77. }
  78. } else {
  79. localizations = DefaultCupertinoLocalizations();
  80. }
  81. return SynchronousFuture<CupertinoLocalizations>(localizations);
  82. }

自此,第十七篇终于结束了!(///▽///)

资源推荐

十七、实用技巧与填坑二 - 图22