欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

程序员文章站 2022-05-30 22:21:46
...

前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。

Flutter应用程序可以包括代码(code)和资产(asset),有时也将资产称为资源(resource),由于大多数人比较习惯资源的概念,本文如果不加说明,都将asset称为资源。 资源中包含的文件会与你的应用程序绑定在一起最后部署到宿主机上,可在软件运行期间访问这部分文件。 常见的资源类型包括静态数据(例如JSON文件),配置文件图标和图像JPEGWebPGIF动画WebP / GIFPNGBMPWBMP)等。

资源的注册(指定)

Flutter使用位于项目根目录下的pubspec.yaml文件来标识app所需要的资源,如下所示:

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

上面的代码将assets目录下的my_icon.pngbackground.png文件进行了注册(指定),这样我们就可以直接在代码中访问到这两个文件,有时我们可能有很多个资源文件需要在pubspec.yaml文件中指定,这时如果像上面代码那样一个文件一个文件注册就显得有点麻烦,我们可以使用以/字符结尾的格式将整个文件夹下的文件同时指定进来,就像下面这样:

flutter:
  assets:
    - assets/

注意,上面的代码会将assets目录下的所有直接子文件都指定进来,但是assets下的子文件夹下的文件(即间接子文件)是不会被指定进来的,需要对这些间接子文件或者子文件夹进行再次指定。

其实说白了,就是将代码中需要访问的资源文件在pubspec.yaml文件中进行一个注册,这样就能通过pubspec.yaml文件的注册信息定位到最终需要的文件进而实现访问,比如,假设现在文件的目录结构如下:

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

pubspec.yaml文件的声明如下:

flutter:
   assets:
     - assets/

这样我们只能访问到assets的直接子文件flutter_blue.png,不能访问到assets的子目录child中的文件flutter_red.png,如果想要访问,可以将pubspec.yaml文件更新为:

flutter:
   assets:
     - assets/
     - assets/child/
     #或者:- assets/child/flutter_blue.png

这样既可以访问到文件flutter_blue.png了。

注意:在flutter项目的pubspec.yaml文件中指定资源时,assets节点下的目录名是没有限制的,这个实例中用的是assets,你也可以根据自己的需要使用其他目录,只要保证使用的目录位置再项目的根目录下即可,比如:

flutter:
   assets:
     - images/

将资源文件夹命名为images,这也是完全可以的。

资源的打包

pubspec.yaml文件中flutter节下的assets子节中指定的文件会包含在最终的app内(即一起被打包),每一个资源文件的位置都会以一个确定的路径(相对于pubspec.yaml的相对路径)来进行标识。

app的构建(build)期间,Flutter会将资源(asset)打包放入名为资源包(asset bundle的特殊存档中,应用程序可以在运行时读取这些存档

资源的自动版本管理

app的构建进程可以自动对资源进行版本管理:可以在不同的上下文中使用资源的不同版本。 在pubspec.yamlassets部分中指定资源的路径时,构建进程会查找相邻子目录中具有相同名称的所有文件,这些文件会与指定的资源一起打包进资源包中。

举个例子,假设你的项目的文件目录结构是这样的:

  .../pubspec.yaml
  .../assets/flutter_icon.png
  .../assets/red_icon/flutter_icon.
  .../assets/icon.png
  ...etc.

而你的pubspec.yaml文件中是这么声明的:

flutter:
  assets:
    - assets/flutter_icon.png

这样的话assets/flutter_icon.pngassets/red/flutter_icon.png文件都会被打包进你的资源包中,前者被认为是主版本,后者被认为是主版本的变体。

如果pubspec.yaml文件使用如下的声明方法:

flutter:
  assets:
    - assets/

这样assets/icon.pngassets/flutter_icon.pngassets/red/flutter_icon.png都会被包含进去。

Flutter会根据不同的屏幕分辨率选择合适的图像(不同的资源版本)进行使用,这一点在下面我会再进行介绍。将来,这种机制可能还会扩展到根据宿主机所处的不同地区使用不同版本的资源、根据屏幕的方向(横屏还是竖屏)选择不同版本的资源等其他场景。

加载资源

我们可以在app中使用AssetBundle类的对象来访问资源中的文件,具体的,根据加载资源的类型,可以将资源的加载分为两类:

  • 加载资源中的string/text文件(通过AssetBundle.loadString(String key)方法)
  • 加载资源中的图像/二进制文件(通过AssetBundle.load(String key)方法)

这两种方法的使用前提是需要提供的key值,这里的key值指的是pubspec.yaml文件中注册(指定)的资源的路径,比如在pubspec.yaml文件中声明了:

flutter:
  assets:
    - assets/flutter_icon.png

那么如果想加载flutter_icon.pngkey值就是'assets/flutter_icon.png'

下面我们来分别介绍一下如何具体地加载文本资源文件和图像/二进制文件。

加载文本资源

我们刚才提到,加载资源文件需要借助AssetBundle类的对象,每一个Flutter appimport 'package:flutter/services.dart'包下都提供有一个实例化好的AssetBundle rootBundle对象,我们可以通过该对象访问资源包中的主资源,比如:

class _MyAppState extends State<MyApp> {
  Image image;

  _loadImage() async {
    ByteData byteData = await rootBundle.load('assets/flutter_icon.png');
    setState(() {
      image = Image.memory(byteData.buffer.asUint8List());
    });
  }

  @override
  void initState() {
    _loadImage();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Assets  demo',
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('Assets demo'),
        ),
        body: Center(
          child: image,
        ),
      ),
    );
  }
}

