干货 | 在 Flutter 中实现最佳 UX 性能的 12 个图像技巧和最佳实践

使用这些基本 Flutter 技巧提高用户体验和性能

Image widget 是 Flutter 中最常用的 widget 之一,但我相信我们没有充分利用它的功能,仅仅显示一个图片是不够的,你还应该给用户他们需要的最佳体验!

在这篇文章中,我将谈论一些图像技巧和最佳实践,以获得更好的性能和用户体验。

1.使用 WebP 而不是 JPG/PNG

WebP 是下一代图像格式,它比 PNG 和 JPEG 小约 25%,并且比其他格式快。

这意味着,你的应用程序将使用更少的内存,构建速度更快。

这里有一些基准:

dart 复制代码
Image.asset(
  // 'image.jpg',
  'image.webp', // PREFER
);

2.设置宽度和高度以保留 UI 空间

它可以防止应用程序出现布局偏移

dart 复制代码
Image.network(
  imageUrl,
  width: 200,
  height: 150,
);

3.降低图片的显示分辨率以减少内存使用

你的图片可能会导致设备内存膨胀,这是因为,尽管它们在 UI 中占据相对较小的一部分,Flutter 还是会以全分辨率渲染它们,从而消耗大量内存。

为了避免这种问题,可以使用 cacheWidthcacheHeight 参数对指定大小的图像进行解码。

此外,我们可以使用 Flutter 开发者工具轻松检测超大图像。

如果图像过大,它会反转图像,使其颠倒。

注意!缓存大小不应该小于小部件的大小,否则,由于分辨率低,它看起来像素化!

dart 复制代码
Image.network(
  imageUrl,
  cacheWidth: 100,
  cacheHeight: 150,
);

4.预加载/预缓存您的图像,以便即时加载图像

如果你在显示图像之前缓存它们,Flutter 将跳过构建的处理步骤并立即显示它们。

dart 复制代码
class MyImage extends StatefulWidget {
  const MyImage({super.key});

  @override
  State createState() => _MyImageState();
}

class _MyImageState extends State {
  late final Image myImage;

  @override
  void initState() {
    super.initState();
    myImage = Image.asset('path');
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    precacheImage(myImage.image, context);
  }

  @override
  Widget build(BuildContext context) {
    return myImage;
  }
}

5.加载时显示进度指示器

突然弹出图像不是预期的行为,用户可能会因为网络连接不足而错过图像并向下滚动,或者可能在屏幕上看到一些空白,等等。我们应该始终通知用户图像正在加载。

dart 复制代码
return Image.network(
  imageUrl,
  loadingBuilder: (_, child, event) {
    if (event == null) return child;
    return const Center(child: CircularProgressIndicator());
  },
);

6.加载时显示进度百分比指示器

我们也可以显示进度百分比,而不是无限加载,这样对用户来说更有用。

dart 复制代码
return Image.network(
  imageUrl,
  loadingBuilder: (_, child, event) {
    if (event == null) return child;
    return Center(
      child: CircularProgressIndicator(
        value: event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 0),
      ),
    );
  },
);

7.加载时显示闪烁效果,以提高用户体验

显示进度条是好的,但并不是最好的选择,显示闪烁效果(Shimmer)要比显示进度条好得多。

dart 复制代码
return Image.network(
  imageUrl,
  height: 200,
  width: 350,
  loadingBuilder: (_, child, event) {
    if (event == null) return child;
    return const Shimmer(
      height: 200,
      width: 350,
     );
  }
);

// Most Basic Shimmer
class Shimmer extends StatefulWidget {
  const Shimmer({
    super.key,
    this.width,
    this.height,
    this.minOpacity = 0.015,
    this.maxOpacity = 0.15,
    this.borderRadius = const BorderRadius.all(Radius.circular(4)),
    this.child,
  });

  final double? width;
  final double? height;
  final double minOpacity;
  final double maxOpacity;
  final BorderRadius? borderRadius;
  final Widget? child;

  @override
  State createState() => _ShimmerState();
}

class _ShimmerState extends State with SingleTickerProviderStateMixin {
  late final AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
      lowerBound: widget.minOpacity,
      upperBound: widget.maxOpacity,
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: FadeTransition(
        opacity: controller,
        child: Container(
          width: widget.width,
          height: widget.height,
          decoration: BoxDecoration(
            color: Colors.black,
            borderRadius: widget.borderRadius,
          ),
          child: widget.child,
        ),
      ),
    );
  }
}

我创建了一个简单的 shimmer 小部件,但你可以从官方文档中学习如何创建高级版本的 shimmer 效果。

官方文档:创建 shimmer 加载效果

8.显示 blurhash 作为占位符

