2025年6月14日

初めてのFlutter

✅ Flutter環境構築(Mac向け・VS Codeを使う場合)

1. Flutter SDKのインストール

🔗 公式サイトからダウンロード

✅ インストール手順(macOS)

1. Flutter SDKを解凍して任意の場所に配置(例: ~/development/flutter)

cd ~/development
unzip ~/Downloads/flutter*macos*<version>.zip

2. パスを通す

echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc
source ~/.zshrc

3. インストール確認

flutter --version
<!-- ディレクトリがなければ作成 -->
mkdir ~/development
<!-- ダウンロードされた場所からdevelopmentに移動 -->
mv ~/Desktop/flutter ~/development/

2. 依存ツールの確認

<!-- 1.Xcodeコマンドラインツールの有効化 -->
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
<!-- 2.初回起動用セットアップの実行 -->
sudo xcodebuild -runFirstLaunch
<!-- 3.CocoaPodsのインストール -->
sudo gem install cocoapods
<!-- 4.CocoaPodsのインストール成功後の確認 -->
pod --version

このコマンドで、以下をチェックしてくれます:Xcode(iOS開発用)、Android Studio(Android用のエミュレータ)、VS Codeとの連携

flutter doctor

Simulator Runtimeのエラー対策

flutter doctorをして以下のエラーがでたら行う

<!-- flutter doctorを実行後 -->
[!] Xcode - develop for iOS and macOS (Xcode 16.3)
    ✗ Unable to get list of installed Simulator runtimes.

Xcodeがシミュレータのランタイムを認識できない状態ですが、これはよくある症状で以下の手順で直ることがあります。

🔁 Xcode側でRuntimeの再インストールを試す Xcodeを開く メニュー → Settings...(旧バージョンでは Preferences...) Components タブを開く 最新の iOS Simulator(例:iOS 17.5など)が表示されていれば、「Install」ボタンで再インストール 完了後、Xcodeを再起動 → 再度 flutter doctor ✅ 最後に再確認

Simulator runtimeも問題なければ再度:

<!-- flutter doctor実行後 -->
「Xcode」の項目が [✓] になれば準備完了です。

📝 Flutter ToDoアプリ 機能概要(初版)

🎯 アプリの目的

シンプルなToDo(やること)を登録・一覧表示・編集・削除できるモバイルアプリケーション。


📱 実装済み画面

画面名概要
一覧画面登録されたToDoリストを表示し、操作できる
追加/編集画面ToDoを新規作成 or 既存の内容を更新する

✅ 実装済みの主な機能

機能カテゴリ内容
🔍 一覧表示ListViewでToDoをリスト形式で表示
➕ ToDo追加FloatingActionButtonから追加画面に遷移し、タイトルを入力して追加可能
✔️ 完了チェックToDoをタップで isDone 状態をトグルし、チェックアイコンと装飾を更新
🗑 削除ToDoを左にスワイプすることで削除(Dismissibleを使用)
✏️ 編集ToDoを長押しで編集画面に遷移し、タイトルを変更可能
🔁 画面遷移Navigator.push で画面遷移し、戻ると結果を pop() で受け取る形式を採用

🧱 ディレクトリ構成(簡易)

lib/
├── main.dart                    // アプリの起点・ToDo一覧画面
├── models/
│   └── todo.dart               // ToDoモデル定義(title, isDone)
└── screens/
    └── add_todo_screen.dart    // ToDo追加・編集共通画面

🛠 技術構成(今後の拡張を意識)

  • 状態管理:StatefulWidget(将来 Riverpod などに置き換え可能)
  • データ保存:未実装(今後 shared_preferencessqflite を検討)
  • テスト:未実装(今後 widget_test.dart に追加可能)

🚀 今後のステップ候補

優先度機能備考
★★★データの永続化アプリ再起動後もToDoが残るようにする
★★☆状態管理の導入RiverpodProvider など
★★☆UI改善(テーマ・アイコンなど)ダークモード対応、マテリアル3の活用など
★★☆テスト自動化ユニットテスト・ウィジェットテスト

ソースコード

<!-- lib/main.dart -->
import 'package:flutter/material.dart';
import 'models/todo.dart';
import 'screens/add_todo_screen.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(primarySwatch: Colors.blue),
      home: const TodoListScreen(),
    );
  }
}

class TodoListScreen extends StatefulWidget {
  const TodoListScreen({super.key});

