前言
一个应用程序主要由两部分内容组成:代码和资源。代码关注逻辑功能,而如图片、字符串、字体、配置文件等资源则关注视觉功能。
资源外部化,即把代码与资源分离,是现代 UI 框架的主流设计理念。因为这样不仅有利于单独维护资源,还可以对特定设备提供更准确的兼容性支持,使得我们的应用程序可以自动根据实际运行环境来组织视觉功能,适应不同的屏幕大小和密度等。
随着各类配置各异的终端设备越来越多,资源管理也越来越重要。今天就学习一下 Flutter 中的图片、配置和字体的管理机制。
资源管理
在移动开发中,常见的资源类型包括 JSON 文件、配置文件、图标、图片以及字体文件等。它们都会被打包到 App 安装包中,而 App 中的代码可以在运行时访问这些资源。
在 Android平台中,为了区分不同分辨率的手机设备,使用以 drawable 分辨率命名的文件夹来分别存放不同分辨率的图片,其他类型的资源也都有各自的存放方式,比如布局文件放在 res/layout 目录下,资源描述文件放在 res/values 目录下,原始文件放在 assets 目录下等。
而在 Flutter 中,资源管理则简单得多:资源(assets)可以是任意类型的文件,比如 JSON 配置文件或是字体文件等,而不仅仅是图片。·
而关于资源的存放位置,Flutter 并没有像 Android 那样预先定义资源的目录结构,所以我们可以把资源存放在项目中的任意目录下,只需要使用根目录下的 pubspec.yaml 文件,对这些资源的所在位置进行显示声明就可以了,以帮助 Flutter 识别出这些资源。
而在指定路径名的过程中,我们即可以对每一个文件进行挨个指定,也可以采用子目录批量指定的方式。
接下来,以一个示例说明挨个指定的和批量指定这两种方式的区别。
如下所示,我们将资源放入 assets 目录下,其中,两张图片 background.png、loading.gif 与 JSON 文件 result.json 在 assets 根目录,而另一张图片 food_icon.jpg 则在 assets 的子目录 icons 下。
通过单个文件声明的,我们需要完整展开资源的相对路径;而对于目录批量指定的方式,只需要在目录名后加路径分隔符就可以了:
flutter:
assets:
- assets/background.jpg # 挨个指定资源路径
- assets/loading.gif # 挨个指定资源路径
- assets/result.json # 挨个指定资源路径
- assets/icons/ # 子目录批量指定
- assets/ # 根目录也是可以批量指定的
需要注意的是,目录批量指定并不递归,只有在该目录下的文件才可以被包括,如果下面还有子目录的话,需要单独声明子目录下的文件。
完成资源的声明后,我们就可以在代码中访问它们了。但是,在 Flutter 中,对不同类型的资源文件处理方式略有差异。
对于图片类资源的访问,可以使用 Image.asset 构造方法完成图片资源的加载及显示,在 Flutter 经典控件学习(文本、图片、按钮) 一文中已经学习过了。
而对于其他资源文件的加载,我们可以通过 Flutter 应用的主资源 Bundle 对象 rootBundle,来直接访问。
对于字符串文件资源,使用 loadString 方法;而对于二进制文件资源,则通过 load 方法。
以下代码演示了获取 result.json 文件,并将其打印的过程:
// 使用 rootBundle 需要导入
import 'package:flutter/services.dart';
rootBundle.loadString('assets/result.json').then((msg) => print(msg));
打印结果如下:
与 Android、iOS 开发类似,Flutter 也遵循了基于像素密度的管理方式,如 1.0x、2.0x、3.0x 或其他任意倍数,Flutter 可以根据当前设备分辨率加载最接近设备像素比例的图片资源。而为了让 Flutter 更好地识别,我们的资源目录应该将 1.0x、2.0x 与 3.0x 的图片资源分开管理。
以 background.jpg 图片为例,这张图片位于 assets 目录下。如果想让 Flutter 适配不同的分辨率,我们需要将其他分辨率的图片放到对应的分辨率子目录中,如下所示:
assets
├── background.jpg //1.0x 图
├── 2.0x
│ └── background.jpg //2.0x 图
└── 3.0x
└── background.jpg //3.0x 图
而在 pubspec.yaml 文件声明这个图片资源时,仅声明 1.0x 图资源即可:
flutter:
assets:
- assets/background.jpg #1.0x 图资源
1.0x 分辨率的图片是资源标识符,而 Flutter 则会根据实际屏幕像素比例加载相应分辨率的图片。这时,如果主资源缺少某个分辨率资源,Flutter 会在剩余的分辨率资源中选择最接近的分辨率资源去加载。
举个例子,如果我们的 App 包只包括了 2.0x 资源,对于屏幕像素比为 3.0 的设备,则会自动降级读取 2.0x 的资源。不过需要注意的是,即使我们的 App 包没有包含 1.0x 资源,我们仍然需要像上面那样在 pubspec.yaml 中将它显示地声明出来,因为它是资源的标识符。
字体则是另外一类较为常用的资源。手机操作系统一般只有默认的几种字体,在大部分情况下可以满足我们的正常需求。但是,在一些特殊的情况下,我们可能需要使用自定义字体来提升视觉体验。
在 Flutter 中,使用自定义字体同样需要在 pubspec.yaml 文件中提前声明。需要注意的是,字体实际上是字符图形的映射。所以,除了正常字体文件外,如果你的应用需要支持粗体和斜体,同样也需要有对应的粗体和斜体字体文件。
在将 RobotoCondensed 字体摆放至 assets 目录下的 fonts 子目录后,下面的代码演示了如何将支持斜体与粗体的 RobotoCondensed 字体加到我们的应用中:
fonts:
- family: RobotoCondensed # 字体名字
fonts:
- asset: assets/fonts/RobotoCondensed-Regular.ttf # 普通字体
- asset: assets/fonts/RobotoCondensed-Italic.ttf
style: italic # 斜体
- asset: assets/fonts/RobotoCondensed-Bold.ttf
weight: 700 # 粗体
这些声明其实都对应着 TextStyle 中的样式属性,如字体名 family 对应着 fontFamily 属性、斜体 italic 与正常 normal 对应着 style 属性、字体粗细 weight 对应着 fontWeight 属性等。在使用时,我们只需要在 TextStyle 中指定对应的字体即可:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
// Row 控件,用来水平摆放子 Widget
body: Column(
children: <Widget>[
Text("This is RobotoCondensed",
style: TextStyle(
fontSize: 20,
fontFamily: 'RobotoCondensed', // 普通字体
)),
Text("This is RobotoCondensed",
style: TextStyle(
fontSize: 20,
fontFamily: 'RobotoCondensed',
fontWeight: FontWeight.w700, // 粗体
)),
Text("This is RobotoCondensed italic",
style: TextStyle(
fontSize: 20,
fontFamily: 'RobotoCondensed',
fontStyle: FontStyle.italic, // 斜体
)),
],
));
}
}
第三方库依赖管理
其实,除了管理这些资源外,pubspec.yaml 更为重要的作用是管理 Flutter 工程代码的依赖,比如第三方库、Dart 运行环境、Flutter SDK 版本都可以通过它来进行统一管理。所以,pubspec.yaml 与 iOS 中的 Podfile、Android 中的 build.gradle、前端的 package.json 在功能上是类似的。
Pub
Dart 提供了包管理工具 Pub,用来管理代码和资源。从本质上说,包(package)实际上就是一个包含了 pubspec.yaml 文件的目录,其内部可以包含代码、资源、脚本、测试和文档等文件。包中包含了需要被外部依赖的功能抽象,也可以依赖其他包。
与 Android 中的 JCenter/Maven、iOS 中的 CocoaPods、前端中的 npm 库类似,Dart 提供了官方的包仓库 Pub。通过 Pub,我们可以很方便地查找到有用的第三方包。
当然,这并不意味着我们可以简单地拿别人的库来拼凑成一个应用程序。Dart 提供包管理工具 Pub 的真正目的是,让你能够找到真正好用的、经过线上大量验证的库,复用他人的成果来缩短开发周期,提升软件质量,而不是重复造轮子。
在 Dart 中,库和应用都属于包。pubspec.yaml 是包的配置文件,包含了包的元数据(比如,包的名称和版本)、运行环境(也就是 Dart SDK 与 Fluter SDK 版本)、外部依赖、内部配置(比如,资源管理)。
看下面的配置,声明了一个 flutter_assets 的应用配置文件,其版本为 1.0,Dart 运行环境区间 2.1 至 3.0 之间,依赖 flutter 和 cupertino_icons:
# 应用名称
name: flutter_assets
# 应用描述
description: A new Flutter application.
# 应用版本
version: 1.0.0
# Dart 运行环境区间
environment:
sdk: ">=2.1.0 <3.0.0"
# Flutter 依赖库
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
对于包,我们通常是指定版本区间,而很少直接指定特定版本,因为包升级变化很频繁,如果有其他的包直接或间接依赖这个包的其他版本时,就会经常发生冲突。
而对于运行环境,如果是团队多人协作的工程,建议将 Dart 与 Flutter 的 SDK 环境写死,统一团队的开发环境,避免因为跨 SDK 版本出现的 API 差异进而导致工程问题。
比如,在上面的示例中,我们可以将 Dart SDK 写死为 2.3.0,Flutter SDK 写死为 1.2.1。
environment:
sdk: 2.3.0
flutter: 1.2.1
基于版本的方式引用第三方包,需要在其 Pub 上进行公开发布,我们可以访问 https://pub.dev/
来获取可用的第三方包。而对于不对外公开发布,或者目前处于开发调试阶段的包,我们需要设置数据源,使用本地路径或 Git 地址的方式进行包声明。
在下面的例子中,我们分别以路径依赖以及 Git 依赖的方式,声明了 package1 和 package2 这两个包:
dependencies:
package1:
path: ../package1/ # 路径依赖
date_format:
git:
url: https://github.com/xxx/package2.git #git 依赖
在开发应用时,我们可以不写明具体的版本号,而是以区间的方式声明包的依赖;但对于一个程序而言,其运行时具体引用哪个版本的依赖包必须要确定下来。因此,除了管理第三方依赖,包管理工具 Pub 的另一个职责是,找出一组同时满足每个包版本约束的包版本。包版本一旦确定,接下来就是下载对应版本的包了。
对于 dependencies 中的不同数据源,Dart 会使用不同的方式进行管理,最终会将远端的包全部下载到本地。比如,对于 Git 声明依赖的方式,Pub 会 clone Git 仓库;对于版本号的方式,Pub 则会从 pub.dartlang.org 下载包。
然后,Pub 会在应用的根目录下创建.packages 文件,将依赖的包名与系统缓存中的包文件路径进行映射,方便后续维护。
最后,Pub 会自动创建 pubspec.lock 文件。pubspec.lock 文件的作用类似 iOS 的 Podfile.lock 或前端的 package-lock.json 文件,用于记录当前状态下实际安装的各个直接依赖、间接依赖的包的具体来源和版本号。
比较活跃的第三方包的升级通常比较频繁,因此对于多人协作的 Flutter 应用来说,我们需要把 pubspec.lock 文件也一并提交到代码版本管理中,这样团队中的所有人在使用这个应用时安装的所有依赖都是完全一样的,以避免出现库函数找不到或者其他的依赖错误。
除了提供功能和代码维度的依赖之外,包还可以提供资源的依赖。在依赖包中的 pubspec.yaml 文件已经声明了同样资源的情况下,为节省应用程序安装包大小,我们需要复用依赖包中的资源。
例子
在 Flutter 中,提供了表达日期的数据结构DateTime
,这个类拥有极大的表示范围,可以表达 1970-01-01 UTC 时间后 100,000,000 天内的任意时刻。不过,如果我们想要格式化显示日期和时间,DateTime 并没有提供非常方便的方法,我们不得不自己取出年、月、日、时、分、秒,来定制显示方式。
值得庆幸的是,我们可以通过 date_format 这个第三方包来实现我们的诉求:date_format 提供了若干常用的日期格式化方法,可以很方便地实现格式化日期的功能。
首先,我们在 Pub 上找到 date_format 这个包,确定其使用说明:
date_format 包最新的版本是 1.0.6,于是接下来我们把 date_format 添加到 pubspec.yaml 中:
随后,IDE(Android Studio)监测到了配置文件的改动,提醒我们进行安装包依赖更新。于是,我们点击 Get dependencies,下载 date_format :
下载完成后,我们就可以在工程中使用 date_format 来进行日期的格式化了:
print(formatDate(DateTime.now(), [mm, '月', dd, '日', hh, ':', n]));
// 输出 2019 年 06 月 30 日 01:56
print(formatDate(DateTime.now(), [m, '月第', w, '周']));
// 输出 6 月第 5 周
总结
现代编程语言大都自带第依赖管理机制,其核心功能是为工程中所有直接或间接依赖的代码库找到合适的版本,但这并不容易。就比如前端的依赖管理器 npm 的早期版本,就曾因为不太合理的算法设计,导致计算依赖耗时过长,依赖文件夹也高速膨胀,一度被开发者们戏称为“黑洞”。而 Dart 使用的 Pub 依赖管理机制所采用的PubGrub
则解决了这些问题,因此被称为下一代版本依赖解决算法,在 2018 年底被苹果公司吸纳,成为 Swift 所采用的依赖管理器算法。
当然,如果工程里的依赖比较多,并且依赖关系比较复杂,即使再优秀的依赖解决算法也需要花费较长的时间才能计算出合适的依赖库版本。如果我们想减少依赖管理器为你寻找代码库依赖版本所耗费的时间,一个简单的做法就是从源头抓起,在 pubspec.yaml 文件中固定那些依赖关系复杂的第三方库们,及它们递归依赖的第三方库的版本号。