网易智造APP的Flutter资源管理工具:Flr(Flutter-R)

2,628 阅读7分钟

前言

网易智造APP是网易严选旗下的统一管理智能硬件产品的APP。从2019年9月起,为了进一步提升APP接入智能硬件产品的效率,我们团队开始使用Flutter技术来实现一些业务模块。在这个过程中,我们遇到的最大痛点之一就是:Flutter的资源管理方案原始、粗陋且低效,给我们开发添加了诸多不便——这些不便主要表现在以下几个问题上:

  • 需要手动创建、维护和管理资源目录以及资源
  • 需要手动为资源添加声明到pubspec.yaml
  • 在代码中只能通过字符串的方式应用资源

为此,我们研发了一个资源管理工具Flr(Flutter-R)来帮助团队在Flutter项目开发中更便捷地管理资源,更安全快捷地应用资源。

什么是Flr

Flr,全称:Flutter-R,是一个使用RubyGem开发的CLI工具,其提供了类似AAPT(Android Asset Packaging Tool)的功能,帮助Flutter开发者在修改了项目的资源后,自动为所有资源添加声明到 pubspec.yaml 以及生成R.dart文件。R.dart类似Android中的R.java,让Flutter开发者可以在代码中通过资源ID的方式应用资源。

截止到目前,Flr支持以下特性:

  • 支持“自动添加资源声明到 pubspec.yaml 和自动生成R.dart文件”的自动化服务,该服务可以通过手动触发,也可以通过监控资源变化触发
  • 支持处理图片资源( .png.jpg.jpeg.gif.webp.icon.bmp.wbmp.svg
  • 支持处理文本资源(.txt.json.yaml.xml
  • 支持处理图片资源变体
  • 支持处理带有“坏味道”的文件名的资源:
    • 文件名带有非法字符,如空格、~@# 等(非法字符是指不在合法字符集合内的字符;合法字符集合的字符有:0-9A-Za-z_$
    • 文件名以数字或者_或者$字符开头

以下是一个演示如何在Flutter项目中使用Flr的示例动图(更详细的使用介绍请看下面的用法介绍章节):

Flr Usage Example

如何安装和升级Flr

安装和升级Flr只需要在终端运行一句命令即可:sudo gem install flr

PS:对于使用Windows系统的Flutter开发者,强烈建议在 WSL(Windows Subsystem for Linux) 环境下安装和运行Flr

如何使用Flr

下面将会详细介绍如何在你的项目中使用Flr

1. 初始化Flutter项目

在Flutter项目根目录下,运行初始化命令:

cd flutter_project_dir
flr init

flr init命令会为当前项目创建一个Flrfile.yaml文件,以及在pubspec.yaml中添加 r_dart_library 依赖库的声明。其中,Flrfile.yaml文件用于供Flutter开发者配置需要Flr扫描的资源目录路径。

2. 编辑Flrfile.yaml

打开Flutter项目根目录下的Flrfile.yaml文件,然后按照文件内的提示进行编辑,配置需要Flr扫描的资源目录路径,如:

assets:

  # config the image asset directories that need to be scanned
  # supported image assets: [".png", ".jpg", ".jpeg", ".gif", ".webp", ".icon", ".bmp", ".wbmp", ".svg"]
  # config example: - lib/assets/images
  images:
    - lib/assets/images

  # config the text asset directories that need to be scanned
  # supported text assets: [".txt", ".json", ".yaml", ".xml"]
  # config example: - lib/assets/texts
  texts:
    - lib/assets/jsons
    - lib/assets/yamls

3. 触发自动化服务

Flr提供了2种方式供Flutter开发者触发“自动添加资源声明到 pubspec.yaml 和自动生成R.dart文件”的自动化服务:

  • 手动触发:运行flr generate命令
  • 监控资源变化触发:运行flr monitor命令

下面将会详细介绍这两者。

方式一:手动触发自动化服务(flr generate

手动触发自动化服务,通过运行flr generate命令实现。

flr generate命令运行后,flr generate命令会对Flrfile.yaml中配置的资源目录进行一次资源扫描,然后为扫描到的资源添加声明到pubspec.yaml,并生成R.dart文件。

方式二:监控资源变化触发自动化服务 (flr monitor

监控资源变化触发自动化服务,通过运行flr monitor命令实现。

flr monitor命令运行后,flr monitor命令会启动一个持续监控资源变化的监控服务。该监控服务会对Flrfile.yaml中配置的资源目录进行监控,若发现这些目录下的资源有变化,就会自动执行flr generate命令。而flr generate命令,如上所述,会根据配置进行一次资源扫描,然后为扫描到的资源添加声明到pubspec.yaml,并生成R.dart文件。

若你希望终止当前的监控服务,手动输入Ctrl-C即可终止。

需要注意的是,若监控期间你修改了Flrfile.yaml中的配置,你应该终止当前的监控服务,并重新运行flr monitor以启动新的监控服务。

关于R.dart

在你运行flr generate或者flr monitor命令后,Flr会根据Flrfile.yaml中的资源目录配置,自动扫描资源,并为扫描到的资源添加声明到pubspec.yaml,以及生成R.dart——R.dart类似Android中的R.java,让Flutter开发者可以在代码中通过资源ID的方式快捷地和安全地应用资源,如下面示例:

import 'package:flutter_r_demo/R.dart';

// sameName.png
var normalImageWidget = Image(
  width: 113,
  height: 128,
  image: R_Image.sameName,
);

// sameName.gif
var gifImageWidget = Image(
  image: R_Image.sameName_gif,
);

// ?test$.svg
var svgImageWidget = Image(
  width: 100,
  height: 100,
  image: R_Svg.a?test$(width: 100, height: 100),
);

// $%^&test.json
var jsonString = await R_Text.a$___test_json();

// ~!@*test.yaml
var yamlString = await R_Text.a____test_yaml();

资源ID

资源ID根据资源的文件名生成。

一般而言,资源ID就是该资源的文件名,如图片资源btn_cycle_down_80_N.png的ID为btn_cycle_down_80_N

不过,在实际开发中,总会遇到文件名存在“坏味道”的资源:资源的文件名带有非法字符或者以大写字母开头或者以数字、_$字符开头——若不做对应处理,这些会导致生成的资源ID不合法,从而导致编译失败。

为避免以上问题,必须保证生成的资源ID是合法的。为此,在根据资源的文件名生成资源ID时会按照以下顺序做检测和修复处理:

  • 检测文件名是否带有非法字符;若有,则把非法字符会转换为_字符,如资源icon.ignoreme.png的资源ID为icon_ignoreme
  • 检测文件名是否以大写字母开头;若是,则把首字母转换为对应的小写字母,如资源User@white.png的资源ID为user_white
  • 检测文件名是否以数字、_$字符开头;若是,则添加前缀字符a,如资源~!$test.png的资源ID为a__$test

资源管理类

R.dart中定义了R和几个R_X资源管理类:R_ImageR_SvgR_Text——这些R_X资源管理类用于定义和管理各自资源类型的资源ID:

  • R:用于定义和管理下面R_X类所需的公用信息,如当前工程的包名

    import 'package:flutter/widgets.dart';
    import 'package:flutter/services.dart' show rootBundle;
    import 'package:flutter_svg/flutter_svg.dart';
    import 'package:r_dart_library/asset_svg.dart';
    
    /// This `R` class is generated and contains references to static resources.
    class R {
      /// package name: flutter_r_demo
      static const package = "flutter_r_demo";
    }
    

    由于Dart不支持内嵌类(nested class)的特性,所以无法实现R.X的用法,只能退而求其次,通过定义R_X类来提供类似的使用体验。

  • R_Image:用于定义和管理所有非SVG类型的图片资源的ID,资源ID的类型为AssetImage静态变量

    class R_Image {
      
      /// asset: "assets/images/sameName.png"
      // ignore: non_constant_identifier_names
      static const sameName = AssetImage("assets/images/sameName.png", package: R.package);
     
      /// asset: "assets/images/sameName.gif"
      // ignore: non_constant_identifier_names
      static const sameName_gif = AssetImage("assets/images/sameName.gif", package: R.package);
    
      /// asset: "assets/images/sameName.jpg"
      // ignore: non_constant_identifier_names
      static const sameName_jpg = AssetImage("assets/images/sameName.jpg", package: R.package);
     
     }
    
  • R_Svg:用于定义和管理SVG类型的图片资源的ID,资源ID的类型为AssetSvg静态函数

    class R_Svg {
    
      /// asset: assets/images/?test$.svg
      // ignore: non_constant_identifier_names
      static AssetSvg a?test$({@required double width, @required double height}) {
        var assetFullPath = "packages/flutter_r_demo/assets/images/\$\$test\$.svg";
        var imageProvider = AssetSvg(assetFullPath, width: width, height: height);
        return imageProvider;
      }
    
    }
    
  • R_Text:用于定义和管理所有文本资源的ID,资源ID的类型为Future<String>静态函数

    class R_Text {
      
      /// asset: assets/jsons/$%^&test.json
      // ignore: non_constant_identifier_names
      static Future<String> a$___test_json() {
        var assetFullPath = "packages/flutter_r_demo/assets/jsons/\$%^&test.json";
        var str = rootBundle.loadString(assetFullPath);
        return str;
      }
    
      /// asset: assets/yamls/~!@*test.yaml
      // ignore: non_constant_identifier_names
      static Future<String> a____test_yaml() {
        var assetFullPath = "packages/flutter_r_demo/assets/yamls/~!@*test.yaml";
        var str = rootBundle.loadString(assetFullPath);
        return str;
      }
    
    }
    

详细例子

若你希望进一步了解如何在Flutter项目中使用Flr工具和在代码中如何使用R.dart,可点击查看这个例子:Flutter-R Demo

TODO

Flr目前可以满足大部分开发需求,但是仍然有很大提升空间,包括:

  • 版本迭代更新

    主要是持续支持和适配Flutter、Dart的新版本,以及根据Flutter开发者的需求增加新特性

  • 资源损坏检测

    主要是检测当前是否存在损坏的图片资源、文本资源。

  • 资源声明的合法性检测和修复(若能修复)

    主要是检测当前资源的声明是否符合YAML的规范,如这个文本资源#test.yamlpubspec.yaml中的声明是- packages/flutter_r_demo/assets/yamls/#test.yaml,然而由于其名称带有YAML的注释字符#,导致Flutter编译时会报以下错误:

    Error detected in pubspec.yaml:
    No file or variants found for asset: packages/flutter_r_demo/assets/yamls/#test.yaml.
    
  • IDE插件支持

    主要是研发FlrAndroid Studio插件版本和VS Code插件版本。

  • Asset Catalog功能支持

    主要是研发提供类似XcodeAsset Catalog的资源管理功能:其通过可视化的方式,让Flutter开发者可以方便地以设备特征(traits)为单位配置资源,包括但不限于图片,颜色,材质,数据。

    Xcode Asset Catalog

以上,若你有兴趣,欢迎加入我们团队一起完善它。