Flutter第一部分(UI)第六篇:一文搞懂Flutter中的资源引用机制
前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
Flutter
应用程序可以包括代码(code
)和资产(asset
),有时也将资产称为资源(resource
),由于大多数人比较习惯资源的概念,本文如果不加说明,都将asset
称为资源。 资源中包含的文件会与你的应用程序绑定在一起最后部署到宿主机上,可在软件运行期间访问这部分文件。 常见的资源类型包括静态数据(例如JSON
文件),配置文件,图标和图像(JPEG
,WebP
,GIF
,动画WebP / GIF
,PNG
,BMP
和WBMP
)等。
资源的注册(指定)
Flutter
使用位于项目根目录下的pubspec.yaml
文件来标识app
所需要的资源,如下所示:
flutter:
assets:
- assets/my_icon.png
- assets/background.png
上面的代码将assets
目录下的my_icon.png
和background.png
文件进行了注册(指定),这样我们就可以直接在代码中访问到这两个文件,有时我们可能有很多个资源文件需要在pubspec.yaml
文件中指定,这时如果像上面代码那样一个文件一个文件注册就显得有点麻烦,我们可以使用以/
字符结尾的格式将整个文件夹下的文件同时指定进来,就像下面这样:
flutter:
assets:
- assets/
注意,上面的代码会将assets
目录下的所有直接子文件都指定进来,但是assets
下的子文件夹下的文件(即间接子文件)是不会被指定进来的,需要对这些间接子文件或者子文件夹进行再次指定。
其实说白了,就是将代码中需要访问的资源文件在pubspec.yaml
文件中进行一个注册,这样就能通过pubspec.yaml
文件的注册信息定位到最终需要的文件进而实现访问,比如,假设现在文件的目录结构如下:
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.yaml
的assets
部分中指定资源的路径时,构建进程会查找相邻子目录中具有相同名称的所有文件,这些文件会与指定的资源一起打包进资源包中。
举个例子,假设你的项目的文件目录结构是这样的:
.../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.png
和assets/red/flutter_icon.png
文件都会被打包进你的资源包中,前者被认为是主版本,后者被认为是主版本的变体。
如果pubspec.yaml
文件使用如下的声明方法:
flutter:
assets:
- assets/
这样assets/icon.png
、assets/flutter_icon.png
和assets/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.png
,key
值就是'assets/flutter_icon.png'
。
下面我们来分别介绍一下如何具体地加载文本资源文件和图像/二进制文件。
加载文本资源
我们刚才提到,加载资源文件需要借助AssetBundle
类的对象,每一个Flutter app
的import '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
对象 - 第二种:使用
DefaultAssetBundle
和context
实例化自己的assetBundle
对象
那么我们在实际的生产中什么时候应该使用第一种、什么时候应该使用第二种呢?答案是一般情况下我们应该尽可能地使用第二种方法,但是如果我们需要在Widget
上下文之外加载资源,我们就无法获取当前环境的上下文context
,这个时候就只能使用第一种方法了。
加载图像
Flutter
可以根据当前设备的分辨率加载合适的图像。
声明分辨率感知(resolution-aware)图像资源
我们可以通过AssetImage
类将图像的加载请求自动映射到最接近当前设备像素比例的资源文件,当然这种做法的前提是使用特定的目录结构来保存图像资源,就像这样:
.../image.png
.../Mx/image.png
.../Nx/image.png
...etc.
其中M
和N
是数字标识符,代表对应文件夹下图像的分辨率等级,也就是说,它们指定了不同设备像素下应该加载的不同图片,主资源默认对应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.png
是72px
乘72px
,那么.../3.0x/my_icon.png
应该是216px
乘216px
, 但如果未指定宽度和高度,它们都将渲染为72px
乘72px
(px
以逻辑像素为单位)。
pubspec.yaml
中asset
部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择最接近自己标准的资源,比如如果需要1x
,但是没找到,那么会去2x
中找,2x
中还没有的话就在3x
中找,以此类推。
加载图片
要加载图片,我们应该在Widget
的build()
方法中使用 AssetImage
类。
例如:
Widget build(BuildContext context) {
// ...
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('assets/icon.png'),
// ...
),
// ...
),
);
// ...
}
使用默认的资源包加载资源时,内部会自动处理分辨率,这些处理对开发者来说是不可见的、自动的, 如果你使用一些更靠近底层的类,比如 ImageStream
或 ImageCache
, 你就需要自己配置一些与缩放相关的参数。
依赖包(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.yaml
的assets
部分中声明它:
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
lib/
是隐含的,所以它不应该包含在资源路径中。
与特定平台共享资源
通过Android
上的AssetManager
和iOS
上的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
中的lookupKeyForAsset
或lookupKeyForAsset: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
应用程序的启动图标与在原生Android
或iOS
应用程序中更新启动图标的方式相同。
Android
在Flutter
项目的根目录中,导航到.../android/app/src/main/res
。各个位图资源文件夹(如mipmap-hdpi
)已包含名为ic_launcher.png
的占位符图像。 只需遵守Android
官方文档中指示的不同屏幕像素密度推荐的图标大小原则,使用我们自己的图标替换掉ic_launcher.png
即可:
iOS
在你的Flutter
项目的根目录中,导航到.../ios/Runner
,在目录Assets.xcassets/AppIcon.appiconset
下已包含了占位符图像, 只需将它们替换为适当大小的图片即可:
更新启动页
当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.png
、aaa@qq.com
、aaa@qq.com
。 如果你使用了其他的文件名,那你还必须更新同一目录中的Contents.json
文件。
您也可以通过打开Xcode
完全自定义storyboard
。在Project Navigator
中导航到Runner/Runner
然后通过打开Assets.xcassets
拖入图片,或者通过在LaunchScreen.storyboard
中使用Interface Builder
进行自定义。