这样就可以加载到我们需要的资源,但是请注意,通过Flutter app给我们实例化好的rootBundle对象加载的是默认的主资源,也就是说它是无法根据不同的上下文环境(比如不同的屏幕分辨率)加载不同版本的资源、它永远加载的都是默认的主资源中的资源,那么如何解决这个问题呢?

Flutter官方推荐的做法是通过DefaultAssetBundle类结合当前的BuildContext来实例化与当前上下文环境相关联的AssetBundle对象,这样不同的上下文环境会实例化到不同的AssetBundle对象,就能保证程序在加载资源的时候根据不同的上下文环境动态决定加载最适合版本对应的资源,具体的做法分为以下两步:

  • 实例化AssetBundle类的对象
  • 加载资源

Talk is cheap ,show you my code

class _MyAppState extends State<MyApp> {
  Image image;

  _loadImage(AssetBundle assetBundle) async {
    ByteData byteData = await assetBundle.load('assets/flutter_icon.png');
    image = Image.memory(byteData.buffer.asUint8List());
  }

  @override
  Widget build(BuildContext context) {
    //这里通过DefaultAssetBundle和当前的上下文环境context实例化AssetBundle
    AssetBundle assetBundle = DefaultAssetBundle.of(context);
    _loadImage(assetBundle);
    return MaterialApp(
      title: 'Assets  demo',
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text('Assets demo'),
        ),
        body: Center(
          child: image,
        ),
      ),
    );
  }
}

以上介绍了两种加载资源文件的方法:

  • 第一种:直接使用Flutter app为我们实例化好的rootBundle对象
  • 第二种:使用DefaultAssetBundlecontext实例化自己的assetBundle对象

那么我们在实际的生产中什么时候应该使用第一种、什么时候应该使用第二种呢?答案是一般情况下我们应该尽可能地使用第二种方法,但是如果我们需要在Widget上下文之外加载资源,我们就无法获取当前环境的上下文context,这个时候就只能使用第一种方法了。

加载图像

Flutter可以根据当前设备的分辨率加载合适的图像。

声明分辨率感知(resolution-aware)图像资源

我们可以通过AssetImage类将图像的加载请求自动映射到最接近当前设备像素比例的资源文件,当然这种做法的前提是使用特定的目录结构来保存图像资源,就像这样:

  .../image.png
  .../Mx/image.png
  .../Nx/image.png
  ...etc.

其中MN是数字标识符,代表对应文件夹下图像的分辨率等级,也就是说,它们指定了不同设备像素下应该加载的不同图片,主资源默认对应1.0倍的分辨率图片。比如,考虑如下名为my_icon.png的文件:

  .../my_icon.png
  .../2.0x/my_icon.png
  .../3.0x/my_icon.png

在设备像素比例为1.8的设备上,将会使用.../2.0x/my_icon.png 这个资源;

在像素比例为2.7的设备上,将会使用.../3.0x/my_icon.png这个资源,以此类推。

如果在使用Image Widget的时候没有指定渲染图像的宽度和高度,Flutter默认会使用标准分辨率来缩放图片资源,使其与主资源占用相同的屏幕空间。 也就是说,如果.../my_icon.png72px72px,那么.../3.0x/my_icon.png应该是216px216px, 但如果未指定宽度和高度,它们都将渲染为72px72pxpx以逻辑像素为单位)。

pubspec.yamlasset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择最接近自己标准的资源,比如如果需要1x,但是没找到,那么会去2x中找,2x中还没有的话就在3x中找,以此类推。

加载图片

要加载图片,我们应该在Widgetbuild()方法中使用 AssetImage类。

例如:

Widget build(BuildContext context) {
  // ...
  return new DecoratedBox(
    decoration: new BoxDecoration(
      image: new DecorationImage(
        image: new AssetImage('assets/icon.png'),
        // ...
      ),
      // ...
    ),
  );
  // ...
}

使用默认的资源包加载资源时,内部会自动处理分辨率,这些处理对开发者来说是不可见的、自动的, 如果你使用一些更靠近底层的类,比如 ImageStreamImageCache, 你就需要自己配置一些与缩放相关的参数。

