头图

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

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

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

1.使用 WebP 而不是 JPG/PNG

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

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

这里有一些基准:


Image.asset(
  // 'image.jpg',
  'image.webp', // PREFER
);

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

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

之前——之后

Image.network(
  imageUrl,
  width: 200,
  height: 150,
);

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


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

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

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

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

注意!缓存大小不应该小于小部件的大小,否则,由于分辨率低,它看起来像素化!
Image.network(
  imageUrl,
  cacheWidth: 100,
  cacheHeight: 150,
);

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

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

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.加载时显示进度指示器

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

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

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

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

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

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

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

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 作为占位符

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

https://blurha.sh/

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

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

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

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

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

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

return FadeInImage.memoryNetwork(
  image: imageUrl,
  placeholder: kTransparentImage,
);

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

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

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

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

11.注意非经常性成本

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

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

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 提升到另一个层次,我们应该让用户重新加载图片,并继续使用应用,而不会遇到任何麻烦。

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,将它们随意组合并自由使用!


杭州程序员张张
11.8k 声望6.7k 粉丝

Web/Flutter/独立开发者/铲屎官