
アプリ開発あるあるですが、「データを取得中のスケルトンUI(Shimmer)」を作る作業、地味に辛くないですか?
これまでの辛い現実(shimmerパッケージなど):
Container や Row で再現する「なんで同じようなコードを2回も書かなきゃいけないんだ...」 そう思ったあなたに朗報です。
skeletonizer パッケージを使えば、その苦行から解放されます。
今日は、UIを「囲むだけ」で魔法のようにスケルトン化してくれる神パッケージを紹介します。
skeletonizer は、既存のWidgetをそのまま使ってスケルトン表示(ローディング表示)を生成してくれるパッケージです。
つまり、「ローディング用のレイアウト」を作る必要がありません。 「本番のUI」に「まだデータがないよ」と伝えるだけで、勝手に Skeleton にしてくれます。
どれくらい楽になるのか、従来のやり方と比べてみましょう。
今回は次のようなカードUIに対して、スケルトンUIを実装する例を考えます。
>
まず、ベースとなるカードのWidgetコードです。 (長くなるので折りたたんでいます。クリックして確認してください)
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,
),
],
),
),
],
),
),
);
}
}
早速実装してみた例がこちら。
やることはたったひとつ。本番用の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),
);
従来の 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つです。
これまでローディングUIの実装に苦労していたFlutter開発者の皆さん、ぜひ skeletonizer を使って、爆速でローディングUIを実装してみてください!
最後までご覧いただき、ありがとうございました。
全体のコード例はGitHubリポジトリにも掲載していますので、ぜひ参考にしてください。
🚀 GitHubで開く可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More