0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter入門 第8回】Widget の基本 ― StatelessWidget と MaterialApp の仕組み

0
Posted at

はじめに

Flutter の UI は、すべて Widget で構成されています。テキスト、ボタン、画像、レイアウト、さらにはアプリ全体の設定まで、あらゆるものが Widget です。

この記事では、Flutter の基本思想である「すべてが Widget」を理解し、StatelessWidget の書き方と主要な Widget の使い方を学びます。

注意: この記事のコードは Flutter プロジェクト内で動作します。import 'package:flutter/material.dart'; を含めた完全なコードを掲載しています。実行には flutter run を使用してください。


Flutter の基本思想:「すべてが Widget」

Flutter では、目に見える UI 要素(テキスト、ボタン、画像)だけでなく、レイアウト(パディング、中央寄せ)やスタイル(テーマ)もすべて Widget です。

Widget を組み合わせてツリー構造(Widget ツリー)を構築することで、画面を作ります。

Widget ツリーの概念

以下は典型的な Widget ツリーの構造です。

MaterialApp
└── Scaffold
    ├── AppBar
    │   └── Text('タイトル')
    └── body: Center
        └── Column
            ├── Icon(Icons.flutter_dash)
            ├── Text('Hello')
            └── ElevatedButton
                └── Text('押してね')

親 Widget が子 Widget を内包する入れ子構造になっています。この構造がそのまま画面のレイアウトを決定します。


StatelessWidget の書き方

StatelessWidget は、状態(State)を持たない Widget です。一度構築されると、外部からのデータが変わらない限り再描画されません。

基本構造

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(
      title: 'StatelessWidget Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const GreetingPage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('挨拶ページ'),
      ),
      body: const Center(
        child: Text(
          'こんにちは、Flutter!',
          style: TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

StatelessWidget のポイント:

  1. StatelessWidget を継承する
  2. const コンストラクタを定義する(const MyWidget({super.key});
  3. build() メソッドをオーバーライドして Widget ツリーを返す
  4. @override アノテーションを付ける

コンストラクタで引数を受け取る

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(
      home: const Scaffold(
        body: Center(
          child: UserCard(
            name: '田中太郎',
            role: 'Flutter エンジニア',
          ),
        ),
      ),
    );
  }
}

class UserCard extends StatelessWidget {
  final String name;
  final String role;

  const UserCard({
    super.key,
    required this.name,
    required this.role,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(name, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
        const SizedBox(height: 8),
        Text(role, style: const TextStyle(fontSize: 16, color: Colors.grey)),
      ],
    );
  }
}

MaterialApp

MaterialApp は Material Design に基づいたアプリの骨格を提供するトップレベルの Widget です。

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(
      // アプリのタイトル(タスクスイッチャー等で表示される)
      title: 'My Flutter App',

      // アプリ全体のテーマ設定
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
        // アプリ全体のテキストテーマ
        textTheme: const TextTheme(
          headlineMedium: TextStyle(fontWeight: FontWeight.bold),
        ),
      ),

      // デバッグバナーを非表示にする
      debugShowCheckedModeBanner: false,

      // 最初に表示する画面
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('MaterialApp デモ'),
      ),
      body: Center(
        child: Text(
          'テーマカラーが Teal に設定されています',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      ),
    );
  }
}

MaterialApp の主要プロパティ

プロパティ 説明
title String アプリのタイトル
theme ThemeData ライトテーマの設定
darkTheme ThemeData? ダークテーマの設定
themeMode ThemeMode テーマモード(system / light / dark
home Widget? 最初に表示する画面
debugShowCheckedModeBanner bool デバッグバナーの表示制御
routes Map<String, WidgetBuilder>? 名前付きルートの定義

Scaffold

Scaffold は Material Design の画面レイアウト構造を提供する Widget です。

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 ScaffoldDemo(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 画面上部のアプリバー
      appBar: AppBar(
        title: const Text('Scaffold デモ'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),

      // 画面の本体
      body: const Center(
        child: Text('ここがメインコンテンツ'),
      ),

      // 浮遊アクションボタン
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ボタンタップ時の処理
        },
        child: const Icon(Icons.add),
      ),

      // 画面下部のナビゲーションバー
      // ※ BottomNavigationBar のタップで選択状態を切り替えるには
      //    StatefulWidget + currentIndex + onTap が必要です(第10回で解説)
      bottomNavigationBar: BottomNavigationBar(
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'ホーム',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: '検索',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'プロフィール',
          ),
        ],
      ),

      // サイドドロワー(ハンバーガーメニュー)
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text(
                'メニュー',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.home),
              title: const Text('ホーム'),
              onTap: () {},
            ),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('設定'),
              onTap: () {},
            ),
          ],
        ),
      ),
    );
  }
}

