フューチャー技術ブログ

ゼロから始めるFlutter生活

はじめに

こんにちは、TIGの村田です。春の入門祭り 第3弾として、兼ねてより気になっていたFlutterに入門してみた話をお届けします。

普段はクラウドインフラ寄りな技術を触っているのですが、実は生まれと育ちはフロントエンド畑で、過去にはUrushiという当社製のOSS開発に携わっていました。

Urushiについては以下のブログで詳細に語られています。2017年の記事ですね。懐かしい限りです。
ES2015 Web componentsと国産Web componentsフレームワークUrushi

昨今の自粛生活の中で私のチーム内でもオンラインもくもく会(社内では通称「引きこもりもくもく」)が流行っているのですが、久しぶりにフロントエンドに触れようと思い立ったのが事の経緯です。

Flutterとは

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

https://flutter.dev/

FlutterはGoogle製のフレームワークです。クロスプラットフォーム対応はAndroidとiOSだけでなくWebまで行き届いていますが、これはReactNativeなど他のクロスプラットフォーム開発フレームワークでも同様ですね。

Flutterの特徴は、開発言語にDartという言語を採用している点です。DartはGoogle製のプログラミング言語なのですが、書き心地はJavaScriptに非常に近く、久しぶりにJSを嗜んだ私でもとっつきやすい言語でした。

やりたいこと

絶賛無人島生活🏝️を満喫していた私は、Flutterでもくもくするにあたって、「カブ価の推移を可視化する」というゴールを定めました。本記事ではカブ価についての詳細は割愛しますが、株価のように時系列に合わせて上下する値の推移を銘柄(島)ごとにグラフ📈化したい、というのが私のやりたかったことです。

Flutterは今回Web版を利用しています。バックエンドのDBについては、Firebaseでうまくライトにやれないかなー程度に考えていました。(最終的にCloud Firestoreを採択してますが、それについては後述します)

実際にやってみる

まずは環境構築

兎にも角にもまずは環境構築をしていきます。私の端末はMacOSなので、MacOS用の手順に従って環境構築を進めていきます。ちなみに今回利用するのはWeb版なので、Web版向けの手順も合わせて実施する必要があります。

Flutterのバージョンを確認しておきましょう。Flutter Webを使うのでChannel betaを利用しています。

$ flutter --version
Flutter 1.17.0 • channel beta • https://github.com/flutter/flutter.git
Framework • revision e6b34c2b5c (4 weeks ago) • 2020-05-02 11:39:18 -0700
Engine • revision 540786dd51
Tools • Dart 2.8.1

さて、手順にはmockアプリをインストールしそれを動作するところまで含まれていますので、やってみましょう!

$ flutter create myapp
Creating project myapp...


(中略)


Running "flutter pub get" in myapp... 1.9s
Wrote 77 files.

All done!
[✓] Flutter: is fully installed. (Channel beta, v1.17.0, on Mac OS X 10.14.6 18G4032, locale ja-JP)
[!] Android toolchain - develop for Android devices: is partially installed; more components are available. (Android SDK version 28.0.3)
[✗] Xcode - develop for iOS and macOS: is not installed.
[✓] Chrome - develop for the web: is fully installed.
[!] Android Studio: is partially installed; more components are available. (version 3.4)
[✓] VS Code: is fully installed. (version 1.45.1)
[✓] Connected device: is fully installed. (2 available)

Run "flutter doctor" for information about installing additional components.

In order to run your application, type:

$ cd myapp
$ flutter run

Your application code is in myapp/lib/main.dart.

これで必要なファイル群が myapp 配下に作成されます。 lib/main.dart が実際にimplementする対象のファイルになるのですが、一旦触らずにアプリの起動を確認します。

$ cd myapp
$ flutter run -d chrome

以下のような画面が表示されれば成功です!画面はとてもシンプルで、右下の「+ボタン」をクリックすると画面内のカウンタがインクリメントされていきます。

これでアプリの開発環境は整いました。では早速アプリの開発に移っていきましょう。

ChartJSプラグインを使ってグラフ表示を行う

Flutter(Dart)で利用できるプラグインはpub.devというページにまとまっているのですが、今回はその中からflutter_web_chartjsというWeb版Flutterで使えるChartJSライブラリを使うことにしました。

