第9章 - 资源管理
嗨,朋友!我是长安。
做一个漂亮的 App,怎么能少得了图片、图标和字体呢?这一章,我们来学习 Flutter 中的资源管理,让你的应用更加生动美观。
📁 什么是资源(Assets)?
资源是应用程序中使用的静态文件,包括:
- 🖼️ 图片:PNG、JPG、SVG、WebP 等
- 🔤 字体:自定义字体文件
- 📄 文件:JSON、文本文件等
- 🎵 音频:MP3、WAV 等
🖼️ 添加图片资源
1. 创建资源目录
首先,在项目根目录创建 assets 文件夹:
my_app/
├── lib/
├── assets/
│ ├── images/ # 存放图片
│ │ ├── logo.png
│ │ └── banner.jpg
│ ├── icons/ # 存放图标
│ └── fonts/ # 存放字体
├── pubspec.yaml
└── ...
2. 在 pubspec.yaml 中声明
打开 pubspec.yaml,找到 flutter: 部分,添加资源声明:
flutter:
# 启用 Material Design 图标
uses-material-design: true
# 声明资源
assets:
- assets/images/ # 整个目录
- assets/images/logo.png # 单个文件
- assets/icons/
注意
- YAML 文件对缩进非常敏感,请使用空格(不要用 Tab)
- 修改
pubspec.yaml后,需要运行flutter pub get
3. 使用图片
// 方式一:Image.asset(最常用)
Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
fit: BoxFit.cover,
)
// 方式二:使用 AssetImage
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/banner.jpg'),
fit: BoxFit.cover,
),
),
)
// 方式三:在 CircleAvatar 中使用
CircleAvatar(
radius: 50,
backgroundImage: AssetImage('assets/images/avatar.png'),
)
4. 图片适应方式(BoxFit)
Image.asset(
'assets/images/photo.jpg',
fit: BoxFit.cover, // 填充容器,可能裁剪(最常用)
// fit: BoxFit.contain, // 完整显示,可能留白
// fit: BoxFit.fill, // 拉伸填满,可能变形
// fit: BoxFit.fitWidth, // 宽度填满
// fit: BoxFit.fitHeight, // 高度填满
// fit: BoxFit.none, // 原始尺寸
// fit: BoxFit.scaleDown, // 缩小以完整显示
)
图示:
原图:📷
cover: [📷📷] 填满,裁剪多余
contain: [ 📷 ] 完整显示,留白
fill: [📷‾‾] 拉伸填满
🌐 加载网络图片
基本用法
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
)
加载状态和错误处理
Image.network(
'https://example.com/image.jpg',
// 加载中显示
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
// 加载失败显示
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(
Icons.broken_image,
size: 50,
color: Colors.grey,
),
);
},
)
使用 cached_network_image(推荐)
cached_network_image 插件可以缓存网络图片,避免重复下载。
- 添加依赖:
dependencies:
cached_network_image: ^3.3.0
- 使用:
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
width: 200,
height: 200,
fit: BoxFit.cover,
)
🎭 图片分辨率适配
Flutter 支持根据设备分辨率加载不同的图片。
创建多分辨率图片
assets/
└── images/
├── logo.png # 1x(基准)
├── 2.0x/
│ └── logo.png # 2x(2倍分辨率)
└── 3.0x/
└── logo.png # 3x(3倍分辨率)
声明资源
flutter:
assets:
- assets/images/logo.png
只需声明基准图片,Flutter 会自动查找对应分辨率的版本。
使用
// Flutter 自动选择合适分辨率的图片
Image.asset('assets/images/logo.png')
🔤 添加自定义字体
1. 准备字体文件
下载字体文件(.ttf 或 .otf),放入 assets/fonts/ 目录:
assets/
└── fonts/
├── Roboto-Regular.ttf
├── Roboto-Bold.ttf
└── Roboto-Italic.ttf
2. 在 pubspec.yaml 中声明
flutter:
fonts:
- family: Roboto # 字体家族名(自己命名)
fonts:
- asset: assets/fonts/Roboto-Regular.ttf
- asset: assets/fonts/Roboto-Bold.ttf
weight: 700 # 粗体
- asset: assets/fonts/Roboto-Italic.ttf
style: italic # 斜体
- family: MyCustomFont # 另一个字体
fonts:
- asset: assets/fonts/MyFont.ttf
3. 使用字体
// 方式一:在 Text 中使用
Text(
'Hello Flutter',
style: TextStyle(
fontFamily: 'Roboto',
fontSize: 24,
fontWeight: FontWeight.bold,
),
)
// 方式二:设置为全局默认字体
MaterialApp(
theme: ThemeData(
fontFamily: 'Roboto', // 全局默认字体
),
home: MyHomePage(),
)
🎨 使用图标
Material Icons(内置)
Flutter 内置了大量 Material Design 图标:
// 基础用法
Icon(Icons.home)
// 带样式
Icon(
Icons.favorite,
size: 30,
color: Colors.red,
)
// 常用图标
Icons.home // 首页
Icons.search // 搜索
Icons.settings // 设置
Icons.person // 个人
Icons.shopping_cart // 购物车
Icons.notifications // 通知
Icons.menu // 菜单
Icons.close // 关闭
Icons.add // 添加
Icons.delete // 删除
Icons.edit // 编辑
Icons.share // 分享
图标查找
查看所有 Material Icons:https://fonts.google.com/icons
Cupertino Icons(iOS 风格)
import 'package:flutter/cupertino.dart';
Icon(CupertinoIcons.heart_fill)
Icon(CupertinoIcons.home)
Icon(CupertinoIcons.gear)
自定义图标(flutter_svg)
使用 SVG 图标更灵活:
- 添加依赖:
dependencies:
flutter_svg: ^2.0.9
- 添加 SVG 文件到资源:
flutter:
assets:
- assets/icons/
- 使用:
import 'package:flutter_svg/flutter_svg.dart';
SvgPicture.asset(
'assets/icons/logo.svg',
width: 100,
height: 100,
colorFilter: ColorFilter.mode(Colors.blue, BlendMode.srcIn),
)
// 网络 SVG
SvgPicture.network(
'https://example.com/icon.svg',
width: 50,
height: 50,
)
📄 读取文本和 JSON 文件
读取文本文件
import 'package:flutter/services.dart';
Future<String> loadTextFile() async {
return await rootBundle.loadString('assets/data/readme.txt');
}
读取 JSON 文件
import 'dart:convert';
import 'package:flutter/services.dart';
Future<List<Map<String, dynamic>>> loadJsonData() async {
final String jsonString = await rootBundle.loadString('assets/data/users.json');
final List<dynamic> jsonData = json.decode(jsonString);
return jsonData.cast<Map<String, dynamic>>();
}
// 使用
class MyWidget extends StatefulWidget {
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
List<Map<String, dynamic>> _users = [];
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
final users = await loadJsonData();
setState(() {
_users = users;
});
}
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_users[index]['name']),
);
},
);
}
}
🎯 实战示例:图片画廊
class ImageGallery extends StatelessWidget {
final List<String> images = [
'assets/images/photo1.jpg',
'assets/images/photo2.jpg',
'assets/images/photo3.jpg',
'assets/images/photo4.jpg',
'assets/images/photo5.jpg',
'assets/images/photo6.jpg',
];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('图片画廊'),
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: images.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 点击查看大图
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImageDetailPage(
imagePath: images[index],
),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
images[index],
fit: BoxFit.cover,
),
),
);
},
),
);
}
}
// 图片详情页
class ImageDetailPage extends StatelessWidget {
final String imagePath;
const ImageDetailPage({super.key, required this.imagePath});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Center(
child: InteractiveViewer(
child: Image.asset(imagePath),
),
),
);
}
}
📝 小结
这一章我们学习了:
- ✅ 如何添加和声明资源(assets)
- ✅ 加载本地图片和网络图片
- ✅ 图片分辨率适配(2x、3x)
- ✅ 添加和使用自定义字体
- ✅ 使用 Material Icons 和自定义图标
- ✅ 读取文本和 JSON 文件
资源管理最佳实践
- 图片压缩 - 使用合适大小的图片,避免过大
- 懒加载 - 网络图片使用缓存库
- 占位图 - 加载中显示占位内容
- 错误处理 - 处理加载失败的情况
💪 练习题
- 添加一张本地图片和一个自定义字体到你的项目
- 创建一个用户头像组件,支持本地图片和网络图片
- 使用 JSON 文件存储一个商品列表,然后在列表中显示
点击查看答案
练习2:用户头像组件
class UserAvatar extends StatelessWidget {
final String? imageUrl;
final String? localImage;
final double size;
const UserAvatar({
super.key,
this.imageUrl,
this.localImage,
this.size = 50,
});
Widget build(BuildContext context) {
return ClipOval(
child: SizedBox(
width: size,
height: size,
child: _buildImage(),
),
);
}
Widget _buildImage() {
// 优先使用网络图片
if (imageUrl != null && imageUrl!.isNotEmpty) {
return Image.network(
imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _defaultAvatar(),
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
);
}
// 其次使用本地图片
if (localImage != null && localImage!.isNotEmpty) {
return Image.asset(
localImage!,
fit: BoxFit.cover,
);
}
// 默认头像
return _defaultAvatar();
}
Widget _defaultAvatar() {
return Container(
color: Colors.grey[300],
child: Icon(
Icons.person,
size: size * 0.6,
color: Colors.grey[600],
),
);
}
}
// 使用
UserAvatar(imageUrl: 'https://example.com/avatar.jpg', size: 60)
UserAvatar(localImage: 'assets/images/avatar.png', size: 60)
UserAvatar(size: 60) // 显示默认头像
🚀 下一步
掌握了资源管理后,下一章我们来学习 网络请求,让应用能够获取网络数据!
由 编程指南 提供
