初心者から始めるFlutter 4.0入門:実践的TODOアプリで学ぶクロスプラットフォーム開発のすべて
アフィリエイト開示

こんにちは!長年、業務系からエンタメ系まで様々なアプリ開発に携わってきたプログラミング教育者です。子供の頃に初めてコードに触れた時のワクワク感を、今も大切にしています。今日は、一つのコードでiOSもAndroidも動かせる魔法のようなフレームワーク「Flutter」の最新バージョンを使って、アプリ開発の第一歩を踏み出しましょう!
この記事は、単にコードの書き方を教えるだけではありません。なぜその技術が必要なのか、チームで開発するならどう書くべきか、そして何より「作る楽しさ」を体感してもらうことを目指します。一緒に、手を動かしながら、アイデアを形にする冒険に出かけましょう!
はじめに
この記事では、Flutter 4.0を使って、基本的な「TODOアプリ」をゼロから開発します。このチュートリアルを通して、あなたは以下のことを学び、体験することができます。
- Flutter開発の基本フロー: 環境構築からアプリの実行までの一連の流れを理解します。
- 宣言的UIの考え方: 「画面は状態の写し鏡である」というFlutterの核心的な概念を掴みます。
- ウィジェットの組み合わせ: 小さな部品(ウィジェット)を組み合わせて複雑な画面を構築する楽しさを実感します。
- 状態管理の基礎: アプリのデータを管理し、ユーザーの操作に応じて画面を更新する方法を学びます。
最終的には、自分で作ったアプリが手元のスマートフォン(のエミュレータ)で動くという、感動的な成功体験を手にすることができます。この小さな一歩が、あなたの開発者としての大きな飛躍に繋がるはずです。
前提知識の確認
必要な基礎知識
プログラミングが全く初めてという方でも挑戦できますが、もし他のプログラミング言語(例えばJavaScript, Python, Javaなど)で以下の概念に触れたことがあると、よりスムーズに学習を進められます。
- 変数: データを格納する箱
- データ型: 文字列(String)、数値(int, double)、真偽値(bool)などの種類
- 関数: 特定の処理をまとめたもの
- if文: 条件によって処理を分ける
- for文: 繰り返し処理
事前に理解しておきたい概念
- オブジェクト指向: 「モノ」としてプログラムの要素(データと処理)を捉える考え方です。Flutterでは、画面の部品からアプリ全体まで、あらゆるものが「オブジェクト」として扱われます。難しく考えず、「役割を持った部品を作る」くらいのイメージで大丈夫です。
「分からなくても大丈夫」な部分
- Dart言語の高度な機能: 最初からDart言語のすべてをマスターする必要はありません。記事で使う構文を「こういう風に書くんだな」と真似するところから始めましょう。
- 複雑な状態管理: この記事では最も基本的な状態管理手法を使います。ProviderやRiverpodといった高度なライブラリは、次のステップで学んでいけば十分です。
完璧な理解を目指すより、まずは動くものを作る楽しさを優先しましょう!
環境構築:最初の一歩
開発を始めるための準備をします。ここは最初の関門ですが、一つ一つ丁寧に進めれば必ず乗り越えられます。
開発環境の準備(初心者向け解説)
Flutterアプリを開発するには、主に以下の3つが必要です。
- Flutter SDK: Flutterの本体です。アプリをビルドするためのツールやライブラリが含まれています。
- 統合開発環境(IDE): コードを書くためのエディタです。
Visual Studio Code
(VS Code)が無料で高機能なので、特におすすめです。 - 実行環境: 作成したアプリを動かすための環境です。iOSなら
Xcode
とiOS Simulator
、AndroidならAndroid Studio
とAndroid Emulator
が必要になります。
必要なツールとインストール方法
-
Flutter SDKのインストール: 公式サイトの手順に従って、お使いのOS(Windows, macOS, Linux)用のSDKをダウンロードし、任意の場所に展開します。その後、「パスを通す」という作業が必要です。これは、コマンドプロンプトやターミナルで
flutter
コマンドをどこからでも使えるようにするための設定です。 -
Visual Studio Codeのインストール: 公式サイトからダウンロードしてインストールします。インストール後、拡張機能マーケットプレイスで「Flutter」と検索し、公式の拡張機能をインストールしてください。これにより、コードの補完やデバッグが非常にやりやすくなります。
-
実行環境のセットアップ:
- iOS向け (macOSのみ): App Storeから
Xcode
をインストールします。インストール後、一度起動してライセンスに同意し、追加コンポーネントのインストールを完了させてください。 - Android向け:
Android Studio
を公式サイトからインストールします。インストールウィザードに従い、Android SDKやエミュレータを設定します。
- iOS向け (macOSのみ): App Storeから
-
動作確認: ターミナルまたはコマンドプロンプトを開き、以下のコマンドを実行します。
flutter doctor
このコマンドは、環境構築が正しくできているかを診断してくれます。
[✓]
が付いている項目はOK、[!]
や[✗]
が付いている項目は、メッセージに従って追加の設定やインストールが必要です。

環境構築でつまずきやすいポイント
- パスが通らない: Flutter SDKを展開したフォルダの
bin
ディレクトリへのパスが、環境変数に正しく設定されているか何度も確認しましょう。設定後はターミナルを再起動するのを忘れずに。 - Androidライセンスの同意:
flutter doctor
で促された場合は、flutter doctor --android-licenses
コマンドを実行して、表示されるすべてのライセンスにy
で同意してください。 - Xcodeのコマンドラインツール:
xcode-select --install
コマンドを実行して、コマンドラインツールがインストールされているか確認しましょう。
焦らず、エラーメッセージをよく読むことが解決への近道です。
基本概念の理解
コードを書き始める前に、Flutterの心臓部とも言える考え方を学びましょう。
核となる考え方
Flutterの最も重要なコンセプトは 「すべてはウィジェットである (Everything is a Widget)」 です。
画面に表示されるボタン、テキスト、レイアウトのための余白、さらにはアプリ全体に至るまで、すべてが「ウィジェット」という部品で構成されています。これらのウィジェットをプラモデルのように組み合わせることで、アプリケーションのUIを構築していきます。これを 宣言的UI と呼びます。
身近な例での説明
宣言的UIをレゴブロックで例えてみましょう。
- 命令的UI(従来の開発手法): 「赤いブロックをA地点に置け。次に、青いブロックをその隣に置け。」と、一つ一つの手順を指示する方法です。
- 宣言的UI(Flutter): 「A地点には赤いブロックと青いブロックが隣り合って並んでいる『完成図』」を定義するだけです。Flutterは、その完成図を見て、必要なブロックを自動的に配置してくれます。
私たちは「どう作るか」ではなく、「何を作るか(どんな見た目か)」をコードで記述することに集中できます。
「なぜそうなるのか」の理解
Flutterには大きく分けて2種類のウィジェットがあります。
StatelessWidget
(状態を持たないウィジェット): 一度描画されたら見た目が変わらない、静的な部品です。例えば、アプリのタイトルやアイコンなどです。まるで印刷された写真のように、変化しません。StatefulWidget
(状態を持つウィジェット): ユーザーの操作などによって見た目が変わる可能性がある、動的な部品です。例えば、チェックボックスや入力フォームなどです。State
(状態)と呼ばれるデータを保持しており、そのデータが変わると、ウィジェットは自分自身を再描画して見た目を更新します。まるで、表示内容が変わる電光掲示板のようなものです。

この「状態」が変わったら「UI」が自動で再構築される、という仕組みが、インタラクティブなアプリを作るための鍵となります。
実践編:手を動かして学ぶ
いよいよTODOアプリを作っていきます。VS Codeで新しいFlutterプロジェクトを作成し、lib/main.dart
を開いて、中のコードをすべて削除してから始めましょう。
ステップ1: 基本的な実装
まずは、アプリの骨格となる静的な画面を作成します。タイトルと、からのリストを表示するだけのシンプルな状態です。
lib/main.dart
に以下のコードを記述してください。
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: 'Flutter TODO App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const TodoListPage(),
);
}
}
class TodoListPage extends StatelessWidget {
const TodoListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TODOリスト'),
),
body: ListView.builder(
itemCount: 0, // まだタスクがないので0
itemBuilder: (context, index) {
// ここでリストの各行を生成する
// 今は空なので何も表示されない
return const ListTile();
},
),
);
}
}
解説:
main()
: アプリの起動点です。runApp()
でアプリの根幹となるウィジェットMyApp
を呼び出します。MyApp
: アプリ全体の設定を行うStatelessWidget
です。MaterialApp
ウィジェットを返し、アプリのタイトルやテーマ、最初に表示する画面 (home
) を指定します。TodoListPage
: TODOリスト画面本体です。Scaffold
は画面の基本的な骨格(ヘッダー、ボディなど)を提供してくれる便利なウィジェットです。AppBar
: 画面上部のバーです。body
: 画面の主要部分です。ListView.builder
は、スクロール可能なリストを効率的に表示するためのウィジェットです。
このコードを実行(VS CodeでF5キー)すると、タイトルバーと空の画面が表示されるはずです。これが最初の成功体験です!
ステップ2: 機能の拡張
次に、タスクを追加できるようにします。そのためには、タスクのリストという「状態」を管理する必要があるので、TodoListPage
を StatefulWidget
に変更します。
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: 'Flutter TODO App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TodoListPage(),
);
}
}
class TodoListPage extends StatefulWidget {
const TodoListPage({super.key});
@override
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
// タスクのリストを状態として管理
final List<String> _todoItems = [];
// タスク追加のメソッド
void _addTodoItem(String task) {
if (task.isNotEmpty) {
setState(() {
_todoItems.add(task);
});
}
}
// タスク追加ダイアログを表示するメソッド
void _pushAddTodoScreen() {
final TextEditingController textFieldController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('新しいタスクを追加'),
content: TextField(
controller: textFieldController,
autofocus: true,
decoration: const InputDecoration(hintText: 'タスク内容を入力...'),
),
actions: <Widget>[
TextButton(
child: const Text('キャンセル'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('追加'),
onPressed: () {
_addTodoItem(textFieldController.text);
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TODOリスト'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView.builder(
itemCount: _todoItems.length, // リストの長さを指定
itemBuilder: (context, index) {
final item = _todoItems[index];
return ListTile(
title: Text(item),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddTodoScreen, // ボタンが押されたらダイアログ表示
tooltip: 'タスクを追加',
child: const Icon(Icons.add),
),
);
}
}
変更点の解説:
TodoListPage
をStatefulWidget
に変更し、実際の状態を管理する_TodoListPageState
を作成しました。_todoItems
という文字列のリストを状態として保持します。- 右下の
+
ボタン (FloatingActionButton
) を追加しました。これを押すと_pushAddTodoScreen
メソッドが呼ばれます。 _pushAddTodoScreen
は、テキスト入力ができるダイアログを表示します。- 「追加」ボタンが押されると
_addTodoItem
が呼ばれ、setState
の中で_todoItems
リストに新しいタスクを追加します。 setState()
を呼ぶことが重要です。これによりFlutterは「状態が変わった」ことを検知し、画面の必要な部分だけを再描画してくれます。
ステップ3: 実用的な応用
タスクの完了状態と削除機能を追加しましょう。タスクを単なる文字列ではなく、タイトルと完了状態を持つオブジェクトとして管理するように変更します。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
// タスクのデータ構造を定義するクラス
class TodoItem {
String title;
bool isDone;
TodoItem({required this.title, this.isDone = false});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter TODO App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const TodoListPage(),
);
}
}
class TodoListPage extends StatefulWidget {
const TodoListPage({super.key});
@override
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
final List<TodoItem> _todoItems = [];
void _addTodoItem(String task) {
if (task.isNotEmpty) {
setState(() {
_todoItems.add(TodoItem(title: task));
});
}
}
void _toggleTodoItem(int index) {
setState(() {
_todoItems[index].isDone = !_todoItems[index].isDone;
});
}
void _removeTodoItem(int index) {
setState(() {
_todoItems.removeAt(index);
});
}
void _pushAddTodoScreen() {
final TextEditingController textFieldController = TextEditingController();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('新しいタスクを追加'),
content: TextField(
controller: textFieldController,
autofocus: true,
decoration: const InputDecoration(hintText: 'タスク内容を入力...'),
),
actions: <Widget>[
TextButton(
child: const Text('キャンセル'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('追加'),
onPressed: () {
_addTodoItem(textFieldController.text);
Navigator.of(context).pop();
},
),
],
);
},
);
}
Widget _buildTodoList() {
return ListView.builder(
itemCount: _todoItems.length,
itemBuilder: (context, index) {
final item = _todoItems[index];
return ListTile(
leading: Checkbox(
value: item.isDone,
onChanged: (bool? value) {
_toggleTodoItem(index);
},
),
title: Text(
item.title,
style: TextStyle(
decoration: item.isDone ? TextDecoration.lineThrough : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_removeTodoItem(index);
},
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TODOリスト'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _buildTodoList(),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddTodoScreen,
tooltip: 'タスクを追加',
child: const Icon(Icons.add),
),
);
}
}
変更点の解説:
TodoItem
クラスを作成し、タスクのtitle
と完了状態isDone
を持つようにしました。_todoItems
はList<String>
からList<TodoItem>
に変わりました。ListTile
にCheckbox
と削除用のIconButton
を追加しました。- チェックボックスがタップされると
_toggleTodoItem
が呼ばれ、タスクの完了状態を切り替えます。完了したタスクには取り消し線が表示されるようにしました。 - 削除ボタンが押されると
_removeTodoItem
が呼ばれ、リストからタスクを削除します。 ListView.builder
の部分を_buildTodoList
というメソッドに切り出し、build
メソッドをスッキリさせました。
ステップ4: チーム開発を意識した改善
現在のコードは main.dart
1つのファイルにすべて書かれていますが、アプリが大きくなると管理が大変になります。そこで、ウィジェットを別のファイルに分割してみましょう。これは リファクタリング と呼ばれる作業で、コードの可読性と保守性を高めます。
lib
フォルダ内にwidgets
という新しいフォルダを作成します。widgets
フォルダ内にtodo_list_item.dart
というファイルを作成し、以下のコードを記述します。
lib/widgets/todo_list_item.dart
import 'package:flutter/material.dart';
import '../main.dart'; // TodoItemクラスをインポート
class TodoListItem extends StatelessWidget {
final TodoItem item;
final VoidCallback onToggle;
final VoidCallback onRemove;
const TodoListItem({
super.key,
required this.item,
required this.onToggle,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: item.isDone,
onChanged: (bool? value) => onToggle(),
),
title: Text(
item.title,
style: TextStyle(
decoration: item.isDone ? TextDecoration.lineThrough : null,
color: item.isDone ? Colors.grey : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: onRemove,
),
);
}
}
main.dart
を以下のように修正します。
lib/main.dart
(修正後)
import 'package:flutter/material.dart';
import 'widgets/todo_list_item.dart'; // 作成したファイルをインポート
// (中略... main, TodoItem, MyApp, TodoListPage, _TodoListPageStateクラスの定義は同じ)
// ただし、_TodoListPageState内の _buildTodoList メソッドを修正する
// ... _TodoListPageStateクラス内の他のメソッドはそのまま ...
Widget _buildTodoList() {
return ListView.builder(
itemCount: _todoItems.length,
itemBuilder: (context, index) {
final item = _todoItems[index];
// 分割したウィジェットを呼び出す
return TodoListItem(
item: item,
onToggle: () => _toggleTodoItem(index),
onRemove: () => _removeTodoItem(index),
);
},
);
}
// ... build メソッドはそのまま ...
改善点の解説:
- リストの各行を表示する
ListTile
の部分を、TodoListItem
という独立したStatelessWidget
に切り出しました。 TodoListItem
は、表示すべきタスクデータ (item
) と、ユーザー操作時の処理 (onToggle
,onRemove
) を親ウィジェットから受け取ります。- これにより、
TodoListPage
はリスト全体の管理に集中でき、個々のアイテムの見た目はTodoListItem
が担当するという役割分担が明確になりました。 - コードが部品化されたことで、再利用しやすくなり、どこで何をしているのかが格段に分かりやすくなります。これは、チームで開発する上で非常に重要な考え方です。
実際の開発現場での活用
業務での使用例
Flutterは、単なるTODOアプリだけでなく、大規模な業務アプリケーションでも活躍しています。例えば、在庫管理アプリ、顧客情報管理(CRM)アプリ、配送追跡アプリなど、多くの企業がFlutterを採用して、開発コストを抑えつつ高品質なアプリをiOSとAndroidに同時に提供しています。
チーム開発でのベストプラクティス
- バージョン管理:
Git
を使ってコードの変更履歴を管理することは必須です。main
ブランチを保護し、機能追加はfeature
ブランチを切って作業を進め、Pull Request
(またはMerge Request
)でレビューを受けてからマージするフローが一般的です。 - コードフォーマッタ: Flutterには強力なフォーマッタが標準で備わっています。VS Codeではファイルを保存するたびに自動でフォーマットするように設定できます。これにより、チーム内でコードの見た目が統一され、レビューがしやすくなります。
- フォルダ構成: 今回は
widgets
フォルダを作成しましたが、実際のプロジェクトではscreens
(画面ごと)、models
(データ構造)、services
(API通信など)のように、役割ごとにフォルダを分けてコードを整理します。
保守性を意識した書き方
- ウィジェットの分割: 今回実践したように、機能単位でウィジェットを小さく分割することは、保守性の基本です。一つのウィジェットが数百行にもなる「巨大ウィジェット」は避けましょう。
- 状態とUIの分離: 今回は
setState
を使いましたが、アプリが複雑になると、状態管理が難しくなります。その際はProvider
やRiverpod
といった状態管理ライブラリを導入し、ビジネスロジック(どうデータを扱うか)とUI(どう見せるか)を明確に分離することが推奨されます。 - マジックナンバーを避ける: コード内に直接
padding: EdgeInsets.all(16.0)
のように数値を書くのではなく、const double defaultPadding = 16.0;
のように定数として定義しておくと、後からデザインを修正する際に一箇所変更するだけで済みます。
よくあるつまずきポイントと解決策
初心者が陥りやすい問題
- ウィジェットツリーの親子関係エラー:
Column
やRow
の中に、サイズが無限に広がる可能性のあるListView
を直接入れてしまうとエラーになります。Expanded
やSizedBox
でサイズを制約してあげる必要があります。 setState
の呼び忘れ: データを更新したのに画面が変わらない場合、ほとんどはsetState
の中でデータの変更を行っていないことが原因です。- Nullエラー: 変数が
null
(空)の状態でそのプロパティにアクセスしようとすると発生します。Dartは安全性が高く、コンパイル時にエラーを検知してくれますが、注意が必要です。
エラーメッセージの読み方
Flutterでエラーが起きると、画面が真っ赤になったり(デバッグモード)、コンソールに大量のログが出たりします。パニックにならず、まずはログの一番上を見てみましょう。そこに「どのファイルの何行目で」「どんな種類のエラーが」発生したかが書かれています。エラーメッセージをキーワードに検索すると、多くの場合は解決策が見つかります。
デバッグの基本的な考え方
print
デバッグ: 最もシンプルで強力な方法です。怪しい箇所の前後でprint('ここまできた');
やprint('変数の値: $myVariable');
のように出力して、プログラムが意図通りに動いているかを確認します。- VS Codeのデバッガ: コードの左端をクリックしてブレークポイントを設定し、デバッグモードで実行(F5キー)すると、その行でプログラムを一時停止できます。変数の値を確認したり、一行ずつ処理を進めたりできるため、複雑な問題の解決に役立ちます。
失敗は学習の絶好の機会です。エラーを恐れず、むしろ友だちになるくらいの気持ちで向き合ってみましょう。
継続的な学習のために
次に学ぶべきこと
このTODOアプリを完成させたら、次は以下のテーマに挑戦してみるのがおすすめです。
- 非同期処理:
Future
,async
,await
を使って、インターネット経由でデータを取得する(API通信)方法。 - 画面遷移:
Navigator
を使って、複数の画面を持つアプリを作成する方法。 - 状態管理ライブラリ:
Provider
やRiverpod
を使った、より高度でスケーラブルな状態管理手法。 - 永続化: アプリを閉じてもデータが消えないように、デバイス内にデータを保存する方法(
shared_preferences
やデータベースなど)。
おすすめの学習リソース
特定のサービス名は挙げませんが、学習を進める上で信頼できる情報源は以下の通りです。
- 公式ドキュメント: 何よりも正確で最新の情報源です。特にCookbookセクションには、具体的な実装例が豊富にあります。
- 質の高い技術ブログ: 個人や企業が発信する技術ブログには、実践的なノウハウや問題解決のヒントが満載です。
- 動画チュートリアル: 視覚的に学びたい方には、コーディングの過程を動画で見せてくれるチュートリアルが有効です。
コミュニティとの関わり方
一人で学習していると、どうしても行き詰まってしまうことがあります。そんな時は、技術コミュニティを頼ってみましょう。オンラインのフォーラムやSNS、地域の勉強会などで質問をしたり、他の開発者と交流したりすることで、新たな発見やモチベーションを得ることができます。教えることは最高の学習法でもあります。いつかあなたが、初心者の質問に答える側になる日も来るでしょう。
まとめ:成長のための次のステップ
お疲れ様でした!あなたは今、Flutterを使って一つのアプリケーションをゼロから作り上げるという、素晴らしい経験をしました。静的な画面の表示から、ユーザーの操作に応じた動的な更新、そして保守性を意識したリファクタリングまで、アプリ開発の重要な要素をたくさん学びましたね。
大切なのは、ここで止まらないことです。今回作ったTODOアプリに、「タスクの編集機能」や「期限日の設定機能」を追加してみてはどうでしょうか?あるいは、全く新しいアイデアで、自分だけのオリジナルアプリ作りに挑戦するのも素晴らしいことです。
プログラミングは、小さな「できた!」の積み重ねです。今日得た成功体験を自信に変えて、これからも楽しみながら学び続け、あなたのアイデアをどんどん形にしていってください。応援しています!
関連商品・おすすめアイテム
![Flutter モバイルアプリ開発バイブル [ 南里勇気 ]](https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/0871/9784839970871.jpg?_ex=128x128)