使いたいパッケージは pubspec.yaml というファイルに以下のような形で記載します。

dependencies:
flutter_web_chartjs: ^0.2.5

これで準備完了です。
Exampleに従ってアプリを実装すれば以下のような画面を表示することができますが、詳細はここでは割愛します。

縦軸と横軸に該当するデータセット群を用意すればグラフを表示できることが確認できました。

FirebaseのDBと繋ぎたい

DBはなるべく簡単に扱えるものにしたく、Cloud Firestoreを使うことにしました。

ちなみに、Cloud Firestoreを使うかRealtime Databaseを使うか少しだけ悩んだのですが、そのへんはこちらの資料にまとめてあります。

諸々の背景は省きますが、「おとなしくCloud FirestoreをNative Modeで使おう」というのが私の結論です。

利用プラグインの追加

さて、実装に移っていきます。先程同様、まずは使いたいプラグインを pubspec.yaml に記載するところからです。cloud_firestoreを使うため、以下のように追記しました。

dependencies:
flutter_web_chartjs: ^0.2.5
cloud_firestore: ^0.13.5

Firebase Appの作成

次にFirebase Appの作成です。Firebase Consoleにて「アプリを追加」から作成し、 アプリID を取得します。

その際、以下のような形でHTMLファイルの修正も求められます。

今回は web/index.html が修正対象になります。この辺の経緯はGitHubページのREADMEにも記載があります。

Due to this bug in dartdevc, you will need to manually add the Firebase JavaScript files to your index.html file.

Web版のFlutterは鋭意アップデート中ということもあり、今後改善されていくポイントなんだろうなと思っています。

index.html のアップデート

必要な変更を加えた index.html<body> タグは以下のようになりました。

<body>
<!-- Due to this bug in dartdevc, you will need to manually add the Firebase JavaScript files to your index.html file. see: https://github.com/FirebaseExtended/flutterfire/blob/master/packages/cloud_firestore/cloud_firestore_web/README.md-->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-firestore.js"></script>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<!-- ADD THIS BEFORE YOUR main.dart.js SCRIPT -->
<script>
// TODO: Replace the following with your app's Firebase project configuration.
// See: https://support.google.com/firebase/answer/7015592
var firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
databaseURL: "YOUR_DATABASE_URL",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
measurementId: "YOUR_MEASUREMENT_ID"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
<!-- END OF FIREBASE INIT CODE -->
<script src="main.dart.js" type="application/javascript"></script>
</body>

main.dart にて本実装

ここまでの下準備が整えば、本実装を行うのみです。
Example等を参考にしつつ実装を進めたのが以下です。

まずはimport文とmain文。FirebaseAppを設定してアプリを起動します。

import 'package:flutter/material.dart';
import 'package:flutter_web_chartjs/chartjs.models.dart';
import 'package:flutter_web_chartjs/chartjs.wrapper.dart';

import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

final FirebaseApp app = await FirebaseApp.configure(
name: 'Turniprice Visualizer',
options: const FirebaseOptions(
apiKey: "YOUR_API_KEY",
projectID: "YOUR_PROJECT_ID",
googleAppID: "YOUR_APP_ID",
databaseURL: "YOUR_DATABASE_URL",
storageBucket: "YOUR_STORAGE_BUCKET",
),
);
final Firestore firestore = Firestore(app: app);

runApp(MyApp(firestore: firestore));
}

先程 runApp() に引数として渡された MyApp クラスの実体です。 _getChartData() が今回のキモとなる部分であり、FirestoreへのアクセスとChartJSで描画するデータセットの整形を担っています。

