Flutter初心者の学習記録 第19回:レイアウトウィジェット – Stack, ListView, GridView

こんにちは、Flutterエンジニアの皆さん!Flutterでアプリを開発する際、適切なレイアウトウィジェットを選択することは、ユーザーフレンドリーで美しいUIを作成するために不可欠です。今回は、特に重要な3つのレイアウトウィジェット「Stack」「ListView」「GridView」について、実践的な使い方とベストプラクティスを詳しく解説します。

Stackウィジェット: 重ね合わせレイアウトの基本

Stackウィジェットは、子ウィジェットを重ね合わせて配置するためのウィジェットです。写真の上にテキストを重ねたり、フローティングアクションボタンを配置したりする際に威力を発揮します。

基本的な使い方

Stack(
  children: [
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    Positioned(
      top: 50,
      left: 50,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    ),
  ],
)

Positionedウィジェットとの組み合わせ

Stackの真価はPositionedウィジェットと組み合わせることで発揮されます。以下のプロパティで位置を制御できます。

  • top: 上からの距離
  • bottom: 下からの距離
  • left: 左からの距離
  • right: 右からの距離
Stack(
  children: [
    Container(
      width: double.infinity,
      height: 300,
      decoration: BoxDecoration(
        image: DecorationImage(
          image: AssetImage('assets/background.jpg'),
          fit: BoxFit.cover,
        ),
      ),
    ),
    Positioned(
      bottom: 20,
      right: 20,
      child: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
    ),
    Positioned(
      top: 20,
      left: 20,
      child: Text(
        'タイトル',
        style: TextStyle(
          fontSize: 24,
          fontWeight: FontWeight.bold,
          color: Colors.white,
        ),
      ),
    ),
  ],
)

Stackのalignmentプロパティ

Positionedで囲まれていない子ウィジェットの配置は、alignmentプロパティで制御できます。

Stack(
  alignment: Alignment.center,
  children: [
    Container(
      width: 300,
      height: 300,
      color: Colors.grey[300],
    ),
    CircularProgressIndicator(),
  ],
)

ListViewウィジェット: スクロール可能なリスト

ListViewは、縦または横にスクロール可能なリストを作成するためのウィジェットです。大量のデータを効率的に表示でき、Flutterアプリで最も頻繁に使用されるウィジェットの一つです。

基本的なListView

ListView(
  children: [
    ListTile(
      leading: Icon(Icons.map),
      title: Text('地図'),
      subtitle: Text('現在地を表示'),
    ),
    ListTile(
      leading: Icon(Icons.photo_album),
      title: Text('アルバム'),
      subtitle: Text('写真を管理'),
    ),
    ListTile(
      leading: Icon(Icons.phone),
      title: Text('電話'),
      subtitle: Text('連絡先一覧'),
    ),
  ],
)

ListView.builder: 効率的なリスト構築

大量のデータを扱う場合は、ListView.builderを使用することで、必要な分だけウィジェットを生成し、メモリ効率を向上させることができます。

ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          child: Text('${index + 1}'),
        ),
        title: Text('アイテム ${index + 1}'),
        subtitle: Text('これは${index + 1}番目のアイテムです'),
        trailing: Icon(Icons.arrow_forward_ios),
        onTap: () {
          print('アイテム ${index + 1} がタップされました');
        },
      ),
    );
  },
)

ListView.separated: 区切り線付きリスト

アイテム間に区切り線を追加したい場合は、ListView.separatedが便利です。

ListView.separated(
  itemCount: 20,
  separatorBuilder: (context, index) => Divider(
    color: Colors.grey,
    thickness: 1,
  ),
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('項目 ${index + 1}'),
      subtitle: Text('詳細情報'),
    );
  },
)

横スクロールのListView

scrollDirectionプロパティを使用して、横スクロールのリストを作成できます。

Container(
  height: 200,
  child: ListView.builder(
    scrollDirection: Axis.horizontal,
    itemCount: 10,
    itemBuilder: (context, index) {
      return Container(
        width: 150,
        margin: EdgeInsets.all(8),
        decoration: BoxDecoration(
          color: Colors.blue[100 * (index % 9 + 1)],
          borderRadius: BorderRadius.circular(10),
        ),
        child: Center(
          child: Text('カード ${index + 1}'),
        ),
      );
    },
  ),
)

GridViewウィジェット: グリッドレイアウト

GridViewは、写真ギャラリーやカタログ表示など、グリッド状にアイテムを配置したい場合に使用します。

GridView.count: 固定列数のグリッド

