第2章:第一个Flutter应用 —— 2.6 资源管理

92 阅读3分钟

2.6 资源管理

📚 核心知识点

  1. 什么是资源(Assets)
  2. 在 pubspec.yaml 中指定资源
  3. 加载图片资源
  4. 加载文本/JSON 资源
  5. 资源变体(分辨率适配)
  6. 特定平台资源(APP图标、启动页)

💡 核心概念

什么是资源(Assets)?

Flutter 应用包含两部分:

  1. 代码 - Dart 代码编译后的内容
  2. 资源(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

最佳实践

  1. 至少提供 1x 和 2x 图片
  2. 图片尺寸建议:
    • 1x: 100×100
    • 2x: 200×200
    • 3x: 300×300
  3. 如果缺少对应分辨率,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/

文件:

或使用 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: 为什么图片加载不出来?

可能原因:

  1. 未在 pubspec.yaml 中声明
# ❌ 忘记添加
flutter:
  uses-material-design: true

# ✅ 正确添加
flutter:
  uses-material-design: true
  assets:
    - images/avatar.png
  1. 路径错误
// ❌ 错误:多了斜杠
Image.asset('/images/avatar.png')

// ✅ 正确
Image.asset('images/avatar.png')
  1. 缩进错误
# ❌ 错误:缩进不对
flutter:
assets:
  - images/

# ✅ 正确:assets 需要缩进
flutter:
  assets:
    - images/
  1. 未运行 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: 如何减小资源文件大小?

  1. 压缩图片

    • 使用 TinyPNG 等工具
    • 选择合适的格式(WebP > PNG > JPG)
  2. 按需加载

# ❌ 不要一次性加载所有
assets:
  - assets/

# ✅ 只加载需要的
assets:
  - assets/data/config.json
  - assets/images/logo.png
  1. 使用矢量图标
// 使用 Icons 而不是图片
Icon(Icons.favorite)  // 很小,可缩放

Q5: 资源文件在运行时可以修改吗?

不可以!

资源文件是只读的,打包到安装包中。

如果需要运行时修改,应该使用:

  • 文件系统(path_provider 包)
  • 数据库(sqflite 包)
  • SharedPreferences(shared_preferences 包)

🎓 跟着做练习

练习1:加载并显示多张图片 ⭐⭐

目标: 创建一个图片画廊

步骤:

  1. 准备3张图片放到 images/ 目录

  2. 在 pubspec.yaml 中添加:

flutter:
  assets:
    - images/
  1. 创建图片列表:
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 文件加载应用配置

步骤:

  1. 创建 assets/config/app_config.json
{
  "api_url": "https://api.example.com",
  "timeout": 30,
  "enable_logs": true,
  "theme": "dark"
}
  1. 创建配置管理类:
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);
  }
}
  1. 使用配置:
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();
      },
    );
  }
}

参考: 《Flutter实战·第二版》2.6节