Flutter for OpenHarmony 微动漫App实战:图片加载实现

通过网盘分享的文件:flutter1.zip

链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97

图片是动漫App的核心内容,封面、角色头像、新闻配图到处都是图片。处理好图片加载,包括加载中状态、加载失败处理、尺寸控制,是提升用户体验的关键。

这篇文章会实现一个通用的网络图片组件,讲解 Image.network 的各种配置、加载状态处理,以及如何设计一个灵活可复用的图片组件。


为什么要封装图片组件

直接用 Image.network 有几个问题:

重复代码:每次都要写 loadingBuilder、errorBuilder。

不一致:不同地方的加载状态、错误处理可能不一样。

难维护:想统一修改加载样式,要改很多地方。

封装成组件后,这些问题都解决了。


组件基础结构

dart 复制代码
import 'package:flutter/material.dart';

class AppNetworkImage extends StatelessWidget {
  final String? imageUrl;
  final double? width;
  final double? height;
  final BoxFit fit;
  final Widget? placeholder;
  final Widget? errorWidget;

  const AppNetworkImage({
    super.key,
    required this.imageUrl,
    this.width,
    this.height,
    this.fit = BoxFit.cover,
    this.placeholder,
    this.errorWidget,
  });

imageUrl 是图片地址,可以为 null。

widthheight 控制尺寸,可选。

fit 控制图片如何填充容器,默认 cover。

placeholdererrorWidget 是自定义的加载中和错误组件。


参数设计思路

必需 vs 可选:只有 imageUrl 是必需的,其他都有默认值。

默认值合理:fit 默认 cover 是最常用的,placeholder 和 errorWidget 默认为 null 使用内置样式。

可定制:需要自定义时可以传入 placeholder 和 errorWidget。


空 URL 处理

dart 复制代码
@override
Widget build(BuildContext context) {
  if (imageUrl == null || imageUrl!.isEmpty) {
    return _buildError();
  }

  return Image.network(
    imageUrl!,
    // 其他配置
  );
}

先检查 URL 是否为空,空的话直接显示错误状态,不发起网络请求。

这样可以避免不必要的网络请求和错误日志。


Image.network 配置

dart 复制代码
return Image.network(
  imageUrl!,
  width: width,
  height: height,
  fit: fit,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return placeholder ?? Container(
      width: width,
      height: height,
      color: Colors.grey[300],
      child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
    );
  },
  errorBuilder: (context, error, stackTrace) {
    return _buildError();
  },
);

widthheight 传给 Image.network,控制图片尺寸。

fit 控制图片如何填充容器。

loadingBuilder 处理加载中状态。

errorBuilder 处理加载失败。


loadingBuilder 详解

dart 复制代码
loadingBuilder: (context, child, loadingProgress) {
  if (loadingProgress == null) return child;
  return placeholder ?? Container(
    width: width,
    height: height,
    color: Colors.grey[300],
    child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
  );
},

loadingProgress 为 null 表示加载完成,返回 child(图片本身)。

loadingProgress 不为 null 表示加载中,显示占位组件。

如果传入了自定义 placeholder 就用它,否则用默认的灰色背景 + 转圈圈。


加载进度显示

loadingProgress 包含加载进度信息:

dart 复制代码
loadingBuilder: (context, child, loadingProgress) {
  if (loadingProgress == null) return child;
  
  final progress = loadingProgress.expectedTotalBytes != null
      ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
      : null;
  
  return Container(
    width: width,
    height: height,
    color: Colors.grey[300],
    child: Center(
      child: CircularProgressIndicator(
        value: progress,
        strokeWidth: 2,
      ),
    ),
  );
},

cumulativeBytesLoaded 是已加载的字节数。

expectedTotalBytes 是总字节数,可能为 null(服务器没返回 Content-Length)。

有进度时显示确定的进度条,没有时显示不确定的转圈圈。


错误处理

