Flutter 空安全的糖果罐

时间:2021-7-9 作者:qvyue

theme: cyanosis

前言

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

组织发布的组件: https://pub.flutter-io.cn/publishers/fluttercandies.com/packages

组织支持空安全的进度: https://github.com/fluttercandies/flutter_candies/issues/5

介绍

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

屏幕适配

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都希望生成)
Flutter 空安全的糖果罐
assets_generator.gif

图片

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

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

列表扩展

Flutter 空安全的糖果罐
9e93eba3cc614ddbb818a3a445a41a99_tplv-k3u1fbpfcp-zoom-1 (1).gif

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

  • 监听元素回收
  • 监听 Viewport 中元素变化
  • 为最后一个元素设置特殊布局
  • 列表倒序特殊布局,类聊天列表
Flutter 空安全的糖果罐
gridview.gif
Flutter 空安全的糖果罐
chat_list.gif

嵌套滚动视图扩展

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

  • 解决 NestedScrollView 的 Header 中不能处理多个 pinnedtrue 的元素的问题。https://github.com/flutter/flutter/issues/22393
  • 解决 NestedScrollView 的 Body 中列表滚动会互相影响的问题。https://github.com/flutter/flutter/issues/21868

Sliver 扩展

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

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

TabBarView 扩展

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

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

文本

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

  • 方便快速生成特殊文本,将字符串转换成特定的 InlineSpan
  • BackgroundTextSpan 自定文字背景,处理圆角或者中英文背景高度不一致的问题。
  • ExtendedWidgetSpan 支持选择和复制, https://github.com/flutter/flutter/issues/38474 .
  • TextOverflowWidget 自定义文本溢出效果, https://github.com/flutter/flutter/issues/26748 。
Flutter 空安全的糖果罐
special_text.jpg
Flutter 空安全的糖果罐
overflow.jpg
Flutter 空安全的糖果罐
background.png
Flutter 空安全的糖果罐
selection.gif

输入框

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

  • 方便快速生成特殊文本,原理很简单,就是把字符串转换成特定的 InlineSpan
  • ExtendedWidgetSpan 支持输入框中插入任何 Widget,比如表情图片。
  • ExtendedWidgetSpan 支持选择和复制, https://github.com/flutter/flutter/issues/30688 。
Flutter 空安全的糖果罐
extended_text_field.gif
Flutter 空安全的糖果罐
extended_text_field_image.gif
Flutter 空安全的糖果罐
custom_toolbar.gif
Flutter 空安全的糖果罐
widget_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: [
  //   'import 'package:example1/src/model/test_model.dart';',
  //   'import 'package:example1/src/model/test_model1.dart';',
  // ],
  exts: {
    '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 [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,可拖拽容器,支持元素移动动画效果,主要包括以下功能:

  • 可拖动子元素
  • 可删除子元素
  • 可固定子元素
  • 元素移动动画效果
Flutter 空安全的糖果罐
image

图片编辑

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

  • 裁剪
  • 翻转
  • 旋转
  • 缩放
  • 色彩矩阵变化
  • 添加文字
  • 混合图片
  • 添加任意图形
Flutter 空安全的糖果罐
editor.gif

Dialog

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

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

资源选择器

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

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

相机资源选择器

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

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

JsonToDart

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

  • 空安全
  • 编辑类名,属性名
  • 去重复类
  • Merge 类属性
  • 数据数组保护
  • 属性命名规范化,只读,排序
  • 国际化
  • 全平台
  • 智能可空
平台 描述 地址
Windows Flutter for Windows https://gitee.com/zmtzawqlp/JsonToDart/releases/
Macos Flutter for Macos https://gitee.com/zmtzawqlp/JsonToDart/releases/
Web Flutter for Web https://zmtzawqlp.gitee.io/jsontodart/
微软商店 功能未同步,以后会替换成 Flutter for UWP https://www.microsoft.com/store/apps/9NBRW9451QSR
Flutter 空安全的糖果罐
JsonToDart.gif

点赞按钮

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

Flutter 空安全的糖果罐
image

增量加载列表

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

  • ListView
  • GridView
  • 瀑布流
  • 多个 Sliver 布局
  • 自定义加载状态 UI
  • 监控进入 Viewport 元素
  • 类聊天列表布局
  • 监控元素回收
Flutter 空安全的糖果罐
listview.gif
Flutter 空安全的糖果罐
multiple_sliver.gif
Flutter 空安全的糖果罐
error.gif
Flutter 空安全的糖果罐
custom_indicator.gif
Flutter 空安全的糖果罐
nested_scrollView.gif
Flutter 空安全的糖果罐
known_sized.gif

下拉刷新

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

Flutter 空安全的糖果罐
appbar.gif
Flutter 空安全的糖果罐
header.gif
Flutter 空安全的糖果罐
image.gif
Flutter 空安全的糖果罐
candies.gif

底部扩散模糊动画

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

Flutter 空安全的糖果罐
image

弹出菜单

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

Flutter 空安全的糖果罐
image

瀑布流

WaterfallFlow,高性能的瀑布流布局,https://github.com/flutter/flutter/issues/40856 。

  • 高性能
  • 易上手,跟 GridView 一样的 api
  • 可监控进入 Viewport 元素
  • 可监控元素回收
Flutter 空安全的糖果罐
random_sized.gif
Flutter 空安全的糖果罐
custom_scrollView.gif
Flutter 空安全的糖果罐
known_sized.gif
Flutter 空安全的糖果罐
variable_sized.gif

迁移

指南

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

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

问题

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

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

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

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

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

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

位置
list.dart bin/cache/dart-sdk/lib/collection/list.dart
growable_array.dart bin/cache/dart-sdk/lib/_internal/vm/lib/growable_array.dart
array.dart bin/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 
  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";
Flutter 空安全的糖果罐
image.png

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

  • List list = List(); 改为 List list = [];

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

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

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

建议

坐和放宽

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

Flutter 空安全的糖果罐
image.png

学会使用 pub.dev

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

Flutter 空安全的糖果罐
image.png

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

Flutter 空安全的糖果罐
image.png

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

  1. https://flutter.cn/ 和 https://dart.cn/ ,从入门到深入,各种资源应有尽有。如果你准备入手 Flutter,这应该是你必看的网站。
Flutter 空安全的糖果罐
截屏2021-04-10 下午2.57.54.png
  1. https://github.com/flutter/flutter/issues ,当遇到一些奇怪问题的时候,可以尝试搜索官方, 也许地球上某个地方的我也遇到了相同的问题。如果没搜到,并且确定是官方的问题,请不要吝啬你的时间,提交一个 issue ,让官方知晓,为其他人节约时间,提供思路。不要担心你的英文水平,只有多写,多练习,才能更好。

  2. https://www.google.com 和 https://stackoverflow.com 程序猿都应该知道和会使用。

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

  4. Flutter 空安全的糖果罐
    image

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

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

    Flutter 空安全的糖果罐
    image

结语

2岁的糖果

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

Flutter 空安全的糖果罐
截屏.png

致糖果们

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

Flutter 空安全的糖果罐
image.png

爱Flutter爱糖果

很开心你能阅读到这里,爱Flutter爱糖果 ,欢迎加入 Flutter Candies ,一起生产可爱的 Flutter小糖果

Flutter 空安全的糖果罐
image

QQ群:181398081。最后放上 Flutter Candies 全家桶,真香。

Flutter 空安全的糖果罐
flutter_candies_logo.png
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。