はじめに こんにちは、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を採択してますが、それについては後述します)
実際にやってみる まずは環境構築 兎にも角にもまずは環境構築をしていきます。私の端末はMac OSなので、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 myappflutter 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 > <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 > <script > if ('serviceWorker' in navigator) { window .addEventListener ('load' , function ( ) { navigator.serviceWorker .register ('flutter_service_worker.js' ); }); } </script > <script > 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" }; firebase.initializeApp (firebaseConfig); </script > <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 { final querySnapshot = await firestore .collection("prices" ) .orderBy("date" , descending: true ) .orderBy("ampm" , descending: true ) .getDocuments(); querySnapshot.documents.forEach((doc) => format(doc)); 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()); var xVal = doc["date" ] + doc["ampm" ]; if (!x.contains(xVal)) { x.add(xVal); } values[doc["label" ]] ??= []; 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
実装時に気にしたことウィジェットには Stateful
と Stateless
の2種類があり、Flutter Doc JP では以下のように説明されています。
今回利用しているChartJSプラグインにおいて ChartJS
クラスは StatefulWidget
として定義されていたため、 MyApp
クラスは StatefulWidget
で実装しています。用途的には、初回通信でのみ値を取得および描画できればよかったので、 StatelessWidget
でも良いのかなと思いましたが、プラグインの実装に従う形で StatefulWidget
を利用しています。
FutureBuilder
vs StreamBuilder
素のBuilderを使うと、Cloud Firestoreのデータ取得が完了する前に画面の描画処理が走ってしまいます。そのため、非同期通信を待つBuilderを使う必要がありました。
非同期Builderには FutureBuilder
と StreamBuilder
の2種類があります。
StreamBuilder
非同期処理の更新する変数が変化する度にウィジェットをbuildし直すBuilder
FutureBuilder
上記の説明はこちら のページから拝借しました。
今回のアプリでは初回のデータ取得のみを待てばよいので、Builderは FutureBuilder
を利用しました。
おわりに 今回はFlutter入門記事ということで、簡単ではありますがFirebaseを利用したアプリ開発をご紹介させて頂きました。
それにしてもDartという開発言語、Future社員の私にとってFuture型の存在がなにか特別な感情をもたらしてくれました。
…という冗談はさておき、書いている間に楽しい気分にさせてくれる言語というのもモチベーションの一部だと思うので、この感情はこれからも大事にしていきたいと思います。
春の入門祭り🌸 はまだまだ続きます! Future技術ブログ始まって以来の超大型連載、ぜひぜひ最後までお付き合いください!!