dart 复制代码
Widget _buildError() {
  return errorWidget ?? Container(
    width: width,
    height: height,
    color: Colors.grey[300],
    child: const Icon(Icons.image, color: Colors.grey),
  );
}

错误状态显示灰色背景 + 图片图标。

如果传入了自定义 errorWidget 就用它。


BoxFit 详解

BoxFit 控制图片如何填充容器:

dart 复制代码
// 填满容器,可能裁剪
fit: BoxFit.cover

// 完整显示,可能留白
fit: BoxFit.contain

// 拉伸填满,可能变形
fit: BoxFit.fill

// 宽度填满
fit: BoxFit.fitWidth

// 高度填满
fit: BoxFit.fitHeight

// 不缩放
fit: BoxFit.none

cover 最常用,图片填满容器,超出部分裁剪,不会变形。

contain 完整显示图片,可能有留白。

fill 拉伸填满,会变形,一般不用。


组件的使用方式

dart 复制代码
// 基本使用
AppNetworkImage(imageUrl: anime.imageUrl)

// 指定尺寸
AppNetworkImage(
  imageUrl: anime.imageUrl,
  width: 100,
  height: 150,
)

// 自定义占位
AppNetworkImage(
  imageUrl: anime.imageUrl,
  placeholder: const ShimmerCard(),
)

// 自定义错误
AppNetworkImage(
  imageUrl: anime.imageUrl,
  errorWidget: Container(
    color: Colors.red[100],
    child: const Icon(Icons.error),
  ),
)

// 不同的填充方式
AppNetworkImage(
  imageUrl: anime.imageUrl,
  fit: BoxFit.contain,
)

一个组件,多种配置,适应各种场景。


圆角图片

配合 ClipRRect 实现圆角:

dart 复制代码
ClipRRect(
  borderRadius: BorderRadius.circular(12),
  child: AppNetworkImage(
    imageUrl: anime.imageUrl,
    width: 100,
    height: 150,
  ),
)

ClipRRect 裁剪子组件的圆角。


圆形图片

配合 ClipOval 实现圆形:

dart 复制代码
ClipOval(
  child: AppNetworkImage(
    imageUrl: user.avatarUrl,
    width: 60,
    height: 60,
  ),
)

ClipOval 裁剪成椭圆,宽高相等时就是圆形。


深色模式适配

dart 复制代码
Widget _buildError() {
  final isDark = Theme.of(context).brightness == Brightness.dark;
  final bgColor = isDark ? Colors.grey[800] : Colors.grey[300];
  final iconColor = isDark ? Colors.grey[600] : Colors.grey;
  
  return errorWidget ?? Container(
    width: width,
    height: height,
    color: bgColor,
    child: Icon(Icons.image, color: iconColor),
  );
}

深色模式下用深灰色背景,图标也用深灰色。

但这需要 context,StatelessWidget 里不能直接访问。可以改成 StatefulWidget 或者在 build 方法里处理。


添加淡入动画

图片加载完成后淡入显示:

dart 复制代码
class AppNetworkImage extends StatefulWidget {
  // 参数定义
}

class _AppNetworkImageState extends State<AppNetworkImage> {
  bool _isLoaded = false;

  @override
  Widget build(BuildContext context) {
    if (widget.imageUrl == null || widget.imageUrl!.isEmpty) {
      return _buildError();
    }

    return AnimatedOpacity(
      opacity: _isLoaded ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 300),
      child: Image.network(
        widget.imageUrl!,
        width: widget.width,
        height: widget.height,
        fit: widget.fit,
        loadingBuilder: (context, child, loadingProgress) {
          if (loadingProgress == null) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              if (mounted && !_isLoaded) {
                setState(() => _isLoaded = true);
              }
            });
            return child;
          }
          return widget.placeholder ?? _buildPlaceholder();
        },
        errorBuilder: (context, error, stackTrace) {
          return _buildError();
        },
      ),
    );
  }
}