Scaffold の主要プロパティ

プロパティ 説明
appBar PreferredSizeWidget? 画面上部のアプリバー
body Widget? 画面のメインコンテンツ
floatingActionButton Widget? 浮遊アクションボタン
bottomNavigationBar Widget? 画面下部のナビゲーション
drawer Widget? 左からスライドするドロワー
endDrawer Widget? 右からスライドするドロワー
backgroundColor Color? 背景色

AppBar

AppBar は画面上部に表示されるアプリケーションバーです。

import 'package:flutter/material.dart';

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 左端のアイコン(通常は戻るボタンやメニューアイコン)
        leading: IconButton(
          icon: const Icon(Icons.menu),
          onPressed: () {},
        ),

        // タイトル
        title: const Text('AppBar デモ'),

        // タイトルを中央に配置するか
        centerTitle: true,

        // 背景色
        backgroundColor: Colors.indigo,

        // テキストとアイコンの色
        foregroundColor: Colors.white,

        // 右端のアクションアイコン
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () {},
          ),
          IconButton(
            icon: const Icon(Icons.notifications),
            onPressed: () {},
          ),
          IconButton(
            icon: const Icon(Icons.more_vert),
            onPressed: () {},
          ),
        ],

        // AppBar の下に表示する Widget(タブバーなど)
        // bottom: TabBar(...),

        // 影の高さ
        elevation: 4,
      ),
      body: const Center(
        child: Text('AppBar のデモ画面'),
      ),
    );
  }
}

Text Widget

Text は文字列を表示する最も基本的な Widget です。

import 'package:flutter/material.dart';

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Text Widget')),
      body: const Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 基本的な Text
            Text('基本のテキスト'),
            SizedBox(height: 16),

            // フォントサイズ指定
            Text(
              'フォントサイズ 28',
              style: TextStyle(fontSize: 28),
            ),
            SizedBox(height: 16),

            // 太字
            Text(
              '太字のテキスト',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 16),

            // 色指定
            Text(
              '赤いテキスト',
              style: TextStyle(color: Colors.red),
            ),
            SizedBox(height: 16),

            // 複数のスタイルを組み合わせ
            Text(
              'カスタムスタイル',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.w600,
                color: Colors.deepPurple,
                letterSpacing: 2.0,
                decoration: TextDecoration.underline,
                decorationColor: Colors.deepPurple,
              ),
            ),
            SizedBox(height: 16),

            // イタリック
            Text(
              'イタリック体',
              style: TextStyle(
                fontStyle: FontStyle.italic,
                fontSize: 18,
              ),
            ),
            SizedBox(height: 16),

            // テキストの折り返しと省略
            Text(
              'これは長いテキストです。maxLines を指定すると指定行数で切り詰められます。overflow で省略記号を表示できます。テキストが長い場合の挙動を確認してみましょう。',
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

TextStyle の主要プロパティ

プロパティ 説明
fontSize double? フォントサイズ
fontWeight FontWeight? 太さ(FontWeight.boldFontWeight.w600 等)
color Color? 文字色
fontStyle FontStyle? FontStyle.italic / FontStyle.normal
letterSpacing double? 文字間隔
wordSpacing double? 単語間隔
decoration TextDecoration? 装飾(下線、取り消し線等)
height double? 行の高さ(倍率)

Icon Widget

Icon はマテリアルデザインアイコンを表示する Widget です。

import 'package:flutter/material.dart';

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Icon Widget')),
      body: const Padding(
        padding: EdgeInsets.all(16.0),
        child: Wrap(
          spacing: 24,
          runSpacing: 24,
          children: [
            // 基本的なアイコン
            Icon(Icons.home),

            // サイズ指定
            Icon(Icons.star, size: 48),

            // 色指定
            Icon(Icons.favorite, color: Colors.red, size: 48),

            // 様々なアイコン
            Icon(Icons.settings, size: 36),
            Icon(Icons.search, size: 36),
            Icon(Icons.person, size: 36),
            Icon(Icons.email, size: 36),
            Icon(Icons.phone, size: 36),
            Icon(Icons.camera_alt, size: 36),
            Icon(Icons.cloud, size: 36, color: Colors.blue),
            Icon(Icons.flutter_dash, size: 48, color: Colors.cyan),
          ],
        ),
      ),
    );
  }
}

利用できるアイコンの一覧は Material Icons で確認できます。Icons.xxx の形式で使用します。


Image Widget

Image Widget は画像を表示するために使用します。

Image.network(ネットワーク画像)

import 'package:flutter/material.dart';

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Image Widget')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // ネットワーク画像
            Image.network(
              'https://storage.googleapis.com/cms-storage-bucket/c823e53b3a1a7b0d36a9.png',
              height: 100,
            ),
            const SizedBox(height: 16),

            // サイズとフィットを指定
            ClipRRect(
              borderRadius: BorderRadius.circular(16),
              child: Image.network(
                'https://storage.googleapis.com/cms-storage-bucket/c823e53b3a1a7b0d36a9.png',
                width: 200,
                height: 100,
                fit: BoxFit.contain,
              ),
            ),
            const SizedBox(height: 16),

            const Text(
              'Image.network はネットワーク上の画像を読み込みます',
              style: TextStyle(fontSize: 14, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

Image.asset(ローカルアセット画像)

ローカル画像を使用するには、まず pubspec.yaml で画像のパスを登録します。

flutter:
  uses-material-design: true
  assets:
    - assets/images/

プロジェクトのルートに assets/images/ ディレクトリを作成し、画像を配置します。

// Image.asset の使用例(pubspec.yaml でアセット登録が必要)
Image.asset(
  'assets/images/logo.png',
  width: 200,
  height: 200,
  fit: BoxFit.contain,
),

BoxFit の種類

説明
BoxFit.contain アスペクト比を維持して収まるように表示
BoxFit.cover アスペクト比を維持して領域を埋めるように表示
BoxFit.fill 領域に合わせて引き伸ばす(歪みあり)
BoxFit.fitWidth 幅に合わせる
BoxFit.fitHeight 高さに合わせる
BoxFit.none 元のサイズのまま表示

ボタン Widget

Flutter には用途に応じた複数のボタン Widget があります。

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,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ButtonDemo(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ボタン Widget')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // ElevatedButton: 影付きの塗りつぶしボタン
            ElevatedButton(
              onPressed: () {
                debugPrint('ElevatedButton がタップされました');
              },
              child: const Text('ElevatedButton'),
            ),
            const SizedBox(height: 16),

            // TextButton: テキストのみのフラットなボタン
            TextButton(
              onPressed: () {
                debugPrint('TextButton がタップされました');
              },
              child: const Text('TextButton'),
            ),
            const SizedBox(height: 16),

            // OutlinedButton: 枠線付きのボタン
            OutlinedButton(
              onPressed: () {
                debugPrint('OutlinedButton がタップされました');
              },
              child: const Text('OutlinedButton'),
            ),
            const SizedBox(height: 16),

            // IconButton: アイコンのみのボタン
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  icon: const Icon(Icons.thumb_up),
                  onPressed: () {},
                  color: Colors.blue,
                  iconSize: 32,
                ),
                IconButton(
                  icon: const Icon(Icons.thumb_down),
                  onPressed: () {},
                  color: Colors.red,
                  iconSize: 32,
                ),
              ],
            ),
            const SizedBox(height: 16),

            // アイコン付きボタン
            ElevatedButton.icon(
              onPressed: () {},
              icon: const Icon(Icons.send),
              label: const Text('送信する'),
            ),
            const SizedBox(height: 16),

            // スタイルをカスタマイズしたボタン
            ElevatedButton(
              onPressed: () {},
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.deepOrange,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 16),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(30),
                ),
                textStyle: const TextStyle(fontSize: 18),
              ),
              child: const Text('カスタムスタイルボタン'),
            ),
            const SizedBox(height: 16),

            // 無効化されたボタン(onPressed を null にする)
            const ElevatedButton(
              onPressed: null,
              child: Text('無効なボタン'),
            ),
          ],
        ),
      ),
    );
  }
}

ボタンの種類

Widget 見た目 用途
ElevatedButton 塗りつぶし + 影 主要なアクション
TextButton テキストのみ 補助的なアクション
OutlinedButton 枠線のみ 中程度の重要度のアクション
IconButton アイコンのみ ツールバー等のアイコンアクション

レイアウト補助 Widget

Padding

Widget の周囲に余白を追加します。

Padding(
  padding: const EdgeInsets.all(16.0),          // 全方向 16
  child: Text('パディング付きテキスト'),
),

Padding(
  padding: const EdgeInsets.symmetric(
    horizontal: 24.0,    // 左右 24
    vertical: 8.0,       // 上下 8
  ),
  child: Text('左右と上下で異なるパディング'),
),

Padding(
  padding: const EdgeInsets.only(
    left: 16.0,
    top: 8.0,
  ),
  child: Text('個別指定のパディング'),
),

Center

子 Widget を親の中央に配置します。

Center(
  child: Text('中央に配置されたテキスト'),
),

SizedBox

固定サイズのボックスを作成します。Widget 間のスペーサーとしてよく使われます。

Column(
  children: [
    Text('上のテキスト'),
    SizedBox(height: 24),    // 縦方向のスペース
    Text('下のテキスト'),
  ],
),

Row(
  children: [
    Text('左'),
    SizedBox(width: 16),     // 横方向のスペース
    Text('右'),
  ],
),

// 固定サイズのボックス
SizedBox(
  width: 200,
  height: 100,
  child: DecoratedBox(
    decoration: BoxDecoration(color: Colors.blue),
    child: Center(child: Text('200x100')),
  ),
),

Divider

水平方向の区切り線を表示します。

Column(
  children: [
    Text('セクション 1'),
    Divider(),                          // デフォルトの区切り線
    Text('セクション 2'),
    Divider(thickness: 2, color: Colors.red),  // カスタム区切り線
    Text('セクション 3'),
  ],
),

実践例: プロフィールカード画面の作成

ここまで学んだ Widget を組み合わせて、プロフィールカード画面を作成します。

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'プロフィールカード',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const ProfilePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('プロフィール'),
        centerTitle: true,
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () {},
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // ヘッダー部分
            Container(
              width: double.infinity,
              color: Theme.of(context).colorScheme.inversePrimary,
              padding: const EdgeInsets.only(bottom: 24),
              child: const Column(
                children: [
                  // アバター(アイコンで代用)
                  CircleAvatar(
                    radius: 50,
                    backgroundColor: Colors.white,
                    child: Icon(
                      Icons.person,
                      size: 60,
                      color: Colors.indigo,
                    ),
                  ),
                  SizedBox(height: 16),
                  Text(
                    '山田 太郎',
                    style: TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 4),
                  Text(
                    'Flutter Developer',
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.black54,
                    ),
                  ),
                ],
              ),
            ),

            // 統計情報
            const Padding(
              padding: EdgeInsets.symmetric(vertical: 16),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  _StatItem(label: '投稿', count: '128'),
                  _StatItem(label: 'フォロワー', count: '1.2K'),
                  _StatItem(label: 'フォロー中', count: '356'),
                ],
              ),
            ),

            const Divider(),

            // プロフィール情報リスト
            const _ProfileInfoTile(
              icon: Icons.email,
              title: 'メール',
              subtitle: 'taro.yamada@example.com',
            ),
            const _ProfileInfoTile(
              icon: Icons.location_on,
              title: '所在地',
              subtitle: '東京都渋谷区',
            ),
            const _ProfileInfoTile(
              icon: Icons.work,
              title: '職業',
              subtitle: 'モバイルアプリエンジニア',
            ),
            const _ProfileInfoTile(
              icon: Icons.link,
              title: 'ウェブサイト',
              subtitle: 'https://example.com',
            ),

            const SizedBox(height: 24),

            // 自己紹介
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '自己紹介',
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(height: 8),
                  Text(
                    'Flutter と Dart が大好きなエンジニアです。'
                    'クロスプラットフォーム開発の魅力を日々発信しています。'
                    'お気軽にフォローしてください!',
                    style: TextStyle(
                      fontSize: 15,
                      height: 1.6,
                      color: Colors.black87,
                    ),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 24),

            // アクションボタン
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Row(
                children: [
                  Expanded(
                    child: ElevatedButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.person_add),
                      label: const Text('フォローする'),
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: OutlinedButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.message),
                      label: const Text('メッセージ'),
                      style: OutlinedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 32),
          ],
        ),
      ),
    );
  }
}

// 統計アイテム Widget
class _StatItem extends StatelessWidget {
  final String label;
  final String count;

  const _StatItem({
    required this.label,
    required this.count,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          count,
          style: const TextStyle(
            fontSize: 22,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 4),
        Text(
          label,
          style: const TextStyle(
            fontSize: 14,
            color: Colors.grey,
          ),
        ),
      ],
    );
  }
}

// プロフィール情報タイル Widget
class _ProfileInfoTile extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;

  const _ProfileInfoTile({
    required this.icon,
    required this.title,
    required this.subtitle,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon, color: Colors.indigo),
      title: Text(title),
      subtitle: Text(subtitle),
    );
  }
}

Widget ツリーの構造

上記のプロフィールカード画面の Widget ツリーは以下のようになります。

MaterialApp
└── Scaffold
    ├── AppBar
    │   ├── Text('プロフィール')
    │   └── IconButton(Icons.edit)
    └── body: SingleChildScrollView
        └── Column
            ├── Container(ヘッダー)
            │   └── Column
            │       ├── CircleAvatar
            │       │   └── Icon(Icons.person)
            │       ├── Text('山田 太郎')
            │       └── Text('Flutter Developer')
            ├── Row(統計情報)
            │   ├── _StatItem('投稿')
            │   ├── _StatItem('フォロワー')
            │   └── _StatItem('フォロー中')
            ├── Divider
            ├── _ProfileInfoTile(メール)
            ├── _ProfileInfoTile(所在地)
            ├── _ProfileInfoTile(職業)
            ├── _ProfileInfoTile(ウェブサイト)
            ├── Column(自己紹介)
            │   ├── Text('自己紹介')
            │   └── Text('Flutter と Dart が...')
            └── Row(ボタン)
                ├── ElevatedButton('フォローする')
                └── OutlinedButton('メッセージ')

このように、小さな Widget を組み合わせて大きな画面を構築するのが Flutter の基本的な考え方です。_StatItem_ProfileInfoTile のように再利用可能な Widget を切り出すことで、コードの見通しが良くなります。


まとめ

この記事で学んだ Widget を整理します。

カテゴリ Widget 用途
アプリ構造 MaterialApp アプリ全体の設定
レイアウト Scaffold 画面の基本構造
ナビゲーション AppBar 画面上部のバー
表示 Text テキスト表示
表示 Icon アイコン表示
表示 Image 画像表示
操作 ElevatedButton 塗りつぶしボタン
操作 TextButton テキストボタン
操作 OutlinedButton 枠線ボタン
補助 Padding 余白の追加
補助 Center 中央寄せ
補助 SizedBox 固定サイズ / スペーサー
補助 Divider 区切り線

次の記事では、StatefulWidget を使った状態管理を学び、ユーザー操作に応じて画面を動的に変化させる方法を解説します。


参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?