为了改善用户体验,可以使用哈希代码显示图像的模糊版本,而不是在图像加载时显示空白的灰色区域。

dart 复制代码
return const SizedBox(
  width: 350,
  height: 200,
  child: BlurHash(
    hash: hashCode,
    imageFit: BoxFit.cover,
    image: imageUrl,
  ),
);

9.使用渐变效果来提高用户体验

默认情况下,图片加载后立即显示,这对我们的视觉体验来说非常糟糕,为了改善这一点,我们可以用一个小的渐入动画来显示它们。

我们可以使用 FadeInImage 来实现这个功能。

它需要字节或资源作为占位符,在这个例子中,我将使用 transparent_image 包来获取透明图像字节。

我们还可以使用 cached_network_image 包来实现这一点,以及更多。

dart 复制代码
return FadeInImage.memoryNetwork(
  image: imageUrl,
  placeholder: kTransparentImage,
);

// 或者
return CachedNetworkImage(
  imageUrl: imageUrl,
);

10.缓存图像以减少网络使用并提高性能

为了避免每次下载相同的图片,我们可以缓存第一次下载的图片并重复使用,为了实现这一点,我们可以创建自己的缓存机制,或者我们可以直接使用 cached_network_image

它缓存网络图像,默认情况下自动显示它们的淡入效果,并提供了更多的图像控制。

11.注意非经常性成本

Image widget 没有 const 构造函数,虽然这在大多数情况下都不是问题,但我们可以通过将它包装在自定义 widget 中来修复它。

它不仅可以让我们的应用程序更具性能,而且我们还可以根据我们的意愿定制小部件,例如,我们可以为每个图像小部件创建一个全局解决方案,而不是每次都处理 error/loading 情况。

dart 复制代码
enum _ImageType { asset, network }

class AppImage extends StatelessWidget {
  const AppImage.asset(
    this.image, {
    super.key,
  }) : type = _ImageType.asset;

  const AppImage.network(
    this.image, {
    super.key,
  }) : type = _ImageType.network;

  final String image;
  final _ImageType type;

  @override
  Widget build(BuildContext context) {
    const errorWidget = Icon(Icons.error);
    return switch (type) {
      _ImageType.asset => Image.asset(
          image,
          errorBuilder: (_, __, ___) => errorWidget,
        ),
      _ImageType.network => Image.network(
          image,
          errorBuilder: (_, __, ___) => errorWidget,
        ),
    };
  }
}


/// 使用
const AppImage.asset(''), // OK
const Image.asset(''), // 错误!!Image 没有const构造函数

// 正如您所知,拥有 const 的小部件非常重要以获得更好的性能。

12.在失败时显示重试按钮

有时候由于网络连接不好或其他原因,图片无法第一次加载,显示错误消息是好的,但这还不够,如果我们想把应用的 UX 提升到另一个层次,我们应该让用户重新加载图片,并继续使用应用,而不会遇到任何麻烦。

dart 复制代码
class MyImage extends StatefulWidget {
  const MyImage({super.key});

  @override
  State createState() => _MyImageState();
}

class _MyImageState extends State {
  int attempt = 0;

  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      cacheKey: '$attempt',
      height: 200,
      width: 250,
      fit: BoxFit.cover,
      errorWidget: (_, __, ___) {
        return RetryWidget(
          height: 200,
          width: 250,
          onTap: () => setState(() => attempt++),
        );
      },
    );
  }
}

// Just a basic retry button
class RetryWidget extends StatelessWidget {
  const RetryWidget({
    super.key,
    required this.height,
    required this.width,
    required this.onTap,
  });

  final double? height;
  final double? width;
  final void Function() onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        height: height,
        width: width,
        alignment: Alignment.center,
        decoration: const BoxDecoration(color: Colors.black12),
        child: const Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.image_not_supported, size: 20),
            SizedBox(height: 12),
            Padding(
              padding: EdgeInsets.symmetric(horizontal: 12),
              child: Text(
                "Image couldn't load, tap here to retry",
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 14, color: Colors.black),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

结语

仅仅展示图片是不够的!您还应该为用户提供他们需要的最佳体验!因此,我强烈建议您创建自己的自定义图片 widget,将它们随意组合并自由使用!


原文:medium.com/itnext/12-i...

相关推荐
孤鸿玉11 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥18 小时前
Flutter Riverpod上手指南
android·flutter·ios
BG1 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng2 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭2 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯2 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan2 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓2 天前
Flutter Getx 的页面传参
flutter
火柴就是我3 天前
flutter 之真手势冲突处理
android·flutter
Speed1233 天前
`mockito` 的核心“打桩”规则
flutter·dart