AnimatedOpacity 控制透明度动画。加载完成后设置 _isLoaded = true,触发淡入。

addPostFrameCallback 确保在帧结束后更新状态,避免在 build 过程中调用 setState。


图片缓存

Image.network 默认有内存缓存,但没有磁盘缓存。如果需要磁盘缓存,可以用 cached_network_image 包:

dart 复制代码
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: anime.imageUrl ?? '',
  width: width,
  height: height,
  fit: fit,
  placeholder: (context, url) => _buildPlaceholder(),
  errorWidget: (context, url, error) => _buildError(),
)

cached_network_image 会把图片缓存到磁盘,下次加载更快。

但在 HarmonyOS 上可能有兼容性问题,需要测试。


图片预加载

可以提前加载图片,用户看到时已经在缓存里:

dart 复制代码
void _precacheImages(List<Anime> animeList) {
  for (final anime in animeList) {
    if (anime.imageUrl != null) {
      precacheImage(NetworkImage(anime.imageUrl!), context);
    }
  }
}

precacheImage 预加载图片到内存缓存。

在列表数据加载完成后调用,用户滚动时图片已经准备好了。


图片尺寸优化

有些 API 支持请求不同尺寸的图片:

dart 复制代码
String _getOptimizedUrl(String? url, {int? width}) {
  if (url == null) return '';
  
  // 假设 API 支持 ?w=200 参数
  if (width != null) {
    return '$url?w=$width';
  }
  return url;
}

请求小尺寸图片可以减少流量和加载时间。


图片加载失败重试

dart 复制代码
class AppNetworkImage extends StatefulWidget {
  // 参数
}

class _AppNetworkImageState extends State<AppNetworkImage> {
  int _retryCount = 0;
  static const int _maxRetry = 3;

  void _retry() {
    if (_retryCount < _maxRetry) {
      setState(() => _retryCount++);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      '${widget.imageUrl}?retry=$_retryCount',  // 加参数强制重新请求
      // 其他配置
      errorBuilder: (context, error, stackTrace) {
        return GestureDetector(
          onTap: _retry,
          child: Container(
            width: widget.width,
            height: widget.height,
            color: Colors.grey[300],
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.refresh, color: Colors.grey),
                const SizedBox(height: 4),
                Text(
                  '点击重试',
                  style: TextStyle(color: Colors.grey[600], fontSize: 12),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

加载失败时显示重试按钮,点击后重新加载。

URL 加上 retry 参数强制重新请求,绕过缓存。


小结

图片加载组件涉及的技术点:Image.networkloadingBuildererrorBuilderBoxFitClipRRect 圆角ClipOval 圆形AnimatedOpacity 淡入precacheImage 预加载

组件设计要点:处理空 URL、加载中状态、加载失败状态,提供合理的默认值,支持自定义。

图片是 App 的重要组成部分,处理好图片加载能显著提升用户体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
nbsaas-boot2 小时前
基于 Java 21 ScopedValue 的多租户动态数据源完整实践
java·开发语言
摘星编程2 小时前
在OpenHarmony上用React Native:Recoil选择器异步数据
javascript·react native·react.js
liuc03172 小时前
Java项目关于不同key的读取
java·开发语言
雨中深巷的油纸伞2 小时前
vue 项目部署到iis后 浏览器刷新404
前端·javascript·vue.js
Zach_yuan2 小时前
面向对象封装线程:用 C++ 封装 pthread
开发语言·c++·算法
菜宾2 小时前
java-seata基础教学
java·开发语言·adb
新镜2 小时前
【Flutter】LTR/RTL 阿拉伯语言/希伯来语言
android·flutter·ios·客户端
谢尔登2 小时前
从源码视角来看Pinia!
前端·javascript·vue.js
梦6502 小时前
JavaScript 循环
开发语言·javascript·ecmascript