Zhengcaiyun Flutter dynamic iconfont practice exploration

Zhengcaiyun technical team.png

North 1.png

foreword

Dynamically modify iconfont? is it possible?

At present, more and more teams are starting to use Flutter to develop apps. In Flutter, we can easily use iconfont instead of pictures to display icons like the front end: put the iconfont font file in the project directory, in the Flutter code Use the icon in the file. However, this method is difficult to meet the needs of dynamically modifying the icon. If the product manager suddenly wants to replace the icon in the project (such as replacing the icon during festivals), we usually can only solve it by issuing the version, but the steps of issuing the version are cumbersome and are not suitable for the old version. invalid.

Next, let's explore a set of Flutter-based iconfont dynamic loading solutions.

iconfont principle

iconfont is "font icon", which is to make the icon into a font file, and then display different icons by specifying different characters. In the font file, each character corresponds to a Unicode code, and each Unicode code corresponds to a display glyph. Different fonts refer to different glyphs, that is, the glyphs corresponding to the characters are different. In iconfont, only the glyphs corresponding to the Unicode codes are made into icons, so different characters will eventually be rendered into different icons.

In Flutter development, iconfont has the following advantages over images:

  • Small size: The installation package size can be reduced.
  • Vector: iconfont are all vector icons, enlargement will not affect their clarity
  • Text styles can be applied: font icon color, size alignment, etc. can be changed just like text.

It is with the above advantages that we will give priority to using iconfonts instead of images in the Flutter project.

How iconfont was used before dynamization

