Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第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 插件可以缓存网络图片,避免重复下载。

  1. 添加依赖:
dependencies:
  cached_network_image: ^3.3.0
  1. 使用:
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 图标更灵活:

  1. 添加依赖:
dependencies:
  flutter_svg: ^2.0.9
  1. 添加 SVG 文件到资源:
flutter:
  assets:
    - assets/icons/
  1. 使用:
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 {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  List<Map<String, dynamic>> _users = [];

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    final users = await loadJsonData();
    setState(() {
      _users = users;
    });
  }

  @override
  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',
  ];

  @override
  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});

  @override
  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 文件

资源管理最佳实践

  1. 图片压缩 - 使用合适大小的图片,避免过大
  2. 懒加载 - 网络图片使用缓存库
  3. 占位图 - 加载中显示占位内容
  4. 错误处理 - 处理加载失败的情况

💪 练习题

  1. 添加一张本地图片和一个自定义字体到你的项目
  2. 创建一个用户头像组件,支持本地图片和网络图片
  3. 使用 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,
  });

  @override
  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)  // 显示默认头像

🚀 下一步

掌握了资源管理后,下一章我们来学习 网络请求,让应用能够获取网络数据!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
第8章 - 页面导航