使用这些基本 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 还是会以全分辨率渲染它们,从而消耗大量内存。
为了避免这种问题,可以使用 cacheWidth
或 cacheHeight
参数对指定大小的图像进行解码。
此外,我们可以使用 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 作为占位符
为了改善用户体验,可以使用哈希代码显示图像的模糊版本,而不是在图像加载时显示空白的灰色区域。
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,将它们随意组合并自由使用!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。