モバイルアプリ開発にFlutterを使ってみた

## はじめに

これまでPWAで提供していたWebアプリを、モバイルアプリとして配信することになりました。モバイルアプリ開発の経験がない中で、いかに効率よくモバイルアプリ化できるか検討した結果、Flutterでモバイルアプリを作成し、公開まで行うことができました。今回は学んだ内容のアウトプットも兼ねて、Flutterについて、初期設定で用意されているサンプルコードを使って紹介しようと思います。

## Flutterとは

Flutterは、Googleが開発したオープンソースのUIフレームワークです。
クロスプラットフォーム対応しており、iOS・Android・Webアプリを単一のコードベースで開発できます。

https://flutter.dev/

## 開発環境

- MacBook Pro M2
- Visual Studio Code
- FVM 3.2.1
- Dart 3.7.0
- Flutter 3.29.0

※ XcodeとAndroid Studioもインストール済みの前提で進めます。


開発環境構築には、Flutter Version Management（FVM）を用いました。FVMは、Flutter SDKのバージョン管理ツールです。

https://fvm.app/

### FVMのインストール

FVMはbrewを使ってインストールしました。

```terminal
brew tap leoafarias/fvm
brew install fvm
```

### パスの設定

Androidについてはパスの設定が必要です。

`.zshrc`に以下を記載してください。

```zsh
export ANDROID_HOME=$HOME/Library/Android/sdk
```

### Flutterのインストール

`fvm use`で使用するFlutterバージョンを指定します。そうすることで、.fvmrcファイルが作成されます。.fvmrcには使用中のFlutterバージョンが記載されます。

```terminal
fvm use [version]
```

途中から参加するメンバーは、`fvm install`を実施して使用中のFlutterバージョンをインストールします。

```terminal
fvm install
```

Flutterのバージョン指定後、バージョンを確認してください。

FVMでインストールしたFlutterを使用する場合は、頭に`fvm`を付けます。`fvm flutter doctor`で開発環境のセットアップが終わっているか確認し、すべての項目にチェックマークが付いたら環境構築完了です。

```terminal
fvm flutter --version
```

```terminal
fvm flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.29.0, on macOS 15.5 24F74 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.1)
[✓] VS Code (version 1.100.2)
[✓] Proxy Configuration
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!
```

## プロジェクトの作成

プロジェクトは以下のコマンドで作成します。

```terminal
fvm flutter create [プロジェクト名]
```

## 実行

プロジェクトに移動して実行します。

```terminal
cd [プロジェクト名]
fvm flutter run
```

実行コマンド入力後、どの環境で実行するのか尋ねられます。エミュレータを一つだけ起動している場合は、自動的にエミュレータ内で起動します。

```terminal
No wireless devices were found.

[1]: macOS (macos)
[2]: Mac Designed for iPad (mac-designed-for-ipad)
[3]: Chrome (chrome)
Please choose one (or "q" to quit): [起動する番号]
```

実行するとカウントアップのデモが表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635206/60bfb67c-5011-4d8f-8d72-1da137bf5070.gif" width="200px"/>

## 更新について

アプリ開発にFlutterを選んで良かった点の一つにホットリロードの機能があります。実行時、ターミナルに表示される説明通り、`r`キーを押すことでコードの変更をすぐに反映させることが可能です。

```dart
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
```

これにより、アプリを毎回ビルドする必要がなくなり開発をスムーズに行うことができます。

## 構成について

Flutterはウィジェットを使ってUIを作っています。パズルみたいに組み合わせる感じです。

### Widgetとは

ウィジェットはUIを構成する部品です。見えているもの全てはウィジェットで作られています。最小単位は画像や文章に至るまでウィジェットです。

ウィジェットは、親子関係のようにツリーで表すことができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635206/8f61e437-08be-4267-8c8f-af4c9312e492.png" width="400px"/>


### サンプルコード

