「Flutterを始めてみたいけど、何から手をつければいいかわからない」
「公式ドキュメントは英語だし、結局どのウィジェットを覚えればアプリが作れるの?」
そんな疑問を持つ方に向けて、この記事ではFlutterの基本ウィジェットから画面遷移、状態管理まで、実際に動くコード付きで段階的に解説します。
Flutterの開発は「ウィジェットというブロックを積み重ねる」感覚で進められるため、他のフレームワークと比較しても直感的に理解しやすいのが特徴です。
記事の最後には、学んだウィジェットを組み合わせた簡単なTODOアプリの作例も用意しています。
読み終わる頃には「自分でもアプリが作れそう」と感じられるはずです。
Flutterの基本
まず、Flutterの前提知識を手短に押さえておきましょう。
Flutterとは
Flutterは、Googleが開発したオープンソースのUIフレームワークです。
プログラミング言語「Dart」を使い、1つのコードベースからiOS・Android・Web・デスクトップアプリを同時に開発できます。
2018年12月にバージョン1.0がリリースされ、2026年5月時点の最新安定版はFlutter 3.44(Dart 3.12)です。
四半期ごとに安定版がリリースされるため、バージョンは今後も更新されていきます。
Google Pay、Nubank、BMW、LG webOS TVをはじめ、多くの企業が本番アプリに採用しており、月間150万人以上の開発者が利用するフレームワークに成長しています(Google I/O 2026発表)。
Flutterの最大の特徴は、UIのすべてが「ウィジェット」で構成されるという設計思想です。
テキストもボタンも余白もレイアウトも、すべてウィジェット。
ブロックを組み合わせるような感覚で画面を作れるため、初心者でも「何が何を担当しているか」が把握しやすい構造になっています。
もう一つ、開発体験で特筆すべきなのがHot Reloadです。
コードを保存した瞬間、アプリを再起動することなく変更が画面に反映されます。
「テキストの色を変えて保存→即座に反映を確認→余白を調整して保存→また即確認」というサイクルが数秒で回るため、UIの試行錯誤が苦になりません。
実際に使ってみると、このスピード感は想像以上で、「ちょっと試してみよう」のハードルが一気に下がります。
環境構築の概要
Flutter開発を始めるには、以下の3つを準備します。
- Flutter SDK — Flutter公式ドキュメントからダウンロード
- エディタ — VS Code(軽量で拡張機能が充実)またはAndroid Studio(エミュレータ管理が楽)
- プラットフォーム別ツール — Androidアプリ開発ならAndroid SDK、iOSならXcode
Scaffold — 画面の土台
Flutterアプリの各画面は、基本的に Scaffold というウィジェットの上に組み立てます。Scaffold は画面上部の AppBar(タイトルバー)、メインの body、下部の BottomNavigationBar などを配置するための土台です。
Scaffold(
appBar: AppBar(title: Text('マイアプリ')),
body: Center(child: Text('ここがメインコンテンツ')),
)
この記事で後述するコード例はすべて Scaffold の body 内に配置する想定で書いています。
なお、コード内では final(再代入不可の変数宣言)、const(コンパイル時定数)、=>(アロー関数)、?.(null安全アクセス)といったDart構文が登場します。
Dart未経験でも読み進められるよう配慮していますが、文法を体系的に学びたい場合はDart公式チュートリアルを併用してみてください。
インストール後、ターミナルで flutter doctor を実行すれば、環境に不足がないかチェックマーク付きで確認できます。
flutter doctor
全項目にチェックが付けば準備完了です。
環境構築の詳細手順はFlutter公式ドキュメント(英語)に記載されています。
日本語で進めたい場合は、有志運営の翻訳サイトFlutter Doc JPも参考になります。
ここではウィジェットの実践に進みましょう。
表示系ウィジェットの基本
Flutterの画面を構成する基本中の基本、「何かを表示する」ためのウィジェットです。
Text — 文字を表示する
もっともシンプルなのが Text です。
文字列を画面に表示し、style プロパティでフォントサイズ・太さ・色を調整できます。
Text(
'こんにちは、Flutter!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
)
TextStyle のプロパティは豊富ですが、最初は fontSize・fontWeight・color の3つを覚えておけば十分です。
複数行のテキストを省略表示にしたい場合は maxLines と overflow: TextOverflow.ellipsis を組み合わせます。
Icon — アイコンを表示する
Flutterにはマテリアルデザインのアイコンが標準で組み込まれています。
外部ライブラリをインストールする必要がなく、Icons.〇〇 で数千種類のアイコンにアクセスできます。
Icon(
Icons.favorite,
size: 32,
color: Colors.red,
)
使えるアイコンの一覧は Material Icons で検索できます。
Flutterではアイコン名がスネークケース(例: Icons.arrow_back)になる点だけ注意してください。
Image — 画像を表示する
画像の表示方法は大きく2つあります。
ローカル画像(assetsフォルダ):
Image.asset('assets/images/logo.png')
pubspec.yaml で assets フォルダのパスを登録する必要があります。
flutter:
assets:
- assets/images/
ネットワーク画像:
Image.network('https://example.com/photo.jpg')
ネットワーク画像は読み込みに時間がかかるため、本番アプリでは cached_network_image パッケージの利用がおすすめです。
Container — 装飾の万能ウィジェット
Container は背景色・余白・枠線・角丸など、見た目の装飾をまとめて担当するウィジェットです。
HTMLでいう div にCSSを当てたものに近い感覚で使えます。
Container(
width: 200,
height: 100,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Text('カード風のデザイン'),
)
decoration に BoxDecoration を渡すことで、角丸・影・グラデーションなど多彩な表現が可能です。
ポイントとして、color を Container 直下と BoxDecoration の両方に指定するとエラーになります。
装飾を使うときは必ず decoration 内で色を指定してください。
これはFlutter初学者がほぼ確実に一度は遭遇するエラーで、エラーメッセージに Cannot provide both a color and a decoration と出たらこのパターンです。
レイアウトの組み方
表示系ウィジェットを覚えたら、次は「画面のどこに何を配置するか」を制御するレイアウトです。
Flutterではレイアウトもウィジェットです。
CSSのflexboxに相当する操作を、ウィジェットの入れ子で表現します。
Row・Column — 横並びと縦並び
Row は子要素を横方向に、Column は縦方向に並べます。
Flutterのレイアウトで最も使用頻度が高い組み合わせです。
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('タイトル', style: TextStyle(fontSize: 20)),
Text('サブタイトル', style: TextStyle(color: Colors.grey)),
Row(
children: [
Icon(Icons.star, color: Colors.amber),
SizedBox(width: 4),
Text('4.8'),
],
),
],
)
mainAxisAlignment は並びの方向(Columnなら縦、Rowなら横)の配置を制御します。crossAxisAlignment はその垂直方向の配置です。
覚えておくと便利な値は以下の3つです。
MainAxisAlignment.start— 先頭寄せ(デフォルト)MainAxisAlignment.center— 中央揃えMainAxisAlignment.spaceBetween— 両端に寄せて均等配置
SizedBox・Spacer・Expanded — 余白と伸縮
SizedBox は固定サイズの余白を作ります。
UI部品間にスペースを空けたいときに重宝します。
SizedBox(height: 16) // 縦方向に16ピクセルの余白
SizedBox(width: 8) // 横方向に8ピクセルの余白
Expanded は残りのスペースを埋めるように伸びる部品です。
Row や Column の中で使います。
Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(hintText: '検索...'),
),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
child: Text('検索'),
),
],
)
この例では、検索フィールドがボタンを除いた残りの幅をすべて使って伸びます。
Spacer は Expanded の簡略版で、空白スペースを埋めるためだけに使います。
Row(
children: [
Text('左寄せのテキスト'),
Spacer(),
Text('右寄せのテキスト'),
],
)
Stack・Positioned — ウィジェットの重ね合わせ
Stack は子要素を重ねて配置します。
プロフィール画像の上にバッジを乗せたり、背景画像の上にテキストを表示するときに使います。
Stack(
children: [
Container(
width: 100,
height: 100,
color: Colors.blue,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Center(child: Text('3', style: TextStyle(color: Colors.white))),
),
),
],
)
Positioned で top・bottom・left・right を指定して、重ねる位置を細かく制御できます。
ListView — スクロール可能なリスト
画面に収まりきらない量のコンテンツを表示するとき、ListView を使います。
少数の固定アイテムなら直接 children に並べる書き方もあります。
ListView(
children: [
ListTile(title: Text('項目1')),
ListTile(title: Text('項目2')),
ListTile(title: Text('項目3')),
],
)
データ件数が多い場合は ListView.builder を使います。
画面に表示される分だけウィジェットを生成するため、メモリ効率が良くなります。
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('アイテム ${index + 1}'),
subtitle: Text('説明テキスト'),
);
},
)
ListTile はリスト1行分のレイアウトを簡単に作れるウィジェットで、leading(左端)・title・subtitle・trailing(右端)を指定するだけで整ったリスト表示になります。
初心者がハマりやすいポイント: Column の中に ListView をそのまま配置すると Vertical viewport was given unbounded height というエラーが出ます。
Columnは子要素のサイズを聞きますが、ListViewは「スクロールできるだけ無限に伸びたい」と答えるため、サイズが決まらず衝突するのが原因です。
解決策は、ListView を Expanded で包んで「残りのスペースを使って」と伝えることです。
このエラーはFlutter開発で非常によく遭遇するので、覚えておくとデバッグ時間を大幅に節約できます。
Padding — 内側の余白
Padding は子ウィジェットの周囲に余白を追加します。
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('余白付きのテキスト'),
)
EdgeInsets の主な使い方を整理しておきます。
EdgeInsets.all(16)— 上下左右すべて16EdgeInsets.symmetric(horizontal: 16, vertical: 8)— 左右16、上下8EdgeInsets.only(top: 8, left: 16)— 特定の方向だけ指定
外側の余白(margin)は Container の margin プロパティで指定します。
Padding専用ウィジェットと Container の padding の違いは機能的にはありませんが、装飾が不要なら Padding を使う方がコードの意図が明確になるでしょう。
操作系ウィジェット
画面を表示できるようになったら、次はユーザーの操作を受け取るウィジェットです。
ElevatedButton・TextButton・IconButton — ボタンの種類
Flutterのボタンは用途に応じて3種類を使い分けます。
// 塗りつぶしボタン(主要なアクション)
ElevatedButton(
onPressed: () {
print('タップされた');
},
child: Text('保存'),
)
// テキストのみのボタン(補助的なアクション)
TextButton(
onPressed: () {},
child: Text('キャンセル'),
)
// アイコンだけのボタン
IconButton(
onPressed: () {},
icon: Icon(Icons.delete),
)
すべてのボタンに共通するのは onPressed プロパティです。
ここにタップ時の処理を書きます。onPressed を null にするとボタンが無効化(グレーアウト)されるのも覚えておくと便利です。
TextField — テキスト入力
ユーザーからテキスト入力を受け取るには TextField を使います。
入力内容をプログラムから取得するには TextEditingController を使います。
class _MyFormState extends State<MyForm> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose(); // メモリリーク防止のため必ず破棄
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: '名前',
hintText: '名前を入力してください',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
print('入力値: ${_controller.text}');
},
child: Text('送信'),
),
],
);
}
}
TextEditingController は dispose() メソッドで破棄する必要があります。
これを忘れるとメモリリークの原因になるため、StatefulWidgetの dispose 内で必ず呼び出してください。
Checkbox・Switch・Radio — 選択系ウィジェット
ON/OFFの切り替えや選択肢の提示に使います。
// チェックボックス
Checkbox(
value: _isChecked,
onChanged: (bool? value) {
setState(() {
_isChecked = value ?? false;
});
},
)
// スイッチ
Switch(
value: _isEnabled,
onChanged: (bool value) {
setState(() {
_isEnabled = value;
});
},
)
どちらも現在の値(value)と変更時のコールバック(onChanged)をセットで渡す形式です。
この「現在の値を保持して、変更時にsetStateで更新する」パターンは、Flutterの状態管理の基本形なので、ここでしっかり馴染んでおきましょう。
GestureDetector・InkWell — タップ検知
ボタン以外の要素にタップ操作を追加したいときは GestureDetector または InkWell で包みます。
// タップの波紋エフェクト付き
InkWell(
onTap: () {
print('カードがタップされた');
},
child: Container(
padding: EdgeInsets.all(16),
child: Text('タップできるカード'),
),
)
InkWell はマテリアルデザインの波紋(リップル)エフェクトが付きます。
エフェクトが不要な場合や、タップ以外のジェスチャー(長押し、スワイプ等)を検知したい場合は GestureDetector を使います。
状態管理の基本(setState)
ここまでの内容は「表示するだけ」でしたが、実際のアプリではボタンを押したら数字が増える、チェックを入れたら表示が変わる、といった動的な変化が必要です。
Flutterで画面を動的に更新する最も基本的な方法が setState です。
StatelessWidgetとStatefulWidgetの違い
Flutterのウィジェットは2種類に分かれます。
- StatelessWidget — 状態を持たない。一度描画したら変わらない(表示だけのUI)
- StatefulWidget — 状態を持つ。ユーザー操作やデータ変更に応じて画面を再描画する
使い分けの基準はシンプルで、画面が変化する必要があるならStatefulWidget、ないならStatelessWidgetです。
// StatelessWidget — 変化しないUI
class Greeting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('こんにちは');
}
}
// StatefulWidget — 変化するUI
class Counter extends StatefulWidget {
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_count', style: TextStyle(fontSize: 48)),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: Text('カウントアップ'),
),
],
);
}
}
setStateの使い方
setState は「この中で変数を変更したら、画面を再描画してね」とFlutterに伝える命令です。
上のカウンター例では、ボタンを押すたびに _count が1増え、画面のテキストが即座に更新されます。
重要なルールが1つあります。setState のコールバック内で変更する変数は、そのStateクラスのフィールドとして宣言すること。build メソッド内のローカル変数を変更しても画面は更新されません。
実際にカウンターアプリを作ると、最初はつい build の中に変数を書いてしまい「タップしても数字が変わらない」と悩むことが多いです。
以下のNG例とOK例を見比べてみてください。
// NG — build内のローカル変数を変更しても反映されない
@override
Widget build(BuildContext context) {
int count = 0; // 再描画のたびに0にリセットされる
return ElevatedButton(
onPressed: () {
setState(() { count++; }); // 意味がない
},
child: Text('$count'),
);
}
// OK — Stateクラスのフィールドを変更する
class _CounterState extends State<Counter> {
int _count = 0; // ここで宣言
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
setState(() { _count++; }); // 正しく反映される
},
child: Text('$_count'),
);
}
}
setStateの限界と次のステップ
setState はシンプルで覚えやすい反面、アプリの規模が大きくなると課題が出てきます。
- 親で
setStateを呼ぶと、子孫まで全体が再描画される - 離れたWidget間で状態を共有するのが難しい(バケツリレーになる)
- ビジネスロジックとUIが混在しやすくなる
これらの課題を解決するのが、Riverpod・BLoC・Providerなどの状態管理パッケージです。
「setStateでは管理しきれないな」と感じたタイミングが、状態管理ライブラリに進むベストなタイミングです。
まずはsetStateで小さなアプリを一つ作り切ってから検討すれば十分でしょう。
画面遷移(Navigator)
アプリが1画面だけで完結することはほとんどありません。
Flutterでは Navigator を使って画面の遷移を管理します。
Navigator.pushとpop — 基本の画面遷移
新しい画面を開くのが push、前の画面に戻るのが pop です。
// 画面Aから画面Bに遷移
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
child: Text('次の画面へ'),
)
// 画面Bから画面Aに戻る
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('戻る'),
)
MaterialPageRoute はプラットフォームに応じた遷移アニメーション(Androidはスライド、iOSは右からスワイプイン)を自動で適用してくれます。
Androidの端末に搭載されている戻るボタンでの遷移にも自動対応します。
画面間のデータ受け渡し
遷移先の画面にデータを渡すには、コンストラクタの引数を使う方法がもっとも簡単です。
// 遷移先の画面
class DetailPage extends StatelessWidget {
final String title;
final int id;
const DetailPage({required this.title, required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('ID: $id')),
);
}
}
// 遷移元で値を渡す
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(title: 'アイテム詳細', id: 42),
),
);
遷移先から遷移元にデータを返したい場合は、Navigator.pop の第2引数に値を渡し、push 側で await して受け取ります。
// 遷移先から値を返す
Navigator.pop(context, '選択されたアイテム');
// 遷移元で受け取る
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionPage()),
);
print('受け取った値: $result');
名前付きルート(routes)
画面が5つ、10つと増えてくると、遷移のたびに MaterialPageRoute を書くのは煩雑です。MaterialApp の routes プロパティで画面を一覧管理すると、可読性が上がります。
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
)
// 名前で遷移
Navigator.pushNamed(context, '/detail');
さらに画面管理が複雑になる場合(ディープリンク対応、認証による遷移制御など)は、go_router パッケージの導入を検討してみてください。
実践:簡単なTODOアプリを作ってみよう
ここまで学んだウィジェットを組み合わせて、シンプルなTODOアプリを作ってみましょう。
完成イメージと使うウィジェット
- テキスト入力(
TextField)でタスクを追加 - リスト表示(
ListView.builder)でタスク一覧を表示 - チェックボックス(
Checkbox)で完了/未完了を切り替え - 削除ボタン(
IconButton)でタスクを削除 - 状態管理は
setStateで実装
コード全文と解説
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: 'TODO App',
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const TodoPage(),
);
}
}
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final _controller = TextEditingController();
final List<Map<String, dynamic>> _todos = [];
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() {
_todos.add({'title': text, 'done': false});
});
_controller.clear();
}
void _toggleTodo(int index) {
setState(() {
_todos[index]['done'] = !_todos[index]['done'];
});
}
void _deleteTodo(int index) {
setState(() {
_todos.removeAt(index);
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TODOリスト')),
body: Column(
children: [
// 入力エリア
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'タスクを入力...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addTodo,
child: const Text('追加'),
),
],
),
),
// タスク一覧
Expanded(
child: _todos.isEmpty
? const Center(
child: Text(
'タスクがありません',
style: TextStyle(color: Colors.grey),
),
)
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return ListTile(
leading: Checkbox(
value: todo['done'],
onChanged: (_) => _toggleTodo(index),
),
title: Text(
todo['title'],
style: TextStyle(
decoration: todo['done']
? TextDecoration.lineThrough
: TextDecoration.none,
color: todo['done']
? Colors.grey
: Colors.black,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteTodo(index),
),
);
},
),
),
],
),
);
}
}
コードのポイント解説:
- 入力エリアは
Row+Expanded+SizedBoxの組み合わせ。Expandedで入力欄が伸縮し、ボタンは固定幅を維持します - タスクの状態を
List<Map<String, dynamic>>で管理し、追加・完了切り替え・削除のたびにsetStateで画面を更新しています - 完了済みタスクは取り消し線(
TextDecoration.lineThrough)とグレー表示で視覚的に区別 - 空の状態で「タスクがありません」を表示する三項演算子は、Flutterアプリでよく使うパターンです
onSubmittedを指定しているため、キーボードのEnterキーでもタスクを追加できます
このTODOアプリは30行弱のロジック(_addTodo・_toggleTodo・_deleteTodo)で動いています。
ウィジェットの組み合わせとsetStateだけで、これだけの機能が実現できるのがFlutterの強みです。
ただし、ここにタスクの編集機能やカテゴリ分けを追加しようとすると、setStateだけでは状態の管理が一気に煩雑になってきます。
「そろそろ限界だな」と感じたタイミングが、RiverpodやBLoCといった状態管理ライブラリに進むベストな頃合いです。
まとめ
この記事では、Flutter入門として基本ウィジェットから画面遷移、状態管理まで実践コード付きで解説しました。
表示系ウィジェット(Text・Icon・Image・Container)で画面に要素を配置し、レイアウトウィジェット(Row・Column・Stack・ListView)で配置を制御する。
操作系ウィジェット(Button・TextField・Checkbox)でユーザーの入力を受け取り、setStateで画面を動的に更新する。
Navigatorで複数画面を行き来させる。
この一連の流れを押さえれば、TODOアプリのようなシンプルなアプリは自力で作れるようになります。
ここから先の学習ステップとしては、まずRiverpod等の状態管理パッケージに進むとアプリの規模に対応しやすくなります。
その後、Firebase連携(認証・データベース)やgo_router(高度な画面遷移管理)を学べば、本格的なアプリ開発の土台が整うでしょう。
最初から完璧を目指す必要はありません。
まずはこの記事のTODOアプリを動かしてみて、「ここを変えたらどうなるだろう」と試すところから始めてみてください。

コメント