この記事は、AI個人開発でスマホアプリを作るときの「最初の1本」を想定した実装メモです。
前回Zennでは、Flutter / React Native / Expo / Swift などの技術選定について整理しました。
今回はその実装編として、Flutterでログインなし・ローカル保存のみのチェックリストアプリを作る最小構成をまとめます。
動作環境
- Flutter 3.x(3.24 以降を想定)
- Dart 3.x
- shared_preferences: ^2.3.0
セットアップ後は `flutter pub get` で依存を取得し、`flutter analyze` で警告が出ないことを確認してから進めてください。本記事のコードは `flutter analyze` で警告が出ない構成にしています。
この記事のゴール
「AIを使って個人開発を始めたいけど、最初の1本で何を作ればいいか分からない」という人向けに、ログインなし・ローカル保存・単機能のチェックリストアプリを、Flutterの最小構成で組み立てます。
- 認証なし(サインイン画面を作らない)
- サーバーなし(クラウド同期しない)
- 画面は1〜2枚
- 状態管理は最小限
「最強の構成」ではなく「公開まで確実にたどり着ける構成」を狙います。バックエンドや認証を最初から入れると、それだけで完成しなくなるからです。
完成イメージ
- チェックリストを追加・チェック・削除できる
- アプリを閉じても内容が残る(端末ローカルに保存)
- それだけ
DB もログインも通知も課金も入れません。まずはこの「単機能で公開できる1本」を作り切ることを優先します。
1. プロジェクト作成
```bash
flutter create checklist_min
cd checklist_min
```
依存は永続化用に `shared_preferences` を1つ足すだけです。
```bash
flutter pub add shared_preferences
```
`shared_preferences` は端末ローカルにKey-Valueで保存するための定番パッケージです。SQLiteやサーバーDBは、データ構造が複雑になってから検討すれば十分です。
2. データモデル
タスク1件を表す最小のクラスです。JSON と相互変換できるようにしておくと、そのまま保存に使えます。
```dart
class TodoItem {
TodoItem({required this.title, this.done = false});
String title;
bool done;
Map toJson() => {'title': title, 'done': done};
factory TodoItem.fromJson(Map json) => TodoItem(
title: json['title'] as String,
done: json['done'] as bool? ?? false,
);
}
```
3. ローカル保存(リポジトリ)
保存・読み込みを1か所にまとめます。`shared_preferences` には文字列で入れたいので、`jsonEncode` / `jsonDecode` を挟みます。
```dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class TodoRepository {
static const _key = 'todo_items';
Future> load() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_key);
if (raw == null) return [];
final list = jsonDecode(raw) as List;
return list
.map((e) => TodoItem.fromJson(e as Map))
.toList();
}
Future save(List items) async {
final prefs = await SharedPreferences.getInstance();
final raw = jsonEncode(items.map((e) => e.toJson()).toList());
await prefs.setString(_key, raw);
}
}
```
ポイントは「保存処理をUIから切り離す」ことです。あとでSQLiteやクラウドに差し替えたくなっても、このクラスの中身を変えるだけで済みます。
4. 画面(1枚)
`StatefulWidget` で完結させます。状態管理ライブラリ(Riverpod / Bloc など)は、画面が増えてから入れれば十分です。最初の1本では入れません。
```dart
import 'package:flutter/material.dart';
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State createState() => _TodoPageState();
}
class _TodoPageState extends State {
final _repo = TodoRepository();
final _controller = TextEditingController();
List _items = [];
@override
void initState() {
super.initState();
_load();
}
Future _load() async {
final items = await _repo.load();
setState(() => _items = items);
}
Future _add() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() {
_items.add(TodoItem(title: text));
_controller.clear();
});
await _repo.save(_items);
}
Future _toggle(int index) async {
setState(() => _items[index].done = !_items[index].done);
await _repo.save(_items);
}
Future _remove(int index) async {
setState(() => _items.removeAt(index));
await _repo.save(_items);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('チェックリスト')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'やることを入力',
border: OutlineInputBorder(),
),
onSubmitted: () => _add(),
),
),
const SizedBox(width: 8),
FilledButton(onPressed: _add, child: const Text('追加')),
],
),
),
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) {
final item = items[i];
return Dismissible(
key: ValueKey('${item.title}$i'),
onDismissed: () => remove(i),
background: Container(color: Colors.red),
child: CheckboxListTile(
value: item.done,
onChanged: () => _toggle(i),
title: Text(
item.title,
style: TextStyle(
decoration: item.done
? TextDecoration.lineThrough
: null,
),
),
),
);
},
),
),
],
),
);
}
}
```
5. エントリポイント
```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: 'Checklist',
theme: ThemeData(useMaterial3: true),
home: const TodoPage(),
);
}
}
```
これで完成です。追加・チェック・スワイプ削除・再起動後も残る、までが動きます。
ここで止めるのが大事
慣れてくると、最初から以下を盛り込みたくなります。
- ログイン / アカウント
- クラウド同期
- プッシュ通知
- 課金 / 広告
- 設定画面 / ダークモード切替
- AI API 連携
ただ、これらを最初の1本に全部入れると、リリースまでの距離が一気に伸びます。まずは単機能で1本公開し、ストア審査を通す体験をするほうが、結果的に2本目以降の速度が上がります。
機能を足すなら、この最小構成が動いてからで十分間に合います。
AIに書かせるときの指示の出し方
Claude Code などのコーディングAIに任せる場合も、いきなり「全部入りのToDoアプリ」と頼むと構成が膨らみます。最初は次のように範囲を絞って依頼すると安定します。
```text
Flutterで、ログインなし・ローカル保存のみのチェックリストアプリを作って。
- 画面は1枚だけ
- shared_preferencesで永続化
- 状態管理ライブラリは使わずStatefulWidgetで完結
- 追加 / チェック / スワイプ削除のみ
```
「やらないこと」を明示するのがコツです。認証やDBを“入れない”と先に伝えると、余計な足場を作られずに済みます。
関連リンク
AI個人開発でスマホアプリを作るときの技術選定については、Zennにまとめています。
AIスマホアプリ個人開発の全体像は、自社サイトにまとめています。
筆者は株式会社バーニングトライブでスマホアプリの開発・運用に携わっています。
Codex・Claude Code・スマホアプリ開発の教材一覧はこちらです。