Flutterでのアプリ開発に入門した備忘録

こんにちは、もち（[@mochi_2225](https://twitter.com/mochi_2225)）です。記事執筆の段階でB2が終わりそうな25卒の学年です。

3月18日に発表会が行われた「[Open Hack U 2022 Spring OSAKA](https://hacku.yahoo.co.jp/hacku2022spring_osaka/)」に参加をしました。そこでの開発でFlutterを使うことになり、3月3日のキックオフから2週間程度でFlutterの学習、練習アプリの開発、ハッカソン作品の開発といったことを行いました。その内容をここに残したいと思います。**今後、Flutterに入門する方の参考になれば幸いです。**

内容は、どう進めたのか、どこで躓いたのかといったことを主に書いていきます。実際に書いたコードなどはGitHubのリポジトリや、参考で上げているサイトの記述をご確認ください。

## バックグラウンドの共有
初めに、筆者の今までの経験などバックグラウンドを共有しようと思います。既に持っている知識や技術は、無意識に利用していることがありますのでご注意ください。

ハッカソンには今までも何度か参加してきました。その時はwebアプリの開発をやっていて、主な技術スタックでフロントエンドがVue.js、バックエンドがPythonのFlaskやMySQLを使用していました。webのフロントエンドとバックエンドのどちらもを一通り経験しています。

一方で、スマホアプリなどネイティブアプリケーションの開発は一切経験がありませんでした。Flutterは1つのコードでAndroidもiOSのどちらのアプリも開発できるなんか凄いフレームワーク、といった程度の事前知識でした。

## Flutterを始める
[flutter-jissen]: https://zenn.dev/kazutxt/books/flutter_practice_introduction
まずはFlutterの大枠を学ぶところから始めました。学習はZennの記事[**『Flutter実践入門』**][flutter-jissen]を教材にしました。

https://zenn.dev/kazutxt/books/flutter_practice_introduction

技術の学習方法は様々あるかと思いますが、私の場合はサンプルコードを写経するのではなく、一通り教材を読んで「どんなことができるのか」という知識を蓄えるという方法を取りました。今回は時間も限られていたので、だいぶ飛ばしながら教材を読んでいました。制作の中で必要となった時に蓄えた知識から必要なものを引き出して、詳細は教材を参照したり検索したりという形で補うという考え方で学習しました。

[『Flutter実践入門』][flutter-jissen]では、Flutter/Dartの基本的な書き方から、環境構築、アプリのリリース、Flutterの細かな仕組みなどなど、**幅広く体系的に**書かれていて良いなと思いました。私がまとめてFlutterを学んだのはこの教材だけでしたが、それでも十分**Flutterの全体像を知ることができた**かと思います。それ以外は個々必要になった場所を詳しく調べるという方法でこの先の開発を進めました。

## 練習用アプリを作ってみる
[『Flutter実践入門』][flutter-jissen]を一通り読んだ次は、ここまで蓄えた知識を試しにアウトプットしようということで、**練習用アプリ**を作ることにしました。ハッカソンの作品でサーバとAPIで通信しながら処理を行うアプリの開発をすることになっていたので、その練習もかねて、[OpenWeatherMap](https://openweathermap.org/)というwebサービスのAPIを利用したお天気アプリもどきを作ることにしました。

実際に作成したものが、[このリポジトリ](https://github.com/mochi-yu/learn-Flutter/tree/main/demo_weather_app)にあるものです。デザインは皆無です。

https://github.com/mochi-yu/learn-Flutter/tree/main/demo_weather_app

このアプリでは、[『Flutter実践入門』][flutter-jissen]のChapter7:デザインパターンで言及のあったUIとデータの分離を取り入れようと思い、**Providerライブラリ**のChangeNotifierProviderやConsumerを使いました。うまく使いこなせたかと言われると正直怪しいです…。

HTTPで通信してREST APIを扱う処理に関しては、Zennの記事[『Step8: APIを呼び出してデータを取得してみよう』](https://zenn.dev/sugitlab/books/flutter_poke_app_handson/viewer/step8)を参考にさせていただきました。**factoryコンストラクタ**と言うものを用いてJSONからデータを吸い出す処理を実装していて、とても使いやすかったです。

<details><summary>データに関するコード</summary>

データのモデル
```weatherData.dart
class WeatherData {
  String weather;   // 現在天気
  String weatherDescription;  // 天気の詳細
  double temp;      // 現在気温 
  double minTemp;   // ここまでの最低気温
  double maxTemp;   // ここまでの最高気温
  int    pressure;  // 現在気圧
  int    humidity;  // 現在湿度
  double windSpeed; // 風速
  int    clouds;    // 雲量

  WeatherData({
    this.weather = 'Weather',
    this.weatherDescription = "WeatherDescription",
    this.temp = 100.0,
    this.minTemp = 100.0,
    this.maxTemp = 100.0,
    this.pressure = 0,
    this.humidity = 200,
    this.windSpeed = 100.0,
    this.clouds = 100,
  });

  factory WeatherData.fromJson(Map<String, dynamic> json) {
    return WeatherData(
      weather: json['weather'][0]['main'],
      weatherDescription: json['weather'][0]['description'],
      temp: json['main']['temp'],
      maxTemp: json['main']['temp_max'],
      minTemp: json['main']['temp_min'],
      pressure: json['main']['pressure'],
      humidity: json['main']['humidity'],
      windSpeed: json['wind']['speed'],
      clouds: json['clouds']['all'],
    );
  }
}
```

Providerで使用するChangeNotifierクラスを継承したクラスの定義
```weatherDataNotifier.dart
import 'package:demo_weather_app/weatherData.dart';
import 'package:flutter/foundation.dart';
import 'fetchWeatherData.dart';

class WeatherDataNotifier extends ChangeNotifier {
  WeatherData _weather = WeatherData();

  // getter
  WeatherData get getWeather => _weather;

  void updateWeather() async {
    _weather = await fetchWeatherData();
    print("weather updated!");
    notifyListeners();  // ここでUI側にデータの変更を通知
  }
}
```
</details>

## ハッカソン作品の開発
練習用アプリの作成もしてみて、Flutterを最低限使える状態にはなれたかなと思ったので、いよいよ本番のハッカソン作品の開発にとりかかりました。作成したアプリのリポジトリは以下になります。

https://github.com/mochi-yu/warikan-app-hacku2022

今回のハッカソンで作成したものの概要は次の通りです。太字にしているところが私たちの制作の中でアピールポイントにしていた点です。

- **割り勘をみんなでフェアにできる**スマホアプリ
- **レシートから品目と情報のデータを読み取る**
- 割り勘ごとにルームが作られて、**複数人が同時に**割り振りできる
- 各参加者の更新はサーバを経由して全参加者に共有され、**リアルタイムで画面が更新**される
- クライアント/サーバ間の通信には**gRPC**を用いる

それから、今回のハッカソンは3人のチームで参加して、
- バックエンド
- レシート読み取り、ルーム参加用QRコードなどのロジック
- クライアント側のUI・データ管理・サーバとの通信

と言ったように役割分担をし、私は3つ目のUI、データ管理、サーバとの通信を担当しました。ハッカソン作成物の詳細については、いずれまた別の記事でまとめられたらなと思います。

全体の制作は、以下のような流れで行いました。
1. アプリの各ページのデザインをFigmaで作成
1. 各ページのUIをFlutterで実装
1. 各ページ間のデータのやり取りを実装
1. サーバとの通信を実装

### アプリのUIの作成
私はアプリのUIを担当するということで、まずはUIのデザインを考えました。もちろんセンスなど無いので手探り状態で、取り敢えずデザインでよく聞く[**Figma**](https://www.figma.com/ja/)というツールを使ってみることにしました。これも事前知識としては名前しか知らない程度でしたが、独特な操作も少なく、そこまで手こずらずに制作に取り掛かれました。

Figmaでのデザイン作成時には、Flutterで実装することを見据えて[『FigmaでFlutter向きのアプリデザインを簡単に作成する方法』](https://www.kamo-it.org/blog/flutter-app-design/)のサイトに紹介されていた[**Material 3 Design Kit**](https://www.figma.com/community/file/1035203688168086460)というものを使いました。この中に既存で入っている各種コンポーネントがFlutterのWidgetに対応するものが多く、実装が楽になりました。

UIを作るうえでは「ボタン」や「ヘッダー」「フッダー」「リスト表示の各要素」など、UIを構成する各部位を**コンポーネント**に分割することを心掛けました。これによりその部品を様々なページで再利用することができ、コード量の削減につながりました。Flutterでのコンポーネントの実装は、[『【flutter】componentの作り方』](https://zenn.dev/tattu/articles/1501b603e966a8)などを参考にしました。

このうち、**ヘッダー(FlutterだとappBarにあたる)のコンポーネント**は、他とは少し違った作り方が必要でした。この点は[『FlutterのAppBarをカスタマイズする』](https://www.memory-lovers.blog/entry/2022/04/11/150000)などを参考にして実装をしました。

それから**下部の固定フッター**は、デフォルトのWidgetとしてBottomNavigationBarというものがありましたが、自分のデザインとは違っていたため、[『【Flutter】Scrollableな領域と固定footerの構造』](https://zenn.dev/aryzae/articles/603a44ea6de662)などを参考にして実装をしました。

### アプリのデータ管理
アプリのデータは練習用アプリでも用いた**Provider**を中心にして実装することにしていました。特にChangeNotifierProviderを用いることで、画面構成を記述しているコードとデータの処理を記述しているコードを分けることができ、コードの可読性がだいぶ上がったと思っています。ただ、Widgetの依存関係などで私が理解しきれいていない点も多く、まだまだ改善点が大有りな実装になっているかと思います。

それから、コンポーネントに分けたこともあり、親ウィジェットから**子ウィジェットが持つ変数を参照する**必要が生じました。この実装は、親側で子ウィジェットのKeyを管理し、それを用いて参照するという方法で、[『【Flutter】親Widgetから子のメソッドを呼ぶには、GlobalObjectKeyを使う』](https://qiita.com/karamage/items/19d53a5870c32bf59742)などを参考にして実装をしました。

### サーバとの通信
サーバとの通信は、練習用アプリではhttpのリクエストを用いていましたが、このアプリではgRPCの通信を実装しました。gRPCによる通信を用いることで、**クライアントが随時サーバ側の変更の確認をしなくとも、サーバ側で変更があったときにサーバから各クライアントへその旨を通知できる**という、いわゆる**リアルタイム通信**を実装することができました。

gRPCの通信では、あらかじめ通信の内容（リクエスト/レスポンスのフォーマットなど）を定めた ```.proto```ファイルを作成し、それに基づいて通信が行われます。この**定義ファイルから通信に必要なソースコードを自動生成**することができ、通信の実装が非常に簡単でした。実装の際には[公式のQuick start](https://grpc.io/docs/languages/dart/quickstart/)や[『Dartを使用したgRPCプログラムについて』](https://zenn.dev/kei_hongyo/articles/cb6aa853974e8e)、[『【Flutter】gRPCを使ってAPI通信を実装する【前編】』](https://rightcode.co.jp/blog/information-technology/flutter-grpc-api-1-syain)などを参考にしました。

処理の概要としては、クライアントでサーバの変更通知をリッスン → 変更通知が届いたらそのデータをDart内のStreamに流す → ChangeNotifierでUI側に反映、というような流れです。[ソースコード](https://github.com/mochi-yu/warikan-app-hacku2022/blob/main/lib/data/warikan_data.dart)としては```warikan_data.dart```に記載してあります。

<details><summary>gRPCに関するコードの抜粋</summary>

```dart:warikan_data.dart(抜粋)
// ConnectBillのデータを検知するStreamをListen
void _listenConnectBill() async {
  listenBill(cl, _warikanData).listen((event) async {
    if(event.type == BILL_CHANGE_TYPE.GUEST) {
      updateJoinUser();  // この関数内でnotifyListeners()を呼んでいる。
    } else if(event.type == BILL_CHANGE_TYPE.ITEM) {
      updatePayUserFromServe(event.id);  // この関数内でnotifyListeners()を呼んでいる。
    } else if(event.type == BILL_CHANGE_TYPE.CONFIRM) {
      _warikanData.isOpen = false;
      notifyListeners();
    }
  });
}

// ConnectBillのデータをリッスンするStreamの作成
Stream<ConnectBillResponse> listenBill(GrpcClient cl, WarikanData wada) async* {
  final responseStream = cl.client.connectBill(
    ConnectBillRequest()
      ..id = wada.roomID
      ..hostName = wada.hostUser.userName
  );

  await for (final response in responseStream) {
    print("サーバの更新を取得");
    yield response;
  }
}
```

</details>

自分が詰まった点として、FlutterのデバッグをChromeブラウザで行っていたのですが、どうにも**ブラウザはgRPC通信との相性が悪いようで、サーバに接続できません**でした。ブラウザで使用する場合は間にプロキシを挟んだり、ブラウザに対応したgRPC-Webというものを使用する必要があるそうです。WindowsアプリやAndroid, iOSでのデバッグでは正常に動作したので、皆さんもそちらをご使用ください。

## Flutterを使ってみて
何と言っても、**マルチプラットフォーム**にビルドができて、1つのコードでiOSからAndroid、Windows、ブラウザまで、幅広いアプリケーションを開発できるのが素晴らしいなと感じました。OSの最新の機能などは使えないことがあるとも耳しますが、一部分だけSwiftやKotlinでコードを書いてFlutterに組み込むということもできたので、あまり気にならないかなと感じました。

今回私は主にUIの制作に取り組みましたが、いわゆる**宣言的UI**でコーディングでき、自分がコードを書く時も、後からコードを見返すときも、とても読みやすかったです。また、**各Widgetがツリー状の階層構造**になっていて、Centerで要素を中央に寄せて、Columnで要素を縦に並べて…というように書けるのが本当に直観的だなと感じました。

1つ難点をあげるとすると、UIを記述するWidgetの階層構造が、コンポーネントを用いたとしても入れ子が深くなってしまい、頭が混乱することがありました。もちろん良質な書き方をすれば、それ次第で改善できることだとは思います。

それから、**Provider**のライブラリを用いることで、各Widgetへのリファレンスや再レンダリングを適切に扱えるのも便利だなと思いました。一方で、自分がProviderの仕様をうまく理解できていないこともあり、適切な実装か分からないままハチャメチャに実装してしまったのが反省です。今回はハッカソンということでスピード重視で進めましたが、**また改めてじっくりとProviderの扱いを学習する機会も必要だなと感じました。**

Flutterの学習全体を振り返ると、[『Flutter実践入門』][flutter-jissen]のような良質な教材のおかげもあり、学習コストはそこまで高くなかったと感じています。これで様々なプラットフォームのアプリケーションが開発できるのであれば、この機会に学習できて非常に良かったです。

## 参考にしたサイトのまとめ
- [『Flutter実践入門』][flutter-jissen]
- [『Step8: APIを呼び出してデータを取得してみよう』](https://zenn.dev/sugitlab/books/flutter_poke_app_handson/viewer/step8)
- [『【flutter】componentの作り方』](https://zenn.dev/tattu/articles/1501b603e966a8)
- [『FlutterのAppBarをカスタマイズする』](https://www.memory-lovers.blog/entry/2022/04/11/150000)
- [『【Flutter】Scrollableな領域と固定footerの構造』](https://zenn.dev/aryzae/articles/603a44ea6de662)
- [『【Flutter】親Widgetから子のメソッドを呼ぶには、GlobalObjectKeyを使う』](https://qiita.com/karamage/items/19d53a5870c32bf59742)
- [『Dartを使用したgRPCプログラムについて』](https://zenn.dev/kei_hongyo/articles/cb6aa853974e8e)
- [『【Flutter】gRPCを使ってAPI通信を実装する【前編】』](https://rightcode.co.jp/blog/information-technology/flutter-grpc-api-1-syain)
- [『FigmaでFlutter向きのアプリデザインを簡単に作成する方法』](https://www.kamo-it.org/blog/flutter-app-design/)