在上一篇文章中,我與你介紹了 Flutter 的主題設置,也就是將視覺資源與視覺配置進行集中管理的機制。
Flutter 提供了遵循 Material Design 規范的 ThemeData,可以對樣式進行定制化:既可以初始化 App 時實現全局整體視覺風格統一,也可以在使用單子 Widget 容器 Theme 實現局部主題的覆蓋,還可以在自定義組件時取出主題對應的屬性值,實現視覺風格的復用。
一個應用程序主要由兩部分內容組成:代碼和資源。代碼關注邏輯功能,而如圖片、字符串、字體、配置文件等資源則關注視覺功能。如果說上一次文章更多的是從邏輯層面分享應該如何管理資源的配置,那今天的分享則會從物理存儲入手與你介紹 Flutter 整體的資源管理機制。
資源外部化,即把代碼與資源分離,是現代 UI 框架的主流設計理念。因為這樣不僅有利于單獨維護資源,還可以對特定設備提供更準確的兼容性支持,使得我們的應用程序可以自動根據實際運行環境來組織視覺功能,適應不同的屏幕大小和密度等。
隨著各類配置各異的終端設備越來越多,資源管理也越來越重要。那么今天,我們就先看看 Flutter 中的圖片、配置和字體的管理機制吧。
## 資源管理
在移動開發中,常見的資源類型包括 JSON 文件、配置文件、圖標、圖片以及字體文件等。它們都會被打包到 App 安裝包中,而 App 中的代碼可以在運行時訪問這些資源。
在 Android、iOS 平臺中,為了區分不同分辨率的手機設備,圖片和其他原始資源是區別對待的:
* iOS 使用 Images.xcassets 來管理圖片,其他的資源直接拖進工程項目即可;
* Android 的資源管理粒度則更為細致,使用以 drawable+ 分辨率命名的文件夾來分別存放不同分辨率的圖片,其他類型的資源也都有各自的存放方式,比如布局文件放在 res/layout 目錄下,資源描述文件放在 res/values 目錄下,原始文件放在 assets 目錄下等。
而在 Flutter 中,資源管理則簡單得多:資源(assets)可以是任意類型的文件,比如 JSON 配置文件或是字體文件等,而不僅僅是圖片。
而關于資源的存放位置,Flutter 并沒有像 Android 那樣預先定義資源的目錄結構,所以我們可以把資源存放在項目中的任意目錄下,只需要使用根目錄下的 pubspec.yaml 文件,對這些資源的所在位置進行顯式聲明就可以了,以幫助 Flutter 識別出這些資源。
而在指定路徑名的過程中,我們既可以對每一個文件進行挨個指定,也可以采用子目錄批量指定的方式。
接下來,**我以一個示例和你說明挨個指定和批量指定這兩種方式的區別。**
如下所示,我們將資源放入 assets 目錄下,其中,兩張圖片 background.jpg、loading.gif 與 JSON 文件 result.json 在 assets 根目錄,而另一張圖片 food\_icon.jpg 則在 assets 的子目錄 icons 下。
~~~
assets
├── background.jpg
├── icons
│ └── food_icon.jpg
├── loading.gif
└── result.json
~~~
對于上述資源文件存放的目錄結構,以下代碼分別演示了挨個指定和子目錄批量指定這兩種方式:通過單個文件聲明的,我們需要完整展開資源的相對路徑;而對于目錄批量指定的方式,只需要在目錄名后加路徑分隔符就可以了:
~~~
flutter:
assets:
- assets/background.jpg # 挨個指定資源路徑
- assets/loading.gif # 挨個指定資源路徑
- assets/result.json # 挨個指定資源路徑
- assets/icons/ # 子目錄批量指定
- assets/ # 根目錄也是可以批量指定的
~~~
需要注意的是,**目錄批量指定并不遞歸,只有在該目錄下的文件才可以被包括,如果下面還有子目錄的話,需要單獨聲明子目錄下的文件。**
完成資源的聲明后,我們就可以在代碼中訪問它們了。**在 Flutter 中,對不同類型的資源文件處理方式略有差異**,接下來我將分別與你介紹。
對于圖片類資源的訪問,我們可以使用 Image.asset 構造方法完成圖片資源的加載及顯示,在第 12 篇文章“[經典控件(一):文本、圖片和按鈕在 Flutter 中怎么用?](https://time.geekbang.org/column/article/110292)”中,你應該已經了解了具體的用法,這里我就不再贅述了。
而對于其他資源文件的加載,我們可以通過 Flutter 應用的主資源 Bundle 對象 rootBundle,來直接訪問。
對于字符串文件資源,我們使用 loadString 方法;而對于二進制文件資源,則通過 load 方法。
以下代碼演示了獲取 result.json 文件,并將其打印的過程:
~~~
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 適配不同的分辨率,我們需要將其他分辨率的圖片放到對應的分辨率子目錄中,如下所示:
~~~
rootBundle.loadString('assets/result.json').then((msg)=>print(msg));
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 中指定對應的字體即可:
~~~
Text("This is RobotoCondensed", style: TextStyle(
fontFamily: 'RobotoCondensed',// 普通字體
));
Text("This is RobotoCondensed", style: TextStyle(
fontFamily: 'RobotoCondensed',
fontWeight: FontWeight.w700, // 粗體
));
Text("This is RobotoCondensed italic", style: TextStyle(
fontFamily: 'RobotoCondensed',
fontStyle: FontStyle.italic, // 斜體
));
~~~
:-: 
圖 1 自定義字體
## 原生平臺的資源設置
在前面的第 5 篇文章“[從標準模板入手,體會 Flutter 代碼是如何運行在原生系統上的](https://time.geekbang.org/column/article/106199)”中,我與你介紹了 Flutter 應用,實際上最終會以原生工程的方式打包運行在 Android 和 iOS 平臺上,因此 Flutter 啟動時依賴的是原生 Android 和 iOS 的運行環境。
上面介紹的資源管理機制其實都是在 Flutter 應用內的,而在 Flutter 框架運行之前,我們是沒有辦法訪問這些資源的。Flutter 需要原生環境才能運行,但是有些資源我們需要在 Flutter 框架運行之前提前使用,比如要給應用添加圖標,或是希望在等待 Flutter 框架啟動時添加啟動圖,我們就需要在對應的原生工程中完成相應的配置,所以**下面介紹的操作步驟都是在原生系統中完成的。**
我們先看一下**如何更換 App 啟動圖標**。
對于 Android 平臺,啟動圖標位于根目錄 android/app/src/main/res/mipmap 下。我們只需要遵守對應的像素密度標準,保留原始圖標名稱,將圖標更換為目標資源即可:
:-: 
圖 2 更換 Android 啟動圖標
對于 iOS 平臺,啟動圖位于根目錄 ios/Runner/Assets.xcassets/AppIcon.appiconset 下。同樣地,我們只需要遵守對應的像素密度標準,將其替換為目標資源并保留原始圖標名稱即可:
:-: 
圖 3 更換 iOS 啟動圖標
然后。我們來看一下**如何更換啟動圖**。
對于 Android 平臺,啟動圖位于根目錄 android/app/src/main/res/drawable 下,是一個名為 launch\_background 的 XML 界面描述文件。
:-: 
圖 4 修改 Android 啟動圖描述文件
我們可以在這個界面描述文件中自定義啟動界面,也可以換一張啟動圖片。在下面的例子中,我們更換了一張居中顯示的啟動圖片:
~~~
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 白色背景 -->
<item android:drawable="@android:color/white" />
<item>
<!-- 內嵌一張居中展示的圖片 -->
<bitmap
android:gravity="center"
android:src="@mipmap/bitmap_launcher" />
</item>
</layer-list>
~~~
而對于 iOS 平臺,啟動圖位于根目錄 ios/Runner/Assets.xcassets/LaunchImage.imageset 下。我們保留原始啟動圖名稱,將圖片依次按照對應像素密度標準,更換為目標啟動圖即可。
:-: 
圖 5 更換 iOS 啟動圖
## 總結
好了,今天的分享就到這里。我們簡單回顧一下今天的內容。
將代碼與資源分離,不僅有助于單獨維護資源,還可以更精確地對特定設備提供兼容性支持。在 Flutter 中,資源可以是任意類型的文件,可以被放到任意目錄下,但需要通過 pubspec.yaml 文件將它們的路徑進行統一地顯式聲明。
Flutter 對圖片提供了基于像素密度的管理方式,我們需要將 1.0x,2.0x 與 3.0x 的資源分開管理,但只需要在 pubspec.yaml 中聲明一次。如果應用中缺少對于高像素密度設備的資源支持,Flutter 會進行自動降級。
對于字體這種基于字符圖形映射的資源文件,Flutter 提供了精細的管理機制,可以支持除了正常字體外,還支持粗體、斜體等樣式。
最后,由于 Flutter 啟動時依賴原生系統運行環境,因此我們還需要去原生工程中,設置相應的 App 啟動圖標和啟動圖。
## 思考題
最后,我給你留下兩道思考題吧。
1. 如果我們只提供了 1.0x 與 2.0x 的資源圖片,對于像素密度為 3.0 的設備,Flutter 會自動降級到哪套資源?
2. 如果我們只提供了 2.0x 的資源圖片,對于像素密度為 1.0 的設備,Flutter 會如何處理呢?
你可以參考原生平臺的經驗,在模擬器或真機上實驗一下。
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略