Flutterのbody

Flutter/Dart

この記事では、Flutterのbodyについて書いていきます。

bodyとは

Scaffoldウィジェットが持つメインコンテンツ用の表示画面です。

画面の上部に表示される appBar や、下部に表示されるbottomNavigationBar などを除いた、アプリ画面の中央大部分のを描画しています。

bodyの対応している型はWidget?なのでFlutterが持つウィジェットを一つだけ直接セットすることができます。

よく使用されるウィジェット

要素を配置、大まかなレイアウトをする

ウィジェット名説明
Container幅・高さ、余白(margin/padding)、背景色、角丸などをまとめて設定する万能枠組み。
Padding余白(padding)を設定するためだけの軽量ウィジェット。
Center子ウィジェットを上下左右の中央に配置。
Align子ウィジェットを任意の領域(右上、左下など)に配置。
Column複数のウィジェットを**垂直(縦)**方向に並べる。
Row複数のウィジェットを**水平(横)**方向に並べる。
Stack複数のウィジェットを**前後(重なり)**方向に重ねて配置。
SizedBox固定の幅や高さを指定したり、ウィジェット同士の隙間(スペース)を作る。

画面以上のコンテンツを表示する場合

ウィジェット名説明
SingleChildScrollView画面サイズを超えた際に、単一の子要素全体をスクロール可能にする。
ListViewリスト形式で複数の要素を上下(または左右)にスクロール表示。
GridViewグリッド(2列・3列などのマス目状)形式で要素を表示。
CustomScrollViewSliver ウィジェットと組み合わせて高度なスクロール効果(ヘッダー伸縮など)を実現。

レスポンシブデザイン

ウィジェット名説明
ExpandedRow や Column 内で、残りの利用可能な領域をいっぱいに広げて占有する。
FlexibleRow や Column 内で、指定の比率に応じて高さを調整する(子要素の大きさに収まることも可能)。
FittedBox親のサイズに合わせて子要素を拡大・縮小してフィットさせる。
SafeAreaノッチ(画面上部の凹み)やホームバーなどのシステム領域を避けて描画する。

サンプル

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const CustomBodyExample(),
    );
  }
}

class CustomBodyExample extends StatefulWidget {
  const CustomBodyExample({super.key});

  @override
  State<CustomBodyExample> createState() => _CustomBodyExampleState();
}

class _CustomBodyExampleState extends State<CustomBodyExample> {
  // リフレッシュ処理(引っ張って更新)の模擬処理
  Future<void> _onRefresh() async {
    await Future.delayed(const Duration(seconds: 1));
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scaffold.body 総合デモ'),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
      ),

      // =========================================================
      // body のメイン構成要素
      // 1. SafeArea : ノッチや画面下部バーなどの干渉を防止
      // 2. RefreshIndicator : 上引っ張り更新機能
      // 3. SingleChildScrollView : 画面全体の縦スクロールを有効化
      // =========================================================
      body: SafeArea(
        child: RefreshIndicator(
          onRefresh: _onRefresh,
          child: SingleChildScrollView(
            // キーボード表示時や画面サイズオーバー時のオーバーフローを防止
            physics: const AlwaysScrollableScrollPhysics(),
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 1. レスポンシブな横並び配置 (Row + Expanded)
                const Text(
                  '1. レイアウト構成 (Row & Expanded)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8.0),
                Row(
                  children: [
                    Expanded(
                      flex: 2,
                      child: Container(
                        height: 60,
                        color: Colors.indigo.shade100,
                        child: const Center(child: Text('Flex: 2')),
                      ),
                    ),
                    const SizedBox(width: 8.0),
                    Expanded(
                      flex: 1,
                      child: Container(
                        height: 60,
                        color: Colors.indigo.shade300,
                        child: const Center(child: Text('Flex: 1')),
                      ),
                    ),
                  ],
                ),

                const Divider(height: 32.0, thickness: 1.0),

                // 2. 要素の重なり配置 (Stack) & 装飾付き領域 (Container)
                const Text(
                  '2. 重ね合わせ & カードデザイン (Stack & Container)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8.0),
                Stack(
                  children: [
                    Container(
                      width: double.infinity,
                      padding: const EdgeInsets.all(20.0),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(12.0),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black.withOpacity(0.1),
                            blurRadius: 10,
                            offset: const Offset(0, 4),
                          ),
                        ],
                      ),
                      child: const Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            'カスタムカード',
                            style: TextStyle(
                                fontSize: 16, fontWeight: FontWeight.bold),
                          ),
                          SizedBox(height: 4.0),
                          Text('Container と BoxDecoration を使ったスタイリング'),
                        ],
                      ),
                    ),
                    // バッジなどを右上に重ねる
                    Positioned(
                      top: 12,
                      right: 12,
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: Colors.orange,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: const Text(
                          'NEW',
                          style: TextStyle(
                              color: Colors.white,
                              fontSize: 10,
                              fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),
                  ],
                ),

                const Divider(height: 32.0, thickness: 1.0),

                // 3. 高速な動的一覧表示 (ListView.builder)
                const Text(
                  '3. 動的一覧リスト (ListView.builder)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8.0),
                // SingleChildScrollView 内で ListView を動かすための設定
                ListView.builder(
                  shrinkWrap: true, // コンテンツに応じた高さにする
                  physics: const NeverScrollableScrollPhysics(), // スクロールは親に任せる
                  itemCount: 3,
                  itemBuilder: (context, index) {
                    return Card(
                      margin: const EdgeInsets.only(bottom: 8.0),
                      child: ListTile(
                        leading: CircleAvatar(child: Text('${index + 1}')),
                        title: Text('リストアイテム項目 #${index + 1}'),
                        subtitle: const Text('サブタイトル情報'),
                        trailing: const Icon(Icons.chevron_right),
                        onTap: () {},
                      ),
                    );
                  },
                ),

                const Divider(height: 32.0, thickness: 1.0),

                // 4. 非同期データの読み込み状態表示 (FutureBuilder)
                const Text(
                  '4. 非同期UI切り替え (FutureBuilder)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8.0),
                FutureBuilder<String>(
                  future: Future.delayed(
                    const Duration(seconds: 2),
                    () => '読み込み完了データ',
                  ),
                  builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return const Center(
                        child: Padding(
                          padding: EdgeInsets.all(16.0),
                          child: CircularProgressIndicator(),
                        ),
                      );
                    } else if (snapshot.hasError) {
                      return Text('エラーが発生しました: ${snapshot.error}');
                    } else {
                      return Container(
                        padding: const EdgeInsets.all(12.0),
                        decoration: BoxDecoration(
                          color: Colors.green.shade50,
                          borderRadius: BorderRadius.circular(8.0),
                        ),
                        child: Row(
                          children: [
                            const Icon(Icons.check_circle, color: Colors.green),
                            const SizedBox(width: 8.0),
                            Text(snapshot.data ?? ''),
                          ],
                        ),
                      );
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

タイトルとURLをコピーしました