class MyApp extends StatefulWidget {
MyApp({this.firestore});
final Firestore firestore;

var values = {};
List<String> x = [];

final colors = [
Colors.blue.withOpacity(0.4),
Colors.yellow.withOpacity(0.4),
Colors.red.withOpacity(0.4),
Colors.green.withOpacity(0.4)
];

Future<ChartData> _getChartData() async {
// Query documents
final querySnapshot = await firestore
.collection("prices")
.orderBy("date", descending: true)
.orderBy("ampm", descending: true)
.getDocuments();

querySnapshot.documents.forEach((doc) => format(doc));

// ChartDataオブジェクトを作成
// datasetsを作成
List<ChartDataset> datasets = [];
int count = 0;
values.forEach((key, value) {
datasets.add(
ChartDataset(
data: value,
label: key,
backgroundColor: colors[count],
)
);
count++;
});

final chartData = ChartData(
labels: x,
datasets: datasets,
);

return chartData;
}

void format(DocumentSnapshot doc) {
print(doc["date"] + "/"+ doc["ampm"] + "/"+ doc["label"] + "/"+ doc["val"].toString());

// "date+ampm"がxにいるかチェックし、いなければリストに追加
var xVal = doc["date"] + doc["ampm"];
if (!x.contains(xVal)) {
x.add(xVal);
}

// labelをキーにしたリストがvaluesにあるかチェックし、いなければリストに追加
values[doc["label"]] ??= [];
// valueをリストに追加
values[doc["label"]].add(doc["val"]);
}

@override
_MyAppState createState() => _MyAppState();
}

ちなみにデータレコードは以下のような情報を持っています。

最後にStateクラスです。 MyApp クラスは StatefulWidget なのでこのStateクラスにてbuild処理を実装します。実装時に気にしたポイントは後述します。

class _MyAppState extends State<MyApp> {

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Turniprice Visualizer'),
),
body: Center(
child: FutureBuilder(
future: widget._getChartData(),
builder: (BuildContext context, AsyncSnapshot<ChartData> snapshot) {
// 通信中
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator();
}
// 通信終了だがエラーあり
if (snapshot.hasError) {
print("[USER-ERROR]" + snapshot.error.toString());
return Text(snapshot.error.toString());
}
// 通信正常終了
if (snapshot.hasData) {
print("[USER-INFO]Fetching data succeeded.");
return ChartJS(
id: 'graph',
config: ChartConfig(
type: ChartType.line,
options: ChartOptions(
animationConfiguration: ChartAnimationConfiguration(
duration: Duration(milliseconds: 1200),
easing: ChartEasing.easeOutQuart,
),
scales: ChartScales(
xAxes: [
ChartAxis(
type: ChartCartesianAxisType.category,
)
],
),
tooltip: ChartTooltip(
intersect: false,
mode: ChartTooltipMode.isIndex,
callbacks: ChartCallbacks(label: (tooltip) {
return 'R\$ ${tooltip.value}';
}))),
data: snapshot.data),
);
} else {
print("[USER-INFO]Fetching data failed.");
return Text('No Data');
}
},
),
),
),
);
}
}

main.dart 実装時に気にしたこと

StatefulWidget vs StatelessWidget

Widgetには StatefulStateless の2種類があり、Flutter Doc JPでは以下のように説明されています。

今回利用しているChartJSプラグインにおいて ChartJS クラスは StatefulWidget として定義されていたため、 MyApp クラスは StatefulWidget で実装しています。用途的には、初回通信でのみ値を取得および描画できればよかったので、 StatelessWidget でも良いのかなと思いましたが、プラグインの実装に従う形で StatefulWidget を利用しています。

FutureBuilder vs StreamBuilder

素のBuilderを使うと、Cloud Firestoreのデータ取得が完了する前に画面の描画処理が走ってしまいます。そのため、非同期通信を待つBuilderを使う必要がありました。

非同期Builderには FutureBuilderStreamBuilder の2種類があります。

  • StreamBuilder
    • 非同期処理の更新する変数が変化する度にウィジェットをbuildし直すBuilder
  • FutureBuilder
    • 指定した非同期処理の完了を待つBuilder

上記の説明はこちらのページから拝借しました。

今回のアプリでは初回のデータ取得のみを待てばよいので、Builderは FutureBuilder を利用しました。

おわりに

今回はFlutter入門記事ということで、簡単ではありますがFirebaseを利用したアプリ開発をご紹介させて頂きました。

それにしてもDartという開発言語、Future社員の私にとってFuture型の存在がなにか特別な感情をもたらしてくれました。

…という冗談はさておき、書いている間に楽しい気分にさせてくれる言語というのもモチベーションの一部だと思うので、この感情はこれからも大事にしていきたいと思います。

春の入門祭り🌸 はまだまだ続きます!Future技術ブログ始まって以来の超大型連載、ぜひぜひ最後までお付き合いください!!