依赖包(dependencies)中的资源图片

要加载依赖包(dependencies)中的图片,必须给AssetImage提供package参数。

比如,假设你的应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:

  .../pubspec.yaml
  .../icons/heart.png
  .../icons/1.5x/heart.png
  .../icons/2.0x/heart.png
  ...etc.

要加载该依赖包下的图像,你应该使用类似如下的代码:

 new AssetImage('icons/heart.png', package: 'my_icons')
打包 package assets

如果在pubspec.yaml文件中声明了期望的资源,它将会打包到相应的package中。特别是,包本身使用的资源必须在pubspec.yaml中指定。

包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml中指定包含哪些图像。 例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:

  .../lib/backgrounds/background1.png
  .../lib/backgrounds/background2.png
  .../lib/backgrounds/background3.png

要包含第一张图像,必须在pubspec.yamlassets部分中声明它:

flutter:
  assets:
    - packages/fancy_backgrounds/backgrounds/background1.png

lib/是隐含的,所以它不应该包含在资源路径中。

与特定平台共享资源

通过Android上的AssetManageriOS上的NSBundle可以在编写特定平台代码的时候获取到Flutter中的资源。

Android

Android中,可以通过AssetManager获得Flutter中的资源。具体的做法是使用AssetManager.getopenFd(String key);来获取对应资源,那么这个key应该是什么?答案是应该根据开发类型的不同来决定:

  • 如果开发的是Flutter插件,则应该使用 PluginRegistry.Registrar.lookupKeyForAsset(assetPath)获取
  • 如果开发的是普通的app,则应该使用FlutterView.getLookupKeyForAsset()获取

比如,假设你的pubspec.yaml文件中声明了如下代码:

flutter:
  assets:
    - icons/heart.png

而你app的包结构是这样的:

  .../pubspec.yaml
  .../icons/heart.png
  ...etc.

那么你可以使用类似如下Java代码访问到icons/heart.png

AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/heart.png");
AssetFileDescriptor fd = assetManager.openFd(key);

ios

iOS中,可以通过mainBundle获取资源。 用于pathForResource:ofType:key可以有两种过获取方式:

  • 通过FlutterPluginRegistrar中的lookupKeyForAssetlookupKeyForAsset:fromPackage:获取
  • 通过FlutterViewController中的lookupKeyForAsset:lookupKeyForAsset:fromPackage:获取

同样应该根据开发类型的不同选择不同的资源获取方式:

  • 开发插件时可以使用FlutterPluginRegistrar
  • 开发普通Flutter app时使用FlutterViewController

比如你可以使用类似如下Object-C代码来访问flutter项目中的 icons/heart.png

NSString* key = [registrar lookupKeyForAsset:@"icons/heart.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];

平台资源

还有时候可以直接在平台项目中使用资源,以下是在加载和运行Flutter框架之前使用资源的两种常见情况。

更新app 图标

更新Flutter应用程序的启动图标与在原生AndroidiOS应用程序中更新启动图标的方式相同。

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

Android

Flutter项目的根目录中,导航到.../android/app/src/main/res。各个位图资源文件夹(如mipmap-hdpi)已包含名为ic_launcher.png的占位符图像。 只需遵守Android官方文档中指示的不同屏幕像素密度推荐的图标大小原则,使用我们自己的图标替换掉ic_launcher.png 即可:

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

iOS

在你的Flutter项目的根目录中,导航到.../ios/Runner,在目录Assets.xcassets/AppIcon.appiconset下已包含了占位符图像, 只需将它们替换为适当大小的图片即可:

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

更新启动页

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制

Flutter框架进行加载时,Flutter会使用本机平台机制将app启动页面绘制到屏幕上,这个启动页面会一直展示,直到Flutter呈现应用程序的第一帧。

注意: 这意味着如果你不在应用程序的main()方法中调用runApp() 函数(底层原理其实是调用window.render()去响应window.onDrawFrame())的话, 你的app启动后会永远启只显示启动页面。

Android

要在Flutter应用程序中添加启动画面,请在.../android/app/src/main。 在res/drawable/launch_background.xml文件中自定义你的启动背景,这样就可以达到更改启动页面的目的。

iOS

要将图片添加到启动屏幕(splash screen)的中心,请导航至.../ios/Runner。在Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为images LaunchImage.pngaaa@qq.comaaa@qq.com。 如果你使用了其他的文件名,那你还必须更新同一目录中的Contents.json文件。

您也可以通过打开Xcode完全自定义storyboard。在Project Navigator中导航到Runner/Runner然后通过打开Assets.xcassets拖入图片,或者通过在LaunchScreen.storyboard中使用Interface Builder进行自定义。

Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制