【Flutter入門】初めてのFlutter開発:シンプルなタスク管理アプリをiPhoneで動かすまで

本投稿は TECOTEC Advent Calendar 2024 2日目の記事です。
こんにちは、決済認証システム開発事業部の田中です。 普段はエンジニアとしてPHPでWebアプリケーションのシステムの開発を行なっています。 しかし、モバイルアプリ開発にも少し興味があり今回初めて挑戦してみることにしました。

しかし、「アプリを作ってみたい!」そう思っても、いざ始めようとすると『どう始めればいいの?』と悩むことはありませんか?

私も、全く同じ状況でした。そこで、アプリ開発未経験の私が「Flutter」というGoogleのツールを使い、簡単なタスク管理アプリをiPhoneで動かすまでの流れを記録しました。

この記事は、アプリ開発に興味があるけど最初の一歩が分からないという初心者の方に向けて書いています。一緒に環境構築からアプリの実装まで学んでいきましょう!


目次


1. FlutterとDartとは?

Flutterとは?

FlutterはGoogleが提供するクロスプラットフォームの開発ツールで、1つのコードベースからiOS、Android、Web、デスクトップ向けのアプリを同時に作ることができます。

特徴:

  • ウィジェットを組み合わせて直感的にUIを構築できます。
  • コード変更が即時に反映される「Hot Reload」機能により、開発効率が向上します。
  • ネイティブコードにコンパイルされ、高速なパフォーマンスを実現します。
  • 多数のパッケージやプラグインが利用可能で、コミュニティも活発です。

Dartとは?

Flutterで使われるプログラミング言語が「Dart」です。

特徴:

  • JavaやJavaScriptに似ており、初心者でも学びやすい言語です。
  • クラスやオブジェクト指向の考え方に対応しています。
  • Flutterに最適化されているため、効率的にアプリを作成することができます。

2. 環境構築と最初の一歩

使用環境

  • PC: MacBook Air (M1, 2020)
  • OS: macOS 15.1
  • Flutter: 3.24.4
  • Dart: 3.4.4
  • エディタ: Visual Studio Code
  • Xcode: Version 16.1(iOSアプリのビルド用)

環境構築の手順

Flutter SDKのインストール

  1. Flutter公式サイトから最新のSDKをダウンロードします。
  2. ダウンロードしたファイルを適当な場所に解凍し、環境変数PATHにFlutterのbinディレクトリを追加します。
  3. ターミナルでflutter doctorコマンドを実行し、必要な依存関係を確認します。

エディタのセットアップ

  • Visual Studio Codeを起動し、拡張機能から「Flutter」と「Dart」をインストールします。

iOSのセットアップ

  • Xcodeのインストール: App Storeから最新のXcodeをインストールします。
  • CocoaPodsのインストール: ターミナルで以下のコマンドを実行します。
sudo gem install cocoapods
  • 注意: flutter doctorで表示されるエラーや警告を確認し、必要な対応を行ってください。

3. DartとFlutterの基本を学びながら「Hello World」アプリを実装

Dartの基本

まずは、Dart言語の基本的な文法を見てみましょう。

変数の宣言

int number = 42; // 整数型の変数
String message = 'こんにちは'; // 文字列型の変数

解説

  • int number = 42;:整数型の変数を宣言します。
  • String message = 'こんにちは';:文字列型の変数を宣言します。

関数の定義

void greet(String name) {
  print('こんにちは、$nameさん');
}

解説

  • void greet(String name)nameという引数を受け取る関数を定義します。
  • print('こんにちは、$nameさん');:コンソールに挨拶を出力します。

クラスの定義

class Person {
  String name;

  Person(this.name);

  void introduce() {
    print('私の名前は$nameです');
  }
}

解説

  • class PersonPersonクラスを定義します。
  • String name;:クラスのプロパティを定義します。
  • Person(this.name);:コンストラクタを定義します。
  • void introduce():クラスのメソッドを定義します。

Flutterアプリの基本構造

Flutterアプリはウィジェット(Widget)の組み合わせで構成されています。ウィジェットはUIの構成要素であり、画面に表示されるすべてのものがウィジェットとして表現されます。


ステップ1: 最小限のFlutterアプリを作成

まずは、最小限のFlutterアプリを作成してみましょう。

void main() {
  runApp(Container());
}

解説

  • void main():Dartアプリケーションのエントリーポイント。
  • runApp(Container()):空のContainerウィジェットを画面に表示します。

このコードを実行すると、何も表示されない空のアプリが起動します。


ステップ2: テキストを表示する

画面にテキストを表示してみます。

import 'package:flutter/material.dart';

void main() {
  runApp(Text('Hello World'));
}

解説

  • import'package:flutter/material.dart';:Flutterのマテリアルデザインのウィジェットを使用するためのインポートです。
  • runApp(Text('Hello World')):テキストを表示するウィジェットを起動します。

ステップ3: テキストを中央に配置する

テキストを画面中央に配置してみましょう。

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text('Hello World'),
    ),
  );
}

解説

  • Center:子ウィジェットを中央に配置するウィジェット。

これでテキストが画面の中央に表示されます。


ステップ4: テキストのスタイルを変更する

テキストのサイズやスタイルを変更してみます。

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello World',
        style: TextStyle(fontSize: 32, color: Colors.blue),
      ),
    ),
  );
}

解説

  • TextStyle:テキストのスタイル(フォントサイズ、色など)を設定するクラスです。
  • fontSize:フォントサイズを指定します。
  • color:テキストの色を指定します。

ステップ5: マテリアルデザインを適用する

Flutterのマテリアルデザインを適用してみましょう。

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Center(
        child: Text(
          'Hello World',
          style: TextStyle(fontSize: 32, color: Colors.blue),
        ),
      ),
    ),
  );
}

解説

  • MaterialApp:マテリアルデザインのスタイルを適用するためのウィジェットです。
  • home:アプリのホーム画面を指定します。

ステップ6: Scaffoldで基本的なレイアウトを作成

アプリらしい見た目にするために、Scaffoldウィジェットを使います。

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Hello World App'),
        ),
        body: Center(
          child: Text(
            'Hello World',
            style: TextStyle(fontSize: 32, color: Colors.blue),
          ),
        ),
      ),
    ),
  );
}

解説

  • Scaffold:アプリの基本的なレイアウト構造を提供するウィジェットです。
  • AppBar:アプリのタイトルバーを表示するウィジェットです。

ステップ7: クラスを使ってコードを整理する

コードが長くなってきたので、クラスを使って整理します。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp()); // アプリのエントリーポイント
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Hello World App'),
        ),
        body: Center(
          child: Text(
            'Hello World',
            style: TextStyle(fontSize: 32, color: Colors.blue),
          ),
        ),
      ),
    );
  }
}

解説

  • MyApp:アプリ全体を管理するクラス。
  • StatelessWidget:状態を持たないウィジェットを作成するための基底クラス。
  • build:ウィジェットのUIを構築するメソッド。

これで、「Hello World」アプリが完成しました。

  • 補足説明

StatelessWidgetStatefulWidgetの違い: StatelessWidgetは不変のウィジェットで、状態を持ちません。一方、StatefulWidgetは状態を持ち、ユーザーの操作やデータの変化に応じてUIを更新できます。


動作確認

このコードを実行すると、以下のような画面が表示されます。

実装コード

以下は、画面に「Hello World」を表示するコードです。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp()); // アプリのエントリーポイント
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Hello World App'), // アプリバーのタイトル
        ),
        body: Center(
          child: Text(
            'Hello World',
            style: TextStyle(fontSize: 32, color: Colors.blue), // テキストのスタイルを設定
          ),
        ),
      ),
    );
  }
}

4. タスク管理アプリの設計と実装

「Hello World」アプリを動かせたので、次は本題であるタスク管理アプリを作成します。このセクションでは、設計実装動作確認という流れで進めます。

設計

まずは、どんなアプリにするか、必要な機能を考えます。

必要な機能

  1. タスクの入力
    • ユーザーがタスク名を入力できる。
  2. タスクの追加
    • 入力したタスクをリストに保存し、表示する。
  3. タスクの完了
    • チェックボックスでタスクを完了済みに設定。
  4. タスクの削除
    • 不要になったタスクを削除。

ステップ1: プロジェクトのセットアップ

新しいFlutterプロジェクトを作成します。

flutter create task_manager_app

解説

  • flutter create: 新しいFlutterプロジェクトを作成するコマンドです。
  • task_manager_app: プロジェクトの名前です。

ステップ2: タスクのデータモデルを作成

タスクを管理するためのデータモデルを作成します。

class Task {
  String name;         // タスクの名前
  bool isCompleted;    // タスクの完了状態

  Task({required this.name, this.isCompleted = false}); // コンストラクタ
}

解説

  • class Task: タスクを表現するクラスを定義します。
  • String name: タスクの名前を保持します。
  • bool isCompleted: タスクが完了しているかどうかを示すフラグです。デフォルトはfalse(未完了)です。
  • コンストラクタでタスク名を必須入力とし、完了状態は任意で指定できます。

ステップ3: メインウィジェットの作成

アプリ全体のエントリーポイントとなるウィジェットを作成します。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TaskApp(), // タスク管理画面を表示
    );
  }
}

解説

  • MaterialApp: アプリ全体のテーマやルーティングを管理します。
  • home: アプリのホーム画面をTaskAppウィジェットに設定します。

ステップ4: タスク管理画面の作成

状態を管理するために、StatefulWidgetを使用します。

class TaskApp extends StatefulWidget {
  @override
  _TaskAppState createState() => _TaskAppState();
}
class _TaskAppState extends State<TaskApp> {
  List<Task> _tasks = []; // タスクリストを保持
  final TextEditingController _taskController = TextEditingController(); // 入力フィールドを制御
}

解説

  • StatefulWidget: 状態を持つウィジェットです。
  • _tasks: タスクを格納するリストを初期化します。
  • _taskController: テキストフィールドの入力内容を管理します。

ステップ5: タスクを追加する機能の実装

ユーザーが入力したタスクをリストに追加する関数を定義します。

void _addTask() {
  String taskName = _taskController.text; // 入力されたタスク名を取得
  if (taskName.isNotEmpty) {
    setState(() {
      _tasks.add(Task(name: taskName)); // 新しいタスクをリストに追加
      _taskController.clear(); // テキストフィールドをクリア
    });
  }
}

解説

  • _taskController.text: テキストフィールドから現在の入力値を取得します。
  • if (taskName.isNotEmpty): タスク名が空でないことを確認します。
  • setState(): 状態が変化したことをFlutterに通知し、UIを更新します。
  • _tasks.add(): タスクリストに新しいタスクを追加します。

ステップ6: タスクの入力フィールドと追加ボタンの作成

ユーザーがタスクを入力し、追加できるUIを構築します。

Padding(
  padding: const EdgeInsets.all(8.0), // 余白を設定
  child: Row(
    children: [
      Expanded(
        child: TextField(
          controller: _taskController, // テキストフィールドのコントローラを設定
          decoration: InputDecoration(labelText: 'タスクを入力'), // ヒントテキストを設定
        ),
      ),
      ElevatedButton(
        onPressed: _addTask, // ボタンが押されたときにタスクを追加
        child: Text('追加'),
      ),
    ],
  ),
)

解説

  • Padding: 周囲に余白を追加します。
  • Row: 子ウィジェットを横に並べます。
  • Expanded: テキストフィールドが可能な限りスペースを占有するようにします。
  • TextField: ユーザーがテキストを入力できるフィールドです。
  • ElevatedButton: タスクを追加するボタンです。

ステップ7: タスクリストの表示

リストビューを使って、タスクリストを表示します。

Expanded(
  child: ListView.builder(
    itemCount: _tasks.length, // リスト内のアイテム数を指定
    itemBuilder: (context, index) {
      return ListTile(
        title: Text(
          _tasks[index].name, // タスク名を表示
          style: TextStyle(
            decoration: _tasks[index].isCompleted
                ? TextDecoration.lineThrough // 完了済みなら取り消し線を表示
                : TextDecoration.none,
          ),
        ),
        leading: Checkbox(
          value: _tasks[index].isCompleted, // チェックボックスの状態を設定
          onChanged: (value) {
            setState(() {
              _tasks[index].isCompleted = value!; // 完了状態を更新
            });
          },
        ),
        trailing: IconButton(
          icon: Icon(Icons.delete), // 削除アイコンを表示
          onPressed: () {
            setState(() {
              _tasks.removeAt(index); // タスクを削除
            });
          },
        ),
      );
    },
  ),
)

解説

  • Expanded: リストビューが可能なスペースを全て占有するようにします。
  • ListView.builder: スクロール可能なリストを効率的に構築します。
  • ListTile: リスト内の各アイテムのレイアウトを提供します。
  • Checkbox: タスクの完了状態を切り替えるチェックボックスです。
  • IconButton: タスクを削除するためのボタンです。

ステップ8: 完成したUIを構築

_TaskAppStateクラスのbuildメソッドを完成させます。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('タスク管理アプリ'), // アプリバーのタイトル
    ),
    body: Column(
      children: [
        // ステップ6で作成した入力フィールドと追加ボタン
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _taskController,
                  decoration: InputDecoration(labelText: 'タスクを入力'),
                ),
              ),
              ElevatedButton(
                onPressed: _addTask,
                child: Text('追加'),
              ),
            ],
          ),
        ),
        // ステップ7で作成したタスクリストの表示
        Expanded(
          child: ListView.builder(
            itemCount: _tasks.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(
                  _tasks[index].name,
                  style: TextStyle(
                    decoration: _tasks[index].isCompleted
                        ? TextDecoration.lineThrough
                        : TextDecoration.none,
                  ),
                ),
                leading: Checkbox(
                  value: _tasks[index].isCompleted,
                  onChanged: (value) {
                    setState(() {
                      _tasks[index].isCompleted = value!;
                    });
                  },
                ),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  onPressed: () {
                    setState(() {
                      _tasks.removeAt(index);
                    });
                  },
                ),
              );
            },
          ),
        ),
      ],
    ),
  );
}

解説

  • Scaffold: アプリの基本的なレイアウトを提供します。
  • AppBar: 画面上部にタイトルバーを表示します。
  • Column: ウィジェットを縦に配置します。

完成したコード

ここまでの実装を統合すると、以下のコードになります。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp()); // アプリのエントリーポイント
}

// アプリ全体を管理するウィジェット
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TaskApp(), // タスク管理画面をホームに設定
    );
  }
}

// タスクを表現するデータモデル
class Task {
  String name;         // タスク名
  bool isCompleted;    // タスクの完了状態

  Task({required this.name, this.isCompleted = false}); // コンストラクタ
}

// タスク管理画面のウィジェット
class TaskApp extends StatefulWidget {
  @override
  _TaskAppState createState() => _TaskAppState();
}

// 状態を管理するクラス
class _TaskAppState extends State<TaskApp> {
  List<Task> _tasks = []; // タスクリスト
  final TextEditingController _taskController = TextEditingController(); // テキストフィールドのコントローラ

  // タスクを追加する関数
  void _addTask() {
    String taskName = _taskController.text; // 入力されたタスク名を取得
    if (taskName.isNotEmpty) {
      setState(() {
        _tasks.add(Task(name: taskName)); // タスクリストに追加
        _taskController.clear(); // テキストフィールドをクリア
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('タスク管理アプリ'), // アプリバーのタイトル
      ),
      body: Column(
        children: [
          // タスクの入力フィールドと追加ボタン
          Padding(
            padding: const EdgeInsets.all(8.0), // 余白を設定
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _taskController, // テキストフィールドのコントローラ
                    decoration: InputDecoration(labelText: 'タスクを入力'),
                  ),
                ),
                ElevatedButton(
                  onPressed: _addTask, // タスクを追加する関数を呼び出す
                  child: Text('追加'),
                ),
              ],
            ),
          ),
          // タスクリストの表示
          Expanded(
            child: ListView.builder(
              itemCount: _tasks.length, // タスクリストの長さ
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(
                    _tasks[index].name, // タスク名を表示
                    style: TextStyle(
                      decoration: _tasks[index].isCompleted
                          ? TextDecoration.lineThrough // 完了済みなら取り消し線を表示
                          : TextDecoration.none,
                    ),
                  ),
                  leading: Checkbox(
                    value: _tasks[index].isCompleted, // チェックボックスの状態
                    onChanged: (value) {
                      setState(() {
                        _tasks[index].isCompleted = value!; // 完了状態を更新
                      });
                    },
                  ),
                  trailing: IconButton(
                    icon: Icon(Icons.delete), // 削除アイコン
                    onPressed: () {
                      setState(() {
                        _tasks.removeAt(index); // タスクを削除
                      });
                    },
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

動作確認

以下のGIFは、完成したタスク管理アプリの動作を示しています。

タスク管理アプリの動作

まとめ

今回の開発を通じて、FlutterとDartの基本を学びながら、シンプルなタスク管理アプリを作成しました。この記事が、初めてのアプリ開発に挑戦する皆さんの一助になれば幸いです。

参考資料

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。
採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。
ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp