2.6 资源管理
📚 核心知识点
- 什么是资源(Assets)
- 在 pubspec.yaml 中指定资源
- 加载图片资源
- 加载文本/JSON 资源
- 资源变体(分辨率适配)
- 特定平台资源(APP图标、启动页)
💡 核心概念
什么是资源(Assets)?
Flutter 应用包含两部分:
- 代码 - Dart 代码编译后的内容
- 资源(Assets) - 打包到安装包中的静态文件
常见的资源类型:
- 📷 图片(PNG, JPG, GIF, WebP)
- 📝 文本文件(TXT, JSON, XML)
- 🎵 音频(MP3, WAV)
- 🎬 视频(MP4)
- 🔤 字体(TTF, OTF)
📝 在 pubspec.yaml 中指定资源
基本语法
flutter:
assets:
- images/avatar.png # 单个文件
- images/ # 整个目录
- assets/data/config.json # 指定路径
指定方式对比
| 方式 | 语法 | 说明 |
|---|---|---|
| 单个文件 | - images/avatar.png | 只包含这一个文件 |
| 整个目录 | - images/ | 包含目录下所有文件 |
| 子目录 | - assets/data/ | 包含子目录下所有文件 |
目录结构示例
project_root/
├── lib/
├── images/
│ ├── avatar.png
│ ├── logo.png
│ └── background.jpg
├── assets/
│ ├── data/
│ │ ├── config.json
│ │ └── users.json
│ └── icons/
│ └── custom_icon.png
└── pubspec.yaml
对应的 pubspec.yaml 配置:
flutter:
assets:
- images/ # 包含 images 目录下所有文件
- assets/data/ # 包含 data 目录下所有文件
- assets/icons/ # 包含 icons 目录下所有文件
🖼️ 加载图片资源
方法1:Image.asset()(推荐)
Image.asset(
'images/avatar.png',
width: 100,
height: 100,
fit: BoxFit.cover, // 填充方式
)
方法2:AssetImage
DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/background.png'),
fit: BoxFit.cover,
),
),
)
图片加载参数
| 参数 | 说明 | 示例 |
|---|---|---|
width | 宽度 | width: 100 |
height | 高度 | height: 100 |
fit | 填充方式 | BoxFit.cover |
color | 颜色滤镜 | color: Colors.blue |
colorBlendMode | 混合模式 | BlendMode.multiply |
BoxFit 填充模式
// 覆盖(可能裁剪)
Image.asset('image.png', fit: BoxFit.cover)
// 包含(可能留白)
Image.asset('image.png', fit: BoxFit.contain)
// 填充(可能变形)
Image.asset('image.png', fit: BoxFit.fill)
// 原始尺寸
Image.asset('image.png', fit: BoxFit.none)
错误处理
Image.asset(
'images/avatar.png',
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 100,
color: Colors.grey[300],
child: const Icon(Icons.error, color: Colors.red),
);
},
)
📄 加载文本/JSON 资源
步骤1:添加资源到 pubspec.yaml
flutter:
assets:
- assets/data/config.json
步骤2:加载文本内容
import 'package:flutter/services.dart' show rootBundle;
// 加载文本文件
String loadText() async {
return await rootBundle.loadString('assets/data/config.txt');
}
步骤3:解析 JSON
import 'dart:convert';
import 'package:flutter/services.dart';
Future<Map<String, dynamic>> loadConfig() async {
// 1. 加载文件内容
final String jsonString = await rootBundle.loadString('assets/data/config.json');
// 2. 解析 JSON
final Map<String, dynamic> data = json.decode(jsonString);
return data;
}
完整示例
class JsonLoaderWidget extends StatefulWidget {
@override
State<JsonLoaderWidget> createState() => _JsonLoaderWidgetState();
}
class _JsonLoaderWidgetState extends State<JsonLoaderWidget> {
Map<String, dynamic>? _data;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
try {
final jsonString = await rootBundle.loadString('assets/data/config.json');
final jsonData = json.decode(jsonString);
setState(() {
_data = jsonData;
_isLoading = false;
});
} catch (e) {
print('加载失败: $e');
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return CircularProgressIndicator();
}
if (_data == null) {
return Text('加载失败');
}
return Column(
children: [
Text('应用名: ${_data!['app_name']}'),
Text('版本: ${_data!['version']}'),
],
);
}
}
🎯 资源变体(分辨率适配)
什么是资源变体?
Flutter 支持为不同屏幕密度提供不同分辨率的图片。
目录结构
images/
├── avatar.png # 1.0x (基准)
├── 2.0x/
│ └── avatar.png # 2.0x (高分辨率)
└── 3.0x/
└── avatar.png # 3.0x (超高分辨率)
pubspec.yaml 配置
flutter:
assets:
- images/avatar.png # 只需指定基准图片
Flutter 会自动查找对应分辨率的图片!
分辨率对应关系
flowchart TB
A["设备屏幕密度"]
B1["1.0 DPR<br/>(普通屏幕)"]
B2["2.0 DPR<br/>(Retina屏幕)"]
B3["3.0 DPR<br/>(超高清屏幕)"]
C1["images/avatar.png"]
C2["images/2.0x/avatar.png"]
C3["images/3.0x/avatar.png"]
A --> B1
A --> B2
A --> B3
B1 --> C1
B2 --> C2
B3 --> C3
style C1 fill:#E3F2FD
style C2 fill:#C8E6C9
style C3 fill:#FFF9C4
使用方式
// 代码不变,Flutter 自动选择合适的分辨率
Image.asset('images/avatar.png')
Flutter 会根据设备的 devicePixelRatio 自动选择:
- DPR = 1.0 → 使用
images/avatar.png - DPR = 2.0 → 使用
images/2.0x/avatar.png - DPR = 3.0 → 使用
images/3.0x/avatar.png
最佳实践
- 至少提供 1x 和 2x 图片
- 图片尺寸建议:
- 1x: 100×100
- 2x: 200×200
- 3x: 300×300
- 如果缺少对应分辨率,Flutter 会自动缩放
📱 特定平台资源
1. 设置 APP 图标
Android
路径: android/app/src/main/res/
目录结构:
res/
├── mipmap-hdpi/
│ └── ic_launcher.png # 72×72
├── mipmap-mdpi/
│ └── ic_launcher.png # 48×48
├── mipmap-xhdpi/
│ └── ic_launcher.png # 96×96
├── mipmap-xxhdpi/
│ └── ic_launcher.png # 144×144
└── mipmap-xxxhdpi/
└── ic_launcher.png # 192×192
修改 AndroidManifest.xml:
<application
android:icon="@mipmap/ic_launcher"
...>
iOS
路径: ios/Runner/Assets.xcassets/AppIcon.appiconset/
需要的尺寸:
- 20×20 (1x, 2x, 3x)
- 29×29 (1x, 2x, 3x)
- 40×40 (1x, 2x, 3x)
- 60×60 (2x, 3x)
- 76×76 (1x, 2x)
- 83.5×83.5 (2x)
- 1024×1024 (1x)
2. 设置启动页(Splash Screen)
Android
路径: android/app/src/main/res/drawable/launch_background.xml
<?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/launch_image" />
</item>
</layer-list>
iOS
路径: ios/Runner/Assets.xcassets/LaunchImage.imageset/
文件:
- LaunchImage.png (1x)
- LaunchImage@2x.png (2x)
- LaunchImage@3x.png (3x)
或使用 Storyboard 自定义:
- 打开 Xcode
- 编辑
LaunchScreen.storyboard
📦 依赖包中的资源
使用包中的图片
// 需要指定 package 参数
Image.asset(
'icons/heart.png',
package: 'my_icons', // 包名
)
包的目录结构
my_icons/
├── lib/
└── icons/
├── heart.png
├── 2.0x/
│ └── heart.png
└── 3.0x/
└── heart.png
包的 pubspec.yaml
flutter:
assets:
- icons/heart.png # 包也需要声明资源
🎯 资源加载流程
flowchart TB
Start["代码请求资源"]
A1["查找 pubspec.yaml"]
A2{"资源已声明?"}
B1["查找文件系统"]
B2{"文件存在?"}
C1["检查设备 DPR"]
C2["选择合适的分辨率"]
D1["加载资源"]
D2["返回资源对象"]
E1["❌ 抛出异常"]
Start --> A1
A1 --> A2
A2 -->|"✅ 是"| B1
A2 -->|"❌ 否"| E1
B1 --> B2
B2 -->|"✅ 是"| C1
B2 -->|"❌ 否"| E1
C1 --> C2
C2 --> D1
D1 --> D2
style D2 fill:#C8E6C9
style E1 fill:#FFCDD2
📝 常见问题
Q1: 为什么图片加载不出来?
可能原因:
- 未在 pubspec.yaml 中声明
# ❌ 忘记添加
flutter:
uses-material-design: true
# ✅ 正确添加
flutter:
uses-material-design: true
assets:
- images/avatar.png
- 路径错误
// ❌ 错误:多了斜杠
Image.asset('/images/avatar.png')
// ✅ 正确
Image.asset('images/avatar.png')
- 缩进错误
# ❌ 错误:缩进不对
flutter:
assets:
- images/
# ✅ 正确:assets 需要缩进
flutter:
assets:
- images/
- 未运行 flutter pub get
# 修改 pubspec.yaml 后需要运行
flutter pub get
Q2: 如何判断资源是否成功加载?
方法1:使用 errorBuilder
Image.asset(
'images/avatar.png',
errorBuilder: (context, error, stackTrace) {
print('图片加载失败: $error');
return Icon(Icons.error);
},
)
方法2:使用 try-catch
try {
final data = await rootBundle.loadString('assets/data.json');
print('加载成功');
} catch (e) {
print('加载失败: $e');
}
Q3: assets 和 images 目录有什么区别?
没有本质区别! 只是命名习惯:
images/- 通常放图片assets/- 通常放其他资源(JSON, 音频等)
两者都需要在 pubspec.yaml 中声明。
Q4: 如何减小资源文件大小?
-
压缩图片
- 使用 TinyPNG 等工具
- 选择合适的格式(WebP > PNG > JPG)
-
按需加载
# ❌ 不要一次性加载所有
assets:
- assets/
# ✅ 只加载需要的
assets:
- assets/data/config.json
- assets/images/logo.png
- 使用矢量图标
// 使用 Icons 而不是图片
Icon(Icons.favorite) // 很小,可缩放
Q5: 资源文件在运行时可以修改吗?
不可以!
资源文件是只读的,打包到安装包中。
如果需要运行时修改,应该使用:
- 文件系统(path_provider 包)
- 数据库(sqflite 包)
- SharedPreferences(shared_preferences 包)
🎓 跟着做练习
练习1:加载并显示多张图片 ⭐⭐
目标: 创建一个图片画廊
步骤:
-
准备3张图片放到
images/目录 -
在 pubspec.yaml 中添加:
flutter:
assets:
- images/
- 创建图片列表:
class ImageGallery extends StatelessWidget {
final List<String> images = [
'images/photo1.png',
'images/photo2.png',
'images/photo3.png',
];
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 2列
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: images.length,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
images[index],
fit: BoxFit.cover,
),
);
},
);
}
}
练习2:创建配置管理器 ⭐⭐⭐
目标: 从 JSON 文件加载应用配置
步骤:
- 创建
assets/config/app_config.json:
{
"api_url": "https://api.example.com",
"timeout": 30,
"enable_logs": true,
"theme": "dark"
}
- 创建配置管理类:
class AppConfig {
final String apiUrl;
final int timeout;
final bool enableLogs;
final String theme;
AppConfig({
required this.apiUrl,
required this.timeout,
required this.enableLogs,
required this.theme,
});
// 从 JSON 创建对象
factory AppConfig.fromJson(Map<String, dynamic> json) {
return AppConfig(
apiUrl: json['api_url'],
timeout: json['timeout'],
enableLogs: json['enable_logs'],
theme: json['theme'],
);
}
// 加载配置
static Future<AppConfig> load() async {
final jsonString = await rootBundle.loadString('assets/config/app_config.json');
final jsonData = json.decode(jsonString);
return AppConfig.fromJson(jsonData);
}
}
- 使用配置:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder<AppConfig>(
future: AppConfig.load(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final config = snapshot.data!;
return MaterialApp(
theme: config.theme == 'dark' ? ThemeData.dark() : ThemeData.light(),
home: HomePage(config: config),
);
}
return CircularProgressIndicator();
},
);
}
}