🐙

Flutterでカード形式のグリッドレイアウトを作成する方法

に公開

Flutterでカード形式のグリッドレイアウトを作成する方法

Flutterアプリでデータをカード形式で表示したいとき、どのようにレイアウトを組めば良いでしょうか。
今回は、カード形式のグリッドレイアウトについて3つの方法を紹介します。


🚨 問題:データをきれいにカードで表示したい

Flutterアプリでデータを表示する際、以下のような課題があります。

  • リスト形式だと見た目が単調
  • データの種類ごとに視覚的に分けたい
  • レスポンシブなレイアウトにしたい
  • カードのサイズを統一したい

📱 方法1:基本的なカード表示

最もシンプルな方法です。Cardウィジェットを使って基本的なカードを作成します。

class BasicCardDemo extends StatelessWidget {
  const BasicCardDemo({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基本カード表示')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // 基本的なカード
            Card(
              // カードの影を設定
              elevation: 4,
              // カードの角丸を設定
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8),
              ),
              child: Padding(
                // カード内の余白を設定
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // カードのタイトル
                    const Text(
                      'カードタイトル',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    // カードの内容
                    const Text(
                      'これは基本的なカードの内容です。',
                      style: TextStyle(fontSize: 14),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            // 複数のカードを縦に並べる
            Card(
              elevation: 4,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8),
              ),
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'もう一つのカード',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      '2つ目のカードの内容です。',
                      style: TextStyle(fontSize: 14),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

メリット

  • 実装が簡単
  • 基本的なカード表示ができる
  • 理解しやすい

デメリット

  • グリッド表示ができない
  • レスポンシブ対応ができない
  • カードの数が多いと見づらい

🔄 方法2:GridViewでグリッド表示

GridViewを使ってカードをグリッド形式で表示する方法です。

class GridCardDemo extends StatelessWidget {
  const GridCardDemo({super.key});

  
  Widget build(BuildContext context) {
    // サンプルデータ
    final List<String> cardTitles = [
      'カード1',
      'カード2',
      'カード3',
      'カード4',
      'カード5',
      'カード6',
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('グリッドカード表示')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: GridView.builder(
          // グリッドの設定
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            // 1行あたりのカード数
            crossAxisCount: 2,
            // カードの縦横比
            childAspectRatio: 1.2,
            // カード間の縦の間隔
            mainAxisSpacing: 16,
            // カード間の横の間隔
            crossAxisSpacing: 16,
          ),
          // カードの総数
          itemCount: cardTitles.length,
          // 各カードの作成
          itemBuilder: (context, index) {
            return Card(
              // カードの影を設定
              elevation: 4,
              // カードの角丸を設定
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8),
              ),
              child: Padding(
                // カード内の余白を設定
                padding: const EdgeInsets.all(12),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    // カードのアイコン
                    const Icon(
                      Icons.star,
                      size: 32,
                      color: Colors.amber,
                    ),
                    const SizedBox(height: 8),
                    // カードのタイトル
                    Text(
                      cardTitles[index],
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 4),
                    // カードの説明
                    const Text(
                      'カードの説明',
                      style: TextStyle(fontSize: 12),
                      textAlign: TextAlign.center,
                    ),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

メリット

  • グリッド形式で表示できる
  • レスポンシブ対応ができる
  • カードのサイズが統一される

デメリット

  • 実装が少し複雑
  • スクロール可能なリストではない
  • カードの数が多いと画面からはみ出る

🎯 方法3:ScrollViewでスクロール可能なグリッド

SingleChildScrollViewGridViewを組み合わせて、スクロール可能なグリッドを作成する方法です。

実装のポイント

  1. ScrollViewでスクロール可能にする
SingleChildScrollView(
  child: GridView.builder(
    // スクロール可能にするための設定
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    // グリッドの設定
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 2,
      childAspectRatio: 1.2,
      mainAxisSpacing: 16,
      crossAxisSpacing: 16,
    ),
    // カードの作成
    itemBuilder: (context, index) => Card(...),
  ),
)
  1. カスタムカードウィジェットを作成
class CustomCard extends StatelessWidget {
  final String title;
  final String description;
  final IconData icon;

  const CustomCard({
    super.key,
    required this.title,
    required this.description,
    required this.icon,
  });

  
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, size: 32, color: Colors.blue),
            const SizedBox(height: 8),
            Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 4),
            Text(description, style: const TextStyle(fontSize: 12)),
          ],
        ),
      ),
    );
  }
}
  1. データモデルを使用
class CardData {
  final String title;
  final String description;
  final IconData icon;

  CardData({
    required this.title,
    required this.description,
    required this.icon,
  });
}

メリット

  • スクロール可能
  • カードの数が多い場合でも対応
  • カスタマイズ性が高い

デメリット

  • 実装が複雑
  • パフォーマンスへの影響がある
  • 管理が大変

✅ 3つの方法を比較した完全版コード

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'カードグリッドレイアウトデモ',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const CardLayoutComparisonDemo(),
    );
  }
}