GridView.count(
  crossAxisCount: 2,
  crossAxisSpacing: 10,
  mainAxisSpacing: 10,
  padding: EdgeInsets.all(10),
  children: List.generate(20, (index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.blue[100 * (index % 9 + 1)],
        borderRadius: BorderRadius.circular(10),
      ),
      child: Center(
        child: Text(
          'アイテム ${index + 1}',
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }),
)

GridView.builder: 効率的なグリッド構築

大量のデータを扱う場合は、GridView.builderを使用します。

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    crossAxisSpacing: 4,
    mainAxisSpacing: 4,
    childAspectRatio: 1,
  ),
  itemCount: 100,
  itemBuilder: (context, index) {
    return Card(
      elevation: 2,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.image,
            size: 40,
            color: Colors.grey[600],
          ),
          SizedBox(height: 8),
          Text('画像 ${index + 1}'),
        ],
      ),
    );
  },
)

GridView.extent: 最大幅指定のグリッド

アイテムの最大幅を指定してグリッドを作成する場合は、GridView.extentを使用します。

GridView.extent(
  maxCrossAxisExtent: 200,
  crossAxisSpacing: 10,
  mainAxisSpacing: 10,
  padding: EdgeInsets.all(10),
  children: [
    Container(color: Colors.red, child: Center(child: Text('1'))),
    Container(color: Colors.green, child: Center(child: Text('2'))),
    Container(color: Colors.blue, child: Center(child: Text('3'))),
    Container(color: Colors.yellow, child: Center(child: Text('4'))),
    Container(color: Colors.purple, child: Center(child: Text('5'))),
    Container(color: Colors.orange, child: Center(child: Text('6'))),
  ],
)

実践的な組み合わせ例

これらのウィジェットを組み合わせることで、より複雑で実用的なレイアウトを作成できます。

写真ギャラリーアプリの例

class PhotoGalleryScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('フォトギャラリー'),
      ),
      body: Stack(
        children: [
          GridView.builder(
            padding: EdgeInsets.all(8),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 8,
              mainAxisSpacing: 8,
              childAspectRatio: 1,
            ),
            itemCount: 50,
            itemBuilder: (context, index) {
              return Container(
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Stack(
                  children: [
                    ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Container(
                        width: double.infinity,
                        height: double.infinity,
                        color: Colors.blue[100 * (index % 9 + 1)],
                        child: Icon(
                          Icons.photo,
                          size: 50,
                          color: Colors.white,
                        ),
                      ),
                    ),
                    Positioned(
                      bottom: 8,
                      right: 8,
                      child: Container(
                        padding: EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.black54,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          '${index + 1}',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 12,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              );
            },
          ),
          Positioned(
            bottom: 20,
            right: 20,
            child: FloatingActionButton(
              onPressed: () {},
              child: Icon(Icons.add_a_photo),
            ),
          ),
        ],
      ),
    );
  }
}

パフォーマンスのベストプラクティス

1. 適切なウィジェットの選択

  • 少数のアイテム: 通常のListViewやGridView
  • 大量のアイテム: ListView.builderやGridView.builder
  • 無限スクロール: ListView.builderと組み合わせた遅延読み込み

2. メモリ効率の向上

ListView.builder(
  itemCount: items.length,
  cacheExtent: 200, // キャッシュする範囲を制限
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index].title),
    );
  },
)

3. 画像の最適化

GridViewで画像を表示する際は、適切なサイズの画像を使用し、必要に応じてキャッシュを活用します。

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
  ),
  itemBuilder: (context, index) {
    return Image.network(
      imageUrls[index],
      fit: BoxFit.cover,
      loadingBuilder: (context, child, loadingProgress) {
        if (loadingProgress == null) return child;
        return Center(
          child: CircularProgressIndicator(
            value: loadingProgress.expectedTotalBytes != null
                ? loadingProgress.cumulativeBytesLoaded /
                  loadingProgress.expectedTotalBytes!
                : null,
          ),
        );
      },
    );
  },
)

まとめ

Stack、ListView、GridViewは、Flutterアプリ開発において欠かせないレイアウトウィジェットです。それぞれの特性を理解し、適切に組み合わせることで、ユーザーにとって使いやすく魅力的なUIを構築できます。

  • Stack: ウィジェットの重ね合わせに使用
  • ListView: スクロール可能なリスト表示に最適
  • GridView: グリッド状のレイアウトを作成

これらのウィジェットをマスターすることで、Flutterでのアプリ開発がより効率的で楽しいものになるでしょう。実際のプロジェクトで積極的に活用し、ユーザーエクスペリエンスの向上を図ってください。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です