在我们现有的 Flutter 项目中,关于 iconfont 的使用,都是通过 [icontfont 官网](https://www.iconfont.cn)下载 ttf 字体文件至项目中的 assets 文件夹下,然后在 pubsepc.yaml 文件中配置来实现 ttf 字体文件的静态加载。
fonts:
    - family: IconFont
      fonts:
        - asset: assets/fonts/iconfont.ttf
复制代码

Then define a class ( ZcyIcons ) to manage all IconData in the iconfont file:

The code of this class can be automatically generated by writing a script, so that every time the iconfont file is updated, the latest code can be generated only by executing the script.

class _MyIcon {
  static const font_name = 'iconfont';
  static const package_name = 'flutter_common';
  const _MyIcon(int codePoint) : super(codePoint, fontFamily: font_name, fontPackage: package_name,);
}

class ZcyIcons {
  static const IconData tongzhi = const _MyIcon(0xe784);
  
  static Map<String, IconData> _map = Map();
  
  ZcyIcons._();

  static IconData from(String name) {
    if(_map.isEmpty) {
      initialization();
    }
    return _map[name];
  }

  static void initialization() {
    _map["tongzhi"] = tongzhi;
  }
}

复制代码

When using, there are two ways to call

  /// 方法1:直接加载
  Icon(ZcyIcons.arrow)
  /// 方法2:通过name的值去取map中对应的IconData
  Icon(ZcyIcons.from(name))
复制代码

虽然第二种方法能通过改变 key 的值来动态的从 map 中加载对应的 IconData ,但是仅局限于所有的 IconData 都已经在 map 中配置好且不再更改。

既然 iconfont 是字体文件,那么如果系统能动态加载字体文件,那么一定也能用同样的方式去动态加载 iconfont。

iconfont 动态化方案

步骤1: 加载远程下发的 ttf 文件

Flutter SDK 提供了 FontLoader 类来实现字体的动态加载。而我们解决这个问题的核心就是这个 FontLoader 类。

它有一个 addFont 方法,支持将 ByteData 格式数据转化为字体包并加载到应用字体资源库:

class FontLoader{
  ...
  
  void addFont(Future<ByteData> bytes) {
    if (_loaded)
      throw StateError('FontLoader is already loaded');
    _fontFutures.add(bytes.then(
        (ByteData data) => Uint8List.view(data.buffer, data.offsetInBytes, data.lengthInBytes)
    ));
  }
  
  Future<void> load() async {
    if (_loaded)
      throw StateError('FontLoader is already loaded');
    _loaded = true;
    final Iterable<Future<void>> loadFutures = _fontFutures.map(
        (Future<Uint8List> f) => f.then<void>(
            (Uint8List list) => loadFont(list, family)
        )
    );
    return Future.wait(loadFutures.toList());
  }
}
复制代码

我们可以创建一个接口来下发 iconfont 字体文件远端地址及该文件的 hash 值,每次启动 APP 将本地字体文件的 hash 值与接口中的值对比,当存在差异时将远端的字体文件下载到本地并以 ByteData 的数据格式供 FontLoader 加载即可。附上部分关键代码:

/// 下载远端的字体文件
static Future<ByteData> httpFetchFontAndSaveToDevice(Uri fontUri) {
  return () async {
    http.Response response;
    try {
      response = await _httpClient.get(uri);
    } catch (e) {
      throw Exception('Failed to get font with url: ${fontUrl.path}');
    }
    if (response.statusCode == 200) {
      return ByteData.view(response.bodyBytes.buffer);
    } else {
      /// 如果执行失败, 抛出异常.
      throw Exception('Failed to download font with url: ${fontUrl.path}');
    }
  };
}

/// 加载字体,先从本地文件加载,如果不存在,则使用[loader]加载
static Future<void> loadFontIfNecessary(ByteData loader, String fontFamilyToLoad) async {
  assert(fontFamilyToLoad != null && loader != null);
  
  if (_loadedFonts.contains(fontFamilyToLoad)) {
    return;
  } else {
    _loadedFonts.add(fontFamilyToLoad);
  }
  
  try {
    Future<ByteData> byteData;
    byteData = file_io.loadFontFromDeviceFileSystem(fontFamilyToLoad);
    if (await byteData != null) {
      return _loadFontByteData(fontFamilyToLoad, byteData);
    }
    
    byteData = loader();
    if (await byteData != null) {
      /// 通过 FontLoader 加载下载好的字体文件
      final fontLoader = FontLoader(familyWithVariantString);
      fontLoader.addFont(byteData);
      await fontLoader.load();
      successLoadedFonts.add(familyWithVariantString);
    }
  } catch (e) {
    _loadedFonts.remove(fontFamilyToLoad);
    print('Error: unable to load font $fontFamilyToLoad because the following exception occured:\n$e');
  }
}
复制代码

步骤2: 通过 icon 的名称获取需要加载的 unicode 值

在实际使用时我们发现需要指定 icon 对应字体文件的 codePoint ,也就是 unicode 值:

代码中通过iconfont 的 unicde 值获取 icon 的用法如下:

/// StringToInt 方法是定义的将 "&#xe67b;" 从 String 类型的16进制值转为 int 类型方法
MyIcons.from(StringToInt('&#xe67b;'));
复制代码

这样的用法对于我们开发来说不是很友好,每次都需要去查找这个 unicde 值对应的是哪个图标,因此我们可以在之前下载 ttf 文件的接口创建一个映射关系表,然后在 iconfont 初始化的时候通过代码将动态下发的 icon 名称和 Unicode 进行关联。

接口返回数据格式:

更改接口格式后代码中 icon 的用法:

/// _aliasMap 是将接口下发的nameList保存起来的 Map
MyIcons.from(StringToInt(_aliasMap['tongzhi']);
复制代码

假设我们有这么一个场景:APP进入首页,下载最新的 iconfont.ttf 文件并加载,但是Icon已经加载完成,此时怎么做到动态刷新当前Icon里面的内容呢?

步骤3:动态加载异步优化

之前的步骤已经可以完成 APP 启动后本地字体文件的更新,但是无法解决 icon 已经加载完成后的数据更新,因此我们的动态化方案需要依赖于 FutureBuilder。

FutureBuilder 会依赖一个 Future,它会根据所依赖的 Future 的状态来动态构建自身。

我们可以扩展一个 Icon 的 dynamic 方法去返回一个依赖于 FutureBuilder 的 Icon,当我们的 iconfont 字体文件更新成功后让 FutureBuilder 强制去刷新这个 Icon。

主要代码如下:

/// Icon的扩展方法,主要实现Icon组件的动态刷新
/// [dynamic] 方法主要通过[FutureBuilder]实现动态加载的核心原理
extension DynamicIconExtension on Icon {
  /// 用来监听新icon字体加载成功后的回调及时刷新icon,
  Widget get dynamic {
    /// 没有使用动态iconfont的情况下直接返回
    if (this.icon is! DynamicIconDataMixin) return this;
    final mix = this.icon as DynamicIconDataMixin;
    final loadedKey = UniqueKey();
    return FutureBuilder(
      future: mix.dynamicIconFont.loadedAsync,
      builder: (context, snapshot) {
        /// 由于icon的配置未发生变化但实际上其使用的字体已经发生了变化,所以这里通过使用不同的key让其强制刷新
        return KeyedSubtree(
          key: snapshot.hasData ? loadedKey : null,
          child: this,
        );
      },
    );
  }

/// 调用代码如下:
Icon(MyIcons.from('&#xe67b;')).dynamic
复制代码

至此,我们的动态化方案支持的能力如下:

  • 可动态修改项目中已有的 icon
  • 通过 name/code 的形式动态设置 icon
  • 可在项目中使用新增的 icon

整个方案的流程图如下:

总结

总体来说,整个方案的核心原理就是通过 FontLoader 来实现字体文件的动态加载。但是其中涉及到一些动态化的处理和 iconfont 的原理探究,涉及到多点多面的知识,需要融会贯通并组合在一起使用。

参考资料

Flutter中文网

推荐阅读

政采云Flutter低成本屏幕适配方案探索

Redis系列之Bitmaps

MySQL 之 InnoDB 锁系统源码分析

招贤纳士

Zhengcaiyun technical team (Zero), a team full of passion, creativity and execution, Base is located in the picturesque Hangzhou. The team currently has more than 300 R&D partners, including "veteran" soldiers from Ali, Huawei, and NetEase, as well as newcomers from Zhejiang University, University of Science and Technology of China, Hangdian University and other schools. In addition to daily business development, the team also conducts technical exploration and practice in the fields of cloud native, blockchain, artificial intelligence, low-code platform, middleware, big data, material system, engineering platform, performance experience, visualization, etc. And landed a series of internal technology products, and continued to explore new boundaries of technology. In addition, the team has also devoted themselves to community building. Currently, they are contributors to many excellent open source communities such as google flutter, scikit-learn, Apache Dubbo, Apache Rocketmq, Apache Pulsar, CNCF Dapr, Apache DolphinScheduler, alibaba Seata, etc. If you want to change, you have been tossed with things, and you want to start tossing things; if you want to change, you have been told that you need more ideas, but you can't break the game; if you want to change, you have the ability to do that, but you don't need you; if you If you want to change what you want to accomplish, you need a team to support it, but there is no place for you to lead people; if you want to change, you have a good understanding, but there is always a blur of that layer of window paper... If you believe in the power of belief, I believe that ordinary people can achieve extraordinary things, and I believe that they can meet a better self. If you want to participate in the process of taking off with the business, and personally promote the growth of a technical team with in-depth business understanding, a sound technical system, technology creating value, and spillover influence, I think we should talk. Anytime, waiting for you to write something, send it to [email protected]

WeChat public account

The article is released simultaneously, the public account of the technical team of Zhengcaiyun, welcome to pay attention

政采云技术团队.png

Guess you like

Origin juejin.im/post/7084024140046270478