Flutter

レイアウトと画面遷移

スクロールできる一覧から詳細画面へ移動し、値を渡して戻る流れを作ります。

対象:Dart 3を同梱したFlutter 3系のstable版を対象に、まずChromeで動かすWebアプリを作ります。

01

準備と画面の設計

Flutter入門のWidgetとListViewまでを使います。study_appのlib/main.dartを次の完成例で全体ごと置き換え、flutter run -d chrome で実行してください。外部パッケージは不要です。一覧と詳細を別のWidgetに分け、選んだ講座名を渡します。

02

一覧と詳細の完成例

Navigatorは画面の履歴を管理します。push で新しい画面を上に積み、pop で取り除いて戻ります。MaterialPageRouteのbuilderは移動先のWidgetを作る関数です。

lib/main.dart(全体を置き換え)DART
import 'package:flutter/material.dart';

void main() => runApp(const StudyApp());
class StudyApp extends StatelessWidget {
  const StudyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: LessonListPage());
  }
}
class LessonListPage extends StatelessWidget {
  const LessonListPage({super.key});
  @override
  Widget build(BuildContext context) {
    const lessons = ['変数', '関数', 'Widget'];
    return Scaffold(
      appBar: AppBar(title: const Text('学習一覧')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: lessons.length,
        itemBuilder: (context, index) {
          final title = lessons[index];
          return Card(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8),
            ),
            child: ListTile(
              title: Text(title),
              subtitle: const Text('タップして目標を確認'),
              onTap: () {
                Navigator.of(context).push<void>(
                  MaterialPageRoute<void>(
                    builder: (context) => DetailPage(title: title),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}
class DetailPage extends StatelessWidget {
  final String title;
  const DetailPage({super.key, required this.title});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('${title}の例を1つ動かしましょう'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () { Navigator.of(context).pop(); },
              child: const Text('一覧へ戻る'),
            ),
          ],
        ),
      ),
    );
  }
}
画面で確認する結果OUTPUT
最初は3件の学習一覧
「関数」をタップするとタイトルが「関数」の詳細画面
「一覧へ戻る」で元の3件の一覧へ戻る
03

BuildContextと値の受け渡し

Navigator.of(context)は、そのcontextより上にあるNavigatorを探します。完成例ではMaterialAppの下にあるLessonListPageのcontextを使っています。MaterialAppを作っているStudyApp自身のcontextでは、そのMaterialAppのNavigatorを見つけられません。

DetailPageの required this.title に講座名を渡します。値を直接渡すことで、詳細画面が一覧の内部変数へ依存せずに表示できます。まず「変数」「Widget」も開き、選択した値が変わることを確かめます。

確認する結果OUTPUT
「変数」を選ぶ → 変数の例を1つ動かしましょう
「Widget」を選ぶ → Widgetの例を1つ動かしましょう
04

幅と高さの制約

Flutterの親Widgetは子に幅・高さの制約を渡します。Columnの中にListViewを置く場合は、残りの高さをExpandedで渡します。完成例のLessonListPageのbodyを次に置き換えて試せます。これはレイアウト確認用なので、行のタップ処理は含めていません。

LessonListPageのScaffold.bodyを置き換えDART
Column(
  children: [
    const Padding(
      padding: EdgeInsets.all(16),
      child: Text('上から順に学びましょう'),
    ),
    Expanded(
      child: ListView.builder(
        itemCount: lessons.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(lessons[index]));
        },
      ),
    ),
  ],
)
画面で確認する結果OUTPUT
案内文が上に固定され、その下の一覧が残りの高さを使う
高さの制限がないListViewは「Vertical viewport was given unbounded height」などのエラーになります。Expandedをスクロール領域の中へ無条件に置くのではなく、親が利用可能な高さを持っているかを確認しましょう。
05

練習:4件目を追加

完成例へ戻し、一覧に「状態管理」を追加してください。詳細画面にも選んだ名前が表示されることを確認します。

解答例を確認する
LessonListPage.build内のlessons宣言を置き換えDART
const lessons = ['変数', '関数', 'Widget', '状態管理'];
画面で確認する結果OUTPUT
4件の一覧が表示される
「状態管理」を開くと「状態管理の例を1つ動かしましょう」
「一覧へ戻る」で4件の一覧へ戻る

WebのURLとの連動や深いリンクが必要なアプリでは、Routerやルーティングパッケージを学びます。このページは画面遷移の基礎として、アプリ内のpush/popを扱いました。

公式ドキュメントで確認する:画面を開いて戻る / レイアウトの制約