【Flutter】まだShimmerで実装してるの?Skeletonizerで既存UIを囲むだけで爆速でスケルトンローディングを実装!

image

はじめに:ローディング画面の実装、めんどくさすぎませんか?

ローディングデモ

アプリ開発あるあるですが、「データを取得中のスケルトンUI(Shimmer)」を作る作業、地味に辛くないですか?

これまでの辛い現実(shimmerパッケージなど):

  1. 本番用のきれいなUIを作る
  2. ローディング用のためだけに、同じレイアウトを ContainerRow で再現する
  3. レイアウト変更があったら、2箇所(本番とローディング)修正が必要

「なんで同じようなコードを2回も書かなきゃいけないんだ...」 そう思ったあなたに朗報です。

skeletonizer パッケージを使えば、その苦行から解放されます。 今日は、UIを「囲むだけ」で魔法のようにスケルトン化してくれる神パッケージを紹介します。


Skeletonizerとは?

skeletonizer は、既存のWidgetをそのまま使ってスケルトン表示(ローディング表示)を生成してくれるパッケージです。

つまり、「ローディング用のレイアウト」を作る必要がありません。 「本番のUI」に「まだデータがないよ」と伝えるだけで、勝手に Skeleton にしてくれます。


百聞は一見にしかず:コード比較

どれくらい楽になるのか、従来のやり方と比べてみましょう。

今回は次のようなカードUIに対して、スケルトンUIを実装する例を考えます。

カードUI>

作成するカードUI

まず、ベースとなるカードのWidgetコードです。 (長くなるので折りたたんでいます。クリックして確認してください)

ベースとなる ArticleCard のコード
class ArticleCard extends StatelessWidget {
  final Article article;

  const ArticleCard({super.key, required this.article});

  
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 1. サムネイル画像
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.network(
                article.imageUrl,
                width: 80,
                height: 80,
                fit: BoxFit.cover,
              ),
            ),
            const SizedBox(width: 12),
            // 2. テキスト情報
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    article.title,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                    ),
                    maxLines: 2,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    article.description,
                    style: TextStyle(color: Colors.grey[600], fontSize: 12),
                    maxLines: 2,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

【After】 Skeletonizerでの実装

早速実装してみた例がこちら。

やることはたったひとつ。本番用のWidgetを Skeletonizer で囲むだけです!

これだけで ArticleCard の中身を一切いじることなく、各ウィジェットが適切な長さのスケルトンに自動変換されます。とても簡単でわかりやすいですよね!

Skeletonizer(
  enabled: _isLoading, // trueならスケルトン、falseなら本番表示
  child: ListView.builder(
    itemCount: 10,
    itemBuilder: (context, index) {
      // 普段使っている本番用のCardをそのまま渡すだけ!
      return ArticleCard(
        article: _articles?[index] ?? dummyArticle,
      );
    },
  ),
)

また、仮のデータを使いたい場合にはBoneMockクラスを使うと便利です。かゆいところに手が届く感じが素晴らしいですね。

final dummyArticle = Article(
  title: BoneMock.title,
  description: BoneMock.words(40),
  imageUrl: BoneMock.words(10),
);

❌ Before これまでのやり方 (Shimmer)

従来の shimmer パッケージだと、こんな風に専用のWidgetを作る必要がありました。 本番用UIとは別に、もう一つレイアウトを組む必要があります。...めんどくさい!

さらに、ベースのUIが変更されるたびにローディング用のUIも修正しなければならず、メンテナンスコストが非常に高くなってしまいます。

// 😭 辛いポイント:本番UIとは別にメンテナンスが必要
Shimmer.fromColors(
  baseColor: Colors.grey[300]!,
  highlightColor: Colors.grey[100]!,
  child: ListView.builder(
    itemCount: 6,
    itemBuilder: (_, __) => Padding(
      padding: const EdgeInsets.only(bottom: 8.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 本番と同じレイアウトを空のContainerで再現する虚無の作業...
          Container(width: 48.0, height: 48.0, color: Colors.white),
          const SizedBox(width: 8.0),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Container(width: double.infinity, height: 8.0, color: Colors.white),
                const SizedBox(height: 4.0),
                Container(width: double.infinity, height: 8.0, color: Colors.white),
              ],
            ),
          )
        ],
      ),
    ),
  ),
);

カスタマイズも簡単!