class CardLayoutComparisonDemo extends StatelessWidget {
  const CardLayoutComparisonDemo({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('カードレイアウト比較'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 方法1: 基本的なカード表示
            _buildSectionTitle('方法1: 基本的なカード表示'),
            _buildBasicCards(),
            const SizedBox(height: 24),
            
            // 方法2: GridViewでグリッド表示
            _buildSectionTitle('方法2: GridViewでグリッド表示'),
            _buildGridCards(),
            const SizedBox(height: 24),
            
            // 方法3: スクロール可能なグリッド
            _buildSectionTitle('方法3: スクロール可能なグリッド'),
            _buildScrollableGridCards(),
            const SizedBox(height: 24),
            
            // 比較説明
            _buildComparisonInfo(),
          ],
        ),
      ),
    );
  }

  // セクションタイトルを表示するウィジェット
  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        title,
        style: const TextStyle(
          fontSize: 16,
          fontWeight: FontWeight.bold,
          color: Colors.blue,
        ),
      ),
    );
  }

  // 方法1: 基本的なカード表示
  Widget _buildBasicCards() {
    return Column(
      children: [
        Card(
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
          child: const Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '基本カード1',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  'これは基本的なカードの内容です。',
                  style: TextStyle(fontSize: 14),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(height: 16),
        Card(
          elevation: 4,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
          child: const Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '基本カード2',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 8),
                Text(
                  '2つ目のカードの内容です。',
                  style: TextStyle(fontSize: 14),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  // 方法2: GridViewでグリッド表示
  Widget _buildGridCards() {
    final List<String> cardTitles = ['カード1', 'カード2', 'カード3', 'カード4'];

    return SizedBox(
      height: 200, // 固定の高さを設定
      child: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 1.2,
          mainAxisSpacing: 16,
          crossAxisSpacing: 16,
        ),
        itemCount: cardTitles.length,
        itemBuilder: (context, index) {
          return Card(
            elevation: 4,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8),
            ),
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(
                    Icons.star,
                    size: 32,
                    color: Colors.amber,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    cardTitles[index],
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  const Text(
                    'グリッドカード',
                    style: TextStyle(fontSize: 12),
                    textAlign: TextAlign.center,
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  // 方法3: スクロール可能なグリッド
  Widget _buildScrollableGridCards() {
    final List<CardData> cardDataList = [
      CardData(
        title: 'スクロール1',
        description: 'スクロール可能なカード',
        icon: Icons.favorite,
      ),
      CardData(
        title: 'スクロール2',
        description: '2つ目のカード',
        icon: Icons.thumb_up,
      ),
      CardData(
        title: 'スクロール3',
        description: '3つ目のカード',
        icon: Icons.star,
      ),
      CardData(
        title: 'スクロール4',
        description: '4つ目のカード',
        icon: Icons.bookmark,
      ),
      CardData(
        title: 'スクロール5',
        description: '5つ目のカード',
        icon: Icons.share,
      ),
      CardData(
        title: 'スクロール6',
        description: '6つ目のカード',
        icon: Icons.download,
      ),
    ];

    return GridView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 1.2,
        mainAxisSpacing: 16,
        crossAxisSpacing: 16,
      ),
      itemCount: cardDataList.length,
      itemBuilder: (context, index) {
        final cardData = cardDataList[index];
        return CustomCard(
          title: cardData.title,
          description: cardData.description,
          icon: cardData.icon,
        );
      },
    );
  }

  // 比較情報を表示するウィジェット
  Widget _buildComparisonInfo() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.blue.shade50,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue.shade200),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            '📊 比較表',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          Text('• 方法1: 基本カード - 実装簡単、グリッド不可'),
          Text('• 方法2: グリッド表示 - レスポンシブ、スクロール不可'),
          Text('• 方法3: スクロールグリッド - 高機能、実装複雑'),
        ],
      ),
    );
  }
}

// カスタムカードウィジェット
class CustomCard extends StatelessWidget {
  final String title;
  final String description;
  final IconData icon;

  const CustomCard({
    super.key,
    required this.title,
    required this.description,
    required this.icon,
  });

  
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, size: 32, color: Colors.blue),
            const SizedBox(height: 8),
            Text(
              title,
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 4),
            Text(
              description,
              style: const TextStyle(fontSize: 12),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

// カードデータモデル
class CardData {
  final String title;
  final String description;
  final IconData icon;

  CardData({
    required this.title,
    required this.description,
    required this.icon,
  });
}

🎯 使い分けのポイント

方法1(基本カード)を使う場合

  • カードの数が少ない場合
  • グリッド表示が不要な場合
  • 実装を簡単に済ませたい場合

方法2(グリッド表示)を使う場合

  • カードをグリッド形式で表示したい場合
  • カードの数が限定的な場合
  • レスポンシブ対応が必要な場合

方法3(スクロールグリッド)を使う場合

  • カードの数が多い場合
  • スクロール機能が必要な場合
  • カスタマイズ性を重視する場合

🧭 おわりに

Flutterでカード形式のグリッドレイアウトを作成する際は、用途に応じて適切な方法を選択することが重要です。

今回紹介した3つの方法を参考に、自分のアプリに最適なカードレイアウトを実装してみてください!

Discussion