👻

Flutter: 画像を読み込み中はShimmerアニメーションを使おう!

に公開

はじめに

画像などが多い画面で読み込みに時間がかかる場合、ユーザーに「コンテンツが準備中である」ことを視覚的に伝える方法の1つとして Shimmer パッケージがあります。

Shimmer とはこれです!

この「キラっキラ」という準備してますよ、読み込みしてますよ。のやつです。
これを実装していきます。
思ったよりかなり簡単でした。

手順

1. パッケージの追加

まず、pubspec.yaml ファイルにshimmerパッケージを追加します。
バージョンは適宜変更してください。

dependencies:
  flutter:
    sdk: flutter
  shimmer: ^3.0.0

その後 pug get を忘れずに!

2. 共通コンポーネントを作成する

アプリ全体で再利用出来るように、共通コンポーネントとして作成します。

下記のコードで

  • 画像URLがnullまたは空の場合は、プレースホルダーを表示
  • 画像読み込み中はShimmerエフェクトを表示
  • 画像読み込み完了時は実際の画像を表示
  • 画像読み込みエラー時はエラー表示またはプレースホルダーを表示

という動作になります。

/// 画像読み込み中にShimmerエフェクトを表示する共通コンポーネント
///
/// 画像の読み込み状態に応じて、Shimmerエフェクト、画像、またはエラー表示を切り替える。
library;

import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:sample/views/widgets/no_image_container.dart';

class ShimmerImageLoader extends StatelessWidget {
  final String? imageUrl;
  final double? width;
  final double? height;
  final BoxFit fit;
  final BorderRadius? borderRadius;
  final Widget? errorWidget;

  const ShimmerImageLoader({
    super.key,
    this.imageUrl,
    this.width,
    this.height,
    this.fit = BoxFit.cover,
    this.borderRadius,
    this.errorWidget,
  });

  @override
  Widget build(BuildContext context) {
    // 画像URLがnullまたは空の場合はNoImageContainerを表示
    if (imageUrl == null || imageUrl!.isEmpty) {
      return NoImageContainer(
        width: width ?? double.infinity,
        height: height ?? double.infinity,
      );
    }

    // 画像を読み込む
    return ClipRRect(
      borderRadius: borderRadius ?? BorderRadius.zero,
      child: Image.network(
        imageUrl!,
        width: width,
        height: height,
        fit: fit,
        loadingBuilder: (context, child, loadingProgress) {
          // 読み込み完了時は画像を表示
          if (loadingProgress == null) return child;

          // 読み込み中はShimmerエフェクトを表示
          return Shimmer.fromColors(
            baseColor: Colors.grey[300]!,
            highlightColor: Colors.grey[100]!,
            child: Container(
              width: width ?? double.infinity,
              height: height ?? double.infinity,
              color: Colors.white,
            ),
          );
        },
        errorBuilder: (context, error, stackTrace) {
          // エラー時はカスタムウィジェットまたはNoImageContainerを表示
          return errorWidget ??
              NoImageContainer(
                width: width ?? double.infinity,
                height: height ?? double.infinity,
              );
        },
      ),
    );
  }
}

3.各画面での使用方法

ShimmerImageLoader を使用する例です。

AspectRatio(
  aspectRatio: 1.2,
  child: ClipRRect(
    borderRadius: const BorderRadius.only(
      topLeft: Radius.circular(4),
      topRight: Radius.circular(4),
    ),
    child: ShimmerImageLoader(
      imageUrl: product.imageUrl,
      width: double.infinity,
      height: double.infinity,
      borderRadius: const BorderRadius.only(
        topLeft: Radius.circular(4),
        topRight: Radius.circular(4),
      ),
    ),
  ),
),

テストの仕方

最後にテストしたいと思います。
どのように遅延させるかという方法なのですが、下記のように _forceShimmer = trueにしてあげて強制的に Shimmer を表示させてあげればOKです。テスト後は削除してください。
(頻繁に確認したいならflaseにするでも良いですが、不要なコードとして残りそうなので削除がおすすめです)

class ShimmerImageLoader extends StatelessWidget {
  // ... 既存のコード ...
  
  // テスト用に強制的にShimmerを表示するフラグ
  static const bool _forceShimmer = true; // テスト後にfalseに戻す
  
  @override
  Widget build(BuildContext context) {
    // ... 既存のコード ...
    
    // テスト用に強制的にShimmerを表示
    if (_forceShimmer) {
      return Shimmer.fromColors(
        baseColor: Colors.grey[300]!,
        highlightColor: Colors.grey[100]!,
        child: Container(
          width: width ?? double.infinity,
          height: height ?? double.infinity,
          color: Colors.white,
        ),
      );
    }
    
    // ... 既存のコード ...
  }
}

その後、ビルドして該当の画面を確認していただければ挙動が確認出来ます。

以上。

パッケージ

https://pub.dev/packages/shimmer

Discussion