skeletonizer はカスタマイズも簡単です。例えば、以下のようにWidgetの特定の箇所をスケルトン化しないように指定することができます。

// オリジナルのUIのコードの一部をカスタマイズ
Card(
  child: ListTile(
    title: Text('The title goes here'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.keep( // アイコンはローディング中もそのまま表示される
      child: Icon(Icons.ac_unit, size: 40),
    ),
  ),
)

ほかにもさまざまなカスタマイズオプションが用意されていますので、ぜひ公式ドキュメントを参照してみてください!

注意点

Skeletonizerを使う際に、ひとつだけ注意点があります。

それはImageNetworkなどの画像Widgetは自動的にスケルトン化されないことです。もしモックデータで無効なURLを渡すと、画像の読み込みエラーが発生してしまいます。

これを防ぐために、オリジナルのUIのコードの画像部分は Skeleton.replace で囲んであげましょう

ClipRRect(
  borderRadius: BorderRadius.circular(8),
  // ローディング中は幅80、高さ80のスケルトンに置き換える
  child: Skeleton.replace(
    width: 80,
    height: 80,
    // ローディングが終わったら本来の画像を表示
    child: Image.network(
      article.imageUrl,
      width: 80,
      height: 80,
      fit: BoxFit.cover,
    ),
  ),
),

まとめ

今回は、既存のUIを囲むだけでスケルトンローディングを実現できる skeletonizer パッケージを紹介しました。

個人的に特に嬉しいポイントは以下の3つです。

  1. 圧倒的な時短: ローディング用UIをゼロから作る必要がない
  2. 保守性の向上: 本番UIの変更が自動的にスケルトンUIにも反映される
  3. 意外と高い柔軟性: 「ここはスケルトン化しない」「アニメーションを変更する」といったカスタマイズも簡単

これまでローディングUIの実装に苦労していたFlutter開発者の皆さん、ぜひ skeletonizer を使って、爆速でローディングUIを実装してみてください!

最後までご覧いただき、ありがとうございました。



全体のコード例はGitHubリポジトリにも掲載していますので、ぜひ参考にしてください。

🚀 GitHubで開く

参考

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!

可茂IT塾ではFlutter/Reactのインターンを募集しています!

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

タグ

Flutter (124)初心者向け (32)イベント (19)Google Apps Script (17)Nextjs (12)可茂IT塾 (11)React (8)riverpod (7)デザイン (7)AI (7)Firebase (7)Figma (6)JavaScript (6)ChatGPT (5)vscode (5)新卒 (4)就活 (4)Slack (4)Dart (4)お知らせ (4)FlutterWeb (3)Prisma (3)NestJS (3)TypeScript (3)アプリ開発 (3)ワーケーション (3)インターン (3)Web (2)Obsidian (2)設計 (2)線型計画法 (2)事例 (2)Git (2)CSS (2)Freezed (2)Image (2)File (2)Material Design (2)経験談 (2)画像 (2)iOS (2)React Hooks (2)tailwindcss (2)社会人 (2)大学生 (2)RSS (1)Google (1)CodeRunner (1)NotebookLM (1)個人開発 (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)supabase (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)table (1)テーブル (1)hooks (1)パワーポイント (1)ブックマーク (1)Pocket (1)ブクマク (1)MCPサーバー (1)趣味 (1)モンスターボール (1)SCSS (1)Swift (1)MapBox (1)Cupertino (1)gpt-oss (1)生成AI (1)llama.cpp (1)LLM (1)ListView (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)Firebase Analytics (1)Gemini AI (1)コード生成 (1)GitHub Copilot (1)GitHub Actions (1)gemini (1)オンラインオフィス (1)html (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE Messaging API (1)LINE Notify (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)build_runner (1)ヒーター (1)作業効率 (1) (1)Flutter実践開発 (1) (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)FireStorage (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)VRChat (1)API (1)検索機能 (1)ローディング (1)Skeletonizer (1)Simmer (1)Shader (1)SharedPreferences (1)オフラインサポート (1)Navigator (1)メール送信 (1)FlutterFlow (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)video_player (1)IMA (1)カスタム認証 (1)アニメーション (1)Arduino (1)ESP32 (1)フリーランス (1)会社員 (1)mac (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)sora2 (1)Gemini CLI (1)Claude Code (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)getAuth (1)クローズドテスト (1)PlayConsole (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!

可茂IT塾ではFlutter/Reactのインターンを募集しています!

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More