  @override
  State<TodoListScreen> createState() => _TodoListScreenState();
}

class _TodoListScreenState extends State<TodoListScreen> {
  final List<Todo> _todos = [];

  void _addTodo(Todo todo) {
    setState(() {
      _todos.add(todo);
    });
  }

  void _editTodo(int index, Todo updated) {
    setState(() {
      _todos[index] = updated;
    });
  }

  void _toggleTodo(int index) {
    setState(() {
      final todo = _todos[index];
      _todos[index] = todo.copyWith(isDone: !todo.isDone);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ToDo 一覧')),
      body: ListView.builder(
        itemCount: _todos.length,
        itemBuilder: (context, index) {
          final todo = _todos[index];
          return Dismissible(
            key: ValueKey(todo.title + todo.isDone.toString()),
            direction: DismissDirection.endToStart, // 左スワイプで削除
            onDismissed: (_) {
              setState(() {
                _todos.removeAt(index);
              });
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('削除しました: ${todo.title}')),
              );
            },
            background: Container(
              color: Colors.red,
              alignment: Alignment.centerRight,
              padding: const EdgeInsets.only(right: 20),
              child: const Icon(Icons.delete, color: Colors.white),
            ),
            child: ListTile(
              title: Text(
                todo.title,
                style: TextStyle(
                  decoration: todo.isDone ? TextDecoration.lineThrough : null,
                  color: todo.isDone ? Colors.grey : null,
                ),
              ),
              trailing: Icon(
                todo.isDone ? Icons.check_box : Icons.check_box_outline_blank,
                color: todo.isDone ? Colors.green : null,
              ),
              onTap: () => _toggleTodo(index),
              onLongPress: () async {
                final updated = await Navigator.push<Todo>(
                  context,
                  MaterialPageRoute(
                    builder: (context) => AddTodoScreen(initialTodo: _todos[index]),
                  ),
                );
                if (updated != null) _editTodo(index, updated);
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final newTodo = await Navigator.push<Todo>(
            context,
            MaterialPageRoute(builder: (context) => const AddTodoScreen()),
          );
          if (newTodo != null) _addTodo(newTodo);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

<!-- lib/screens/add_todo_screen.dart -->
import 'package:flutter/material.dart';
import '../models/todo.dart';

class AddTodoScreen extends StatefulWidget {
  final Todo? initialTodo;

  const AddTodoScreen({super.key, this.initialTodo});

  @override
  State<AddTodoScreen> createState() => _AddTodoScreenState();
}

class _AddTodoScreenState extends State<AddTodoScreen> {
  late final TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(
      text: widget.initialTodo?.title ?? '',
    );
  }

  void _submit() {
    final title = _controller.text.trim();
    if (title.isEmpty) return;

    final updatedTodo = Todo(
      title: title,
      isDone: widget.initialTodo?.isDone ?? false,
    );
    Navigator.pop(context, updatedTodo);
  }

  @override
  Widget build(BuildContext context) {
    final isEditing = widget.initialTodo != null;

    return Scaffold(
      appBar: AppBar(title: Text(isEditing ? 'ToDo編集' : 'ToDo追加')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _controller,
              decoration: const InputDecoration(labelText: 'ToDoのタイトル'),
              onSubmitted: (_) => _submit(),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _submit,
              child: Text(isEditing ? '更新' : '追加'),
            ),
          ],
        ),
      ),
    );
  }
}

<!-- lib/models/todo.dart -->
class Todo {
  final String title;
  final bool isDone;

  Todo({
    required this.title,
    this.isDone = false,
  });

  Todo copyWith({String? title, bool? isDone}) {
    return Todo(
      title: title ?? this.title,
      isDone: isDone ?? this.isDone,
    );
  }
}

所感

chat gptにコードを書いてもらったが、自分一人だとこれを初めて書くのは時間がかかりそうだと感じた。 関数の定義の仕方やその後のWidget書き方はどこかReactのクラスコンポーネントを思わせるようだった。 chat gptがはじめにモデルを作っていたのを見ると、やはりWebフロントエンドの感覚(特にReact)とは異なる思想があるのかもしれないと感じた。 スマホならではのスライドや長押しの操作は関数を渡すだけで対応できるのは初めてFlutterを触るとすごく便利に感じた。 chat gptを使うことで、環境構築もほとんど躓くことがなかった。 良い経験にはなったが、実務で書いていくとなるとまだまだ覚えることがあるのだと感じた。