Flutter 空安全的糖果罐

7,963 阅读14分钟

前言

Fluter 2.0 已经发布了一段时间了,其中一项就是包含 Dart 2.12 的稳定版,完全支持空安全声明。作为一个进步的 Flutter 组织 , 组织的小伙伴也在第一时间支持了空安全。

组织发布的组件: pub.flutter-io.cn/publishers/…

组织支持空安全的进度: github.com/fluttercand…

介绍

以下的组件均已支持空安全,这里只会做简单的介绍,具体使用方法,请到各组件下地址查看。

屏幕适配

Adaptation,用于屏幕适配的组件,你只需要设置设计稿的宽度,其他的尺寸直接按照设计稿填写即可。当然这种适配方式其实是不推荐的,正如作者所言。

用户使用更大的屏幕是为了接收更多的信息, 而不是看到更大的字

基于这个观点, 我个人建议使用文字流式, 图片宽高比, 控件弹性的方案来做

但是很多初学者对于这个原则很难把握, 而等比例放大比较容易理解, 所以我写了这个库

import 'package:adaptation/adaptation.dart';
 MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
    visualDensity: VisualDensity.adaptivePlatformDensity,
  ),
  home: MyHomePage(title: 'Flutter Demo Home Page'),
  builder: (context, widget) {
    return Adaptation(
      child: widget,
      designWidth: 375, // 你的设计稿宽度
    );
  },
);

资源文件生成

assets_generator,用于自动生成 assets 配置 (yaml) 以及 consts 的工具,支持单项目和多模块。

-h, --[no-]help     显示帮助信息
-p, --path          Flutter 项目的根路径
                    (默认 ".")
-f, --folder        assets 文件夹的名字
                    (默认 "assets")
-w, --[no-]watch    是否继续监听 assets 的变化
                    (默认 开启)
-t, --type          pubsepec.yaml 生成配置的类型
                    "d" 代表以文件夹方式生成 "- assets/images/"
                    "f" 代表以文件方式生成   "- assets/images/xxx.jpg"
                    (默认 "d")
-s, --[no-]save     是否保存命令到本地
                    如果执行 "agen" 不带任何命令,将优先使用本地的命令进行执行
-o, --out           const 类放置的位置
                    (默认放置在 "lib" 下面)
-r, --rule          consts 的名字的命名规范
                    "lwu"(小写带下划线) : "assets_images_xxx_jpg"
                    "uwu"(大写带下划线) : "ASSETS_IMAGES_XXX_JPG"
                    "lcc"(小驼峰)      : "assetsImagesXxxJpg"
                    (默认 "lwu")
-c, --class         const 类的名字
                    (默认 "Assets")
    --const-ignore  使用正则表达式忽略一些const(不是全部const都希望生成)

assets_generator.gif

图片

ExtendedImage,集众多功能为一体的图片组件,包括以下主要功能:

  • 缓存网络图片
  • 加载状态(正在加载,完成,失败)
  • 拖拽缩放图片
  • 图片编辑(裁剪,旋转,翻转)
  • 图片预览(跟微信掘金一样)
  • 滑动退出效果(跟微信掘金一样)
  • 设置圆角,边框
  • 支持进度显示
  • 图片预览上滑显示详情(跟图虫一样)
  • 减少内存占用
zoom.gifslide.gifphoto_view.gif
editor.gif9e93eba3cc614ddbb818a3a445a41a99_tplv-k3u1fbpfcp-zoom-1 (1).gif2097626b7d0a406ab769cb528a31388e_tplv-k3u1fbpfcp-zoom-1.gif

列表扩展

ExtendedList,针对官方 ListviewGirdView 做的扩展组件,包括以下主要功能:

  • 监听元素回收
  • 监听 Viewport 中元素变化
  • 为最后一个元素设置特殊布局
  • 列表倒序特殊布局,类聊天列表
gridview.gifchat_list.gif

嵌套滚动视图扩展

ExtendedNestedScrollView,主要解决官方 NestedScrollView 存在的2个问题。

Sliver 扩展

ExtendedSliver,对 Sliver 组件的扩展,主要包括以下功能:

  • SliverPinnedPersistentHeader,跟官方的SliverPersistentHeader(pinned: true) 一样的效果, 不同的是你不需要去设置 minExtentmaxExtent。因为大部分场景下面,我们是无法提前知道 minExtentmaxExtent
  • SliverPinnedToBoxAdapter,可以通过它轻松创建一个置顶的元素,当 child 没有 layout 之前,你没法知道 child 的实际大小,这将是非常有用的组件。
  • ExtendedSliverAppbar,你可以创建一个跟 SliverAppbar 一样效果的组件,而不用去关心 expandedHeight

extended_sliver.gif

TabBarView 扩展

ExtendedTabs,对 TabBarView 组件的扩展,主要包括以下功能:

  • 解决多级 TabBarView 嵌套的时候,无法连贯切换的问题
  • 垂直方向滚动
  • 设置缓存页面数量
  • 提供 CarouselIndicatorColorTabIndicator
link.gifscrollDirection.gif

文本

ExtendedText,针对 Text 组件的扩展,主要包括以下功能:

  • 方便快速生成特殊文本,将字符串转换成特定的 InlineSpan
  • BackgroundTextSpan 自定文字背景,处理圆角或者中英文背景高度不一致的问题。
  • ExtendedWidgetSpan 支持选择和复制, github.com/flutter/flu… .
  • TextOverflowWidget 自定义文本溢出效果, github.com/flutter/flu…
special_text.jpgoverflow.jpg
background.pngselection.gif

输入框

ExtendedTextField,针对 TextField 组件的扩展,主要包括以下功能:

  • 方便快速生成特殊文本,原理很简单,就是把字符串转换成特定的 InlineSpan
  • ExtendedWidgetSpan 支持输入框中插入任何 Widget,比如表情图片。
  • ExtendedWidgetSpan 支持选择和复制, github.com/flutter/flu…
extended_text_field.gifextended_text_field_image.gif
custom_toolbar.gifwidget_span.gif

路由注解

ff_annotation_route,通过注解生成路由映射,统一处理路由,支持 Navigator 1.0Navigator 2.0

  1. 激活工具 pub global activate ff_annotation_route
  2. 增加引用
dependencies:
  # 在子模块中引入
  ff_annotation_route_core: any
  # 在根项目引入,包括一些帮助类以及 ff_annotation_route_core
  ff_annotation_route_library: any
  1. 添加注释

工具会自动处理带参数的构造,不需要做特殊处理。唯一需要注意的是,你需要设置 argumentImports 来为 class/enum 的参数提供 import 地址。现在你也可以使用 @FFArgumentImport() 注释来替代.

@FFArgumentImport('hide TestMode2')
import 'package:example1/src/model/test_model.dart';
@FFArgumentImport()
import 'package:example1/src/model/test_model1.dart' hide TestMode3;
import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';

@FFRoute(
  name: 'flutterCandies://testPageE',
  routeName: 'testPageE',
  description: 'Show how to push new page with arguments(class)',
  // argumentImports are still work for some cases which you can't use @FFArgumentImport()
  // argumentImports: <String>[
  //   'import \'package:example1/src/model/test_model.dart\';',
  //   'import \'package:example1/src/model/test_model1.dart\';',
  // ],
  exts: <String, dynamic>{
    'group': 'Complex',
    'order': 1,
  },
)
class TestPageE extends StatelessWidget {
  const TestPageE({
    this.testMode = const TestMode(
      id: 2,
      isTest: false,
    ),
    this.testMode1,
  });
  factory TestPageE.deafult() => TestPageE(
        testMode: TestMode.deafult(),
      );

  factory TestPageE.required({@required TestMode testMode}) => TestPageE(
        testMode: testMode,
      );

  final TestMode testMode;
  final TestMode1 testMode1;
}
  1. 执行命令生成路由,ff_route <command> [arguments],全部命令如下:
-h, --[no-]help                   帮助信息。

-p, --path                        执行命令的目录,默认当前目录。

-o, --output                      route 和 helper 文件的输出目录路径,路径相对于主项目的 lib 文件夹。

-n, --name                        路由常量类的名称,默认为 `Routes`。

-g, --git                         扫描 git 引用的 package,你需要指定 package 的名字,多个用 `,` 分开
    --routes-file-output          routes 文件的输出目录路径,路径相对于主项目的lib文件夹
    --const-ignore                使用正则表达式忽略一些const(不是全部const都希望生成)
    --[no-]route-constants        是否在根项目中的 `xxx_route.dart` 生成全部路由的静态常量
    --[no-]package                这个是否是一个 package
    --[no-]supper-arguments       是否生成路由参数帮助类

-s, --[no-]save                   是否保存命令到本地。如果保存了,下一次就只需要执行 `ff_route` 就可以了。
    --[no-]null-safety            是否支持空安全,默认 `true`
  1. 设置 MaterialApponGenerateRoute 回调
import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/material.dart';
import 'example_route.dart';
import 'example_routes.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ff_annotation_route demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: Routes.fluttercandiesMainpage,
      onGenerateRoute: (RouteSettings settings) {
        return onGenerateRoute(
          settings: settings,
          getRouteSettings: getRouteSettings,
          routeSettingsWrapper: (FFRouteSettings ffRouteSettings) {
            if (ffRouteSettings.name == Routes.fluttercandiesMainpage ||
                ffRouteSettings.name ==
                    Routes.fluttercandiesDemogrouppage.name) {
              return ffRouteSettings;
            }
            return ffRouteSettings.copyWith(
                widget: CommonWidget(
              child: ffRouteSettings.widget,
              title: ffRouteSettings.routeName,
            ));
          },
        );
      },
    );
  }
}
  1. 打开一个页面
  Navigator.pushNamed(
    context,
    Routes.flutterCandiesTestPageE.name,
    arguments: Routes.flutterCandiesTestPageE.requiredC(
      testMode: const TestMode(
        id: 100,
        isTest: true,
      ),
    ),
  );

可拖拽容器

DraggableContainer,可拖拽容器,支持元素移动动画效果,主要包括以下功能:

  • 可拖动子元素
  • 可删除子元素
  • 可固定子元素
  • 元素移动动画效果

图片编辑

ImageEditor,强大的原生图片处理库,主要包括以下功能:

  • 裁剪
  • 翻转
  • 旋转
  • 缩放
  • 色彩矩阵变化
  • 添加文字
  • 混合图片
  • 添加任意图形

editor.gif

Dialog

SmartDialog,一种更优雅的Dialog 解决方案,主要解决了系统自带的Dialog的一些问题:

  • 必须传 BuildContext。
  • 无法穿透暗色背景,点击 Dialog 后面的页面。
  • 解决系统自带 Dialog 写成的 Loading 弹窗,在网络请求和跳转页面的情况,会存在路由混乱的情况。

smartDialog

资源选择器

AssetPicker,对标微信的多选资源选择器,99%接近于原生微信的操作,主要包括以下功能:

  • ♻️ 支持基于代理重载的全量自定义
  • 💚 99% 的微信风格
  • 📷 图片资源支持
    • 🔬HEIC 格式图片支持
  • 🎥 视频资源支持
  • 🎶 音频资源支持
  • 1️⃣ 单资源模式
  • 💱 国际化支持
  • ➕ 特殊 widget 构建支持(前置/后置)
  • 🗂 自定义路径排序支持
  • 📝 自定义文本构建支持
  • ⏳ 自定义筛选规则支持( photo_manager
  • 🎏 完整的自定义主题
  • 💻 支持 MacOS
123
456
789

相机资源选择器

CameraPicker,对标微信的视频资源选择器,99%接近于原生微信的操作,主要包括以下功能:

  • 🔐 支持健全的空安全
  • 💚 99% 的微信风格
  • 📷 支持拍照
    • ☀️ 支持设置曝光参数
    • 🔍️ 支持捏合缩放
  • 🎥 支持录像
    • ⏱ 支持限制录像时间
    • 🔍 支持录像时缩放
  • 🖾 支持自定义前景 widget 构建

JsonToDart

JsonToDart,强大的 JsonToDart 工具,主要包括以下功能:

  • 空安全
  • 编辑类名,属性名
  • 去重复类
  • Merge 类属性
  • 数据数组保护
  • 属性命名规范化,只读,排序
  • 国际化
  • 全平台
  • 智能可空
平台描述地址
WindowsFlutter for Windowsgitee.com/zmtzawqlp/J…
MacosFlutter for Macosgitee.com/zmtzawqlp/J…
WebFlutter for Webzmtzawqlp.gitee.io/jsontodart/
微软商店功能未同步,以后会替换成 Flutter for UWPwww.microsoft.com/store/apps/…

JsonToDart.gif

点赞按钮

LikeButton,仿推特点赞效果,支持数字动画效果。

增量加载列表

LoadingMoreList,支持各种布局的增量加载列表,主要包括以下功能:

  • ListView
  • GridView
  • 瀑布流
  • 多个 Sliver 布局
  • 自定义加载状态 UI
  • 监控进入 Viewport 元素
  • 类聊天列表布局
  • 监控元素回收
listview.gifmultiple_sliver.giferror.gif
custom_indicator.gifnested_scrollView.gifknown_sized.gif

下拉刷新

PullToRefreshNotification,灵活的自定义下拉刷新组件,可以创造出任意的下拉刷新样式。

appbar.gifheader.gif
image.gifcandies.gif

底部扩散模糊动画

RippleBackdropAnimatePage,骚气十足的模糊动画,只需要几行代码就能帮你实现。

弹出菜单

WPopupMenu,目前最好用的仿微信聊天长按弹出框。

瀑布流

WaterfallFlow,高性能的瀑布流布局,github.com/flutter/flu…

  • 高性能
  • 易上手,跟 GridView 一样的 api
  • 可监控进入 Viewport 元素
  • 可监控元素回收
random_sized.gifcustom_scrollView.gif
known_sized.gifvariable_sized.gif

迁移

指南

感谢 Flutter & Dart 文档中国本地化 全球遮天团 为我们提供了完整准确的文档,dart.cn/null-safety…空安全 迁移大概有下面几个步骤:

  1. 执行flutter pub outdated --mode=null-safety ,检查自己项目依赖的库是否都支持空安全。

  2. 如果都支持了,执行 dart migrate --apply-changes。不加 --apply-changes 的话,会有一个浏览器地址,你打开之后,可以在浏览器中进行修改。我一般还是习惯在直接 --apply-changes 之后直接在 vscode 中进行修改。执行完毕之后,你的 Dart SDK 版本会自动改为大于2.12.0。(注意,执行 dart migrate 命令必须确保 SDK 是小于 2.12.0 的)

environment:
  sdk: '>=2.12.0 <3.0.0'
  1. 工具不是万能的,会有一些 错误,请先查看完 dart.cn/null-safety 之后,根据自己的业务场景对代码进行更正。

问题

空安全对非空 List<T> 的影响是非常大的。

不能对非空的列表设置更大的长度

List 的 length getter 也有一个对应的 setter,这一点鲜为人知。您可以对列表设置一个较短的长度,从而截断它。您也可以对列表设置一个更长的长度,从而使用未初始化的元素填充它。

如果您对一个非空的列表做了这样的操作,在访问未初始化的元素时,就与空安全的健全性发生了冲突。为了防止意外发生,现在对一个非空类型的数组调用 length setter, 并且 准备设置一个更长的长度时,会在运行时抛出一个异常。您仍然可以对任何类型的列表进行截断,也可以对一个可空类型的列表进行填充。

如果您自定义了列表的类型,例如继承了 ListBase 或者混入了 ListMixin,那么这项改动可能会造成较大的影响。以上的两种类型都提供了 insert() 的实现,通过设置长度,为插入的元素提供空间。在空安全中这样做可能会出现错误,所以我们将它们的 insert() 实现改为了 add()。现在您自定义的列表应该继承 add() 方法 方法。

下面我们来跟一波可空列表在做 add 操作时候的流程,来理解下文档所说的意思。

位置
list.dartbin/cache/dart-sdk/lib/collection/list.dart
growable_array.dartbin/cache/dart-sdk/lib/_internal/vm/lib/growable_array.dart
array.dartbin/cache/dart-sdk/lib/_internal/vm/lib/array.dart
  1. ListMixin.add (dart:collection/list.dart:278)
  // List interface.
  void add(E element) {
    // This implementation only works for lists which allow `null` 
    // as element.
    this[this.length++] = element;
  }
  1. List.length= (dart:core-patch/growable_array.dart:227)
  void set length(int new_length) {
    if (new_length > length) {
      // Verify that element type is nullable.
      // 官方在这里做了判断 
      null as T;
      if (new_length > _capacity) {
        _grow(new_length);
      }
      _setLength(new_length);
      return;
    }
    final int new_capacity = new_length;
  1. List._grow (dart:core-patch/growable_array.dart:362)
  void _grow(int new_capacity) {
    // 创建了一个长度为 new_capacity 的数组,并且用 null 填充
    var newData = _allocateData(new_capacity);
    // This is a work-around for dartbug.com/30090: array-bound-check
    // generalization causes excessive deoptimizations because it
    // hoists CheckArrayBound(i, ...) out of the loop below and turns it
    // into CheckArrayBound(length - 1, ...). Which deoptimizes
    // if length == 0. However the loop itself does not execute
    // if length == 0.
    // 从旧列表中复制数据
    if (length > 0) {
      for (int i = 0; i < length; i++) {
        newData[i] = this[i];
      }
    }
    // 通知引擎替换新数据
    _setData(newData);
  }
  1. List._allocateData (dart:core-patch/growable_array.dart:349) 以及 _List (dart:core-patch/array.dart.dart:13)
  static _List _allocateData(int capacity) {
    if (capacity == 0) {
      // Use shared empty list as backing.
      return _emptyList;
    }
    // Round up size to the next odd number, since this is free
    // because of alignment requirements of the GC.
    // (dart:core-patch/array.dart.dart:13)
    return new _List(capacity | 1);
  }
  1. 通知引擎更新新数据
 @pragma("vm:recognized", "graph-intrinsic")
  void _setData(_List array) native "GrowableList_setData";
  1. 通知引擎更新新长度
  @pragma("vm:recognized", "graph-intrinsic")
  void _setLength(int new_length) native "GrowableList_setLength";

image.png

第三步中,会返回元素为 null 的列表,所以在空安全的情况下,列表操作中需要做以下改动。

  • List<int> list = List<int>(); 改为 List<int> list = <int>[];

  • List<int> list = List<int>(1); 改为 List<int> list = List<int>.filled(1, 0);

  • 如果你继承了 ListBase 或者混入了 ListMixin,你需要重写 add() 方法,否则在第二步中就会报错。完整代码

  @override
  void add(T element) {
    _array.add(element);
  }

建议

坐和放宽

对于每次的大版本更新,不要着急升级,特别是你的项目引用了三方组件。三方开源作者是不大可能有时间立刻就更新,订阅一下作者的更新计划,静静等待。一般 stable 版本发布之后都会有热修复版本。如果你是新手,请坐和放宽,静待大佬们发现和解决点一些重大问题之后再更新。

image.png

学会使用 pub.dev

空安全的组件有很明显的标志 Null safety。打开 Versions 一签,通过 Min Dart SDK 很容易就看出组件是从哪个版本开始支持空安全的,比如 extended_image3.0.0 版本支持空安全。

image.png

另外,有些组件还提供了 Prerelease versions。比如 extended_image 还提供了非空安全版本. 当然 Prerelease versions 也可能是预览版,修复紧急问题(一些用户使用 Flutter master/dev/beta 分支,该分支可能会有一些 apibreaking change),作者会发布预览版来满足这部分人群。

image.png

每个人都从萌新而来,爱护萌新,但也不应该纵容巨婴。

  1. flutter.cn/dart.cn/ ,从入门到深入,各种资源应有尽有。如果你准备入手 Flutter,这应该是你必看的网站。

截屏2021-04-10 下午2.57.54.png

  1. github.com/flutter/flu… ,当遇到一些奇怪问题的时候,可以尝试搜索官方, 也许地球上某个地方的我也遇到了相同的问题。如果没搜到,并且确定是官方的问题,请不要吝啬你的时间,提交一个 issue ,让官方知晓,为其他人节约时间,提供思路。不要担心你的英文水平,只有多写,多练习,才能更好。

  2. www.google.comstackoverflow.com 程序猿都应该知道和会使用。

  3. juejin.cn 应该算是国内 Flutter 文章最多的一个网站了,对于英文不好的小伙伴,有中文的各种各样的 Flutter 相关文章也是极好的。

  4. qq群:181398081,如果通过上面的方式,你还是没法解决问题,你可以在 qq群 里面提问,很荣幸群里有一群热心的群友,互相帮助,互相学习。

  5. 最后,不管在哪里提问,尽量上代码,或者阐明清楚意图,因为也许想法或者解决方向从开始就是不正确的。

结语

2岁的糖果

不知不觉,糖果 已经 2岁 了,Flutter2.0 了。感谢 糖果 的小伙伴,对开源组件的持续支持。从 Flutter Candies 一桶天下 到现在又一年了,组织也在不断地壮大。欢迎更多的小伙伴都加入进来,一起为 Flutter 社区添砖加瓦。

截屏2021-04-09 下午5.58.22.png

致糖果们

2019年2月14日 孤单一个人,到现在的 2000 人,感谢每个糖果的支持,感谢积极回复问题的糖果们,感谢智能憨憨的群机器人。如果你喜欢分享,请加入我们;如果你需要别人的分享,也请加入我们。

image.png

爱Flutter爱糖果

很开心你能阅读到这里,爱Flutter爱糖果 ,欢迎加入 Flutter Candies ,一起生产可爱的 Flutter小糖果 QQ群:181398081。最后放上 Flutter Candies 全家桶,真香。