#### ライブラリの読み込み

ウィジェットライブラリを読み込みます。このライブラリは、iOSとAndroidの両方に対応したUIを作成できます。

```dart
import 'package:flutter/material.dart';
```

この他にも、ウィジェットライブラリには、iOS風のUIを提供する`package:flutter/cupertino.dart
`や、TextやContainerなど基本的なウィジェットを含む`package:flutter/widgets.dart`などがあります。

#### Widgetの構築

まずは、アプリのエントリーポイントを作成。最初に呼ばれる関数です。

runAppでFlutterにMyAppウィジェットを描画する命令を出します。

```dart
void main() {
  runApp(const MyApp());
}
```

MyAppには、実際に描画するウィジェットを記述していきます。MyAppはクラスとして定義されており、StatelessWidgetを継承しています。親クラスであるStatelessWidgetのbuildメソッドをオーバーライドして独自のウィジェットを構築します。



```dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
```

継承しているStatelessWidgetは、状態を持たないウィジェットを作成するためのクラスです。この他にも、状態を持つStatefulWidgetや、子ウィジェットにデータを共有できるInheritedWidgetなどがあります。

このサンプルではStatelessWidgetを継承し、buildメソッドをオーバライドしてMaterialAppウィジェットを返しています。

https://api.flutter.dev/flutter/material/MaterialApp-class.html

MaterialAppはアプリの土台を作成するウィジェットです。
- MaterialApp
  - title：アプリ名（ブラウザ実行の場合タスクバーなどに表示されます）
  - theme：色やデザインテーマを設定します
  - home：最初に表示する画面(サンプルはMyHomePage)

それでは、MyHomePageの中身を見ていきましょう。

```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

MyHomePageはStatefulWidgetを継承して作られています。StatefulWidgetは、状態を持つウィジェットを定義するためのクラスです。

StatefulWidgetは、２つのクラスに分けて記述します。これは、UIとロジックを分離するためだけでなく、状態の変更をStateクラス内で完結させ、Flutter内部が再描画の最適化を行いやすいようにするためです。

MyHomePageクラスでは、親クラスからタイトルを受け取り、createStateメソッドで状態を持つ_MyHomePageクラスを生成しています。

_MyHomePageクラスでは、カウントの値を保持する_counter変数と、ボタンを押されたときにカウントを増加させる_incrementCounterメソッドを定義しています。_incrementCounterメソッドの中では、setState関数で_counterの値を加算しており、これによりUIが再描画されます。

buildメソッドでは、Scaffoldウィジェットを返しています。Scaffoldウィジェットはアプリの基本的な画面レイアウトを提供するウィジェットで、サンプルではappBar、body、floatingActionButtonの3つのプロパティーから構成されています。それぞれのプロパティーにはTextやIconといった基本的なウィジェットが使用されています。

https://api.flutter.dev/flutter/material/Scaffold-class.html

これまでのウィジェットの説明を踏まえて、改めてサンプルアプリを見てみましょう。このようにFlutterアプリはウィジェットを組み合わせながら作られています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635206/9c650a3d-0309-4671-a5e2-8cd95b661cec.png" width="400px"/>


<details><summary>全体コード</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

</details>

## まとめ

今回はFlutterについて、サンプルプログラムを交えながら紹介しました。AndroidとiOSの両方に対応したモバイルアプリを同時に開発できるため、工数の面でも魅力的な選択と言えるのではないでしょうか。また、単一のコードベースで記述できることにより、プラットフォーム間での機能の偏りなど防ぐことも期待できます。

注意点として、端末に依存する機能（センサー類など）を使用する場合は、それぞれのOSに合わせた実装が必要になることもあります。そのため、アプリの用途や求められる機能によっては、FlutterではなくSwiftやKotlinを選んだ方が適しているケースもあるでしょう。

今後もFlutterの知見を深めながら、アプリ開発のスキル向上に繋げていきたいと思います。