はじめにこんにちは、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. Alldone ! [✓] Flutter: is fully installed. (Channel beta, v1.17.0, on Mac OS X 10.14.6 18G4032, locale ja-JP) [!] Android toolchain - developfor Android devices: is partially installed; more components are available. (Android SDK version 28.0.3) [✗] Xcode - developfor iOS and macOS: is not installed. [✓] Chrome - developfor 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 isin 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 > <!-- 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 vsStatelessWidgetウィジェットにはStateful とStateless の2種類があり、Flutter Doc JP では以下のように説明されています。
今回利用しているChartJSプラグインにおいてChartJS クラスはStatefulWidget として定義されていたため、MyApp クラスはStatefulWidget で実装しています。用途的には、初回通信でのみ値を取得および描画できればよかったので、StatelessWidget でも良いのかなと思いましたが、プラグインの実装に従う形でStatefulWidget を利用しています。
FutureBuilder vsStreamBuilder素のBuilderを使うと、Cloud Firestoreのデータ取得が完了する前に画面の描画処理が走ってしまいます。そのため、非同期通信を待つBuilderを使う必要がありました。
非同期BuilderにはFutureBuilder とStreamBuilder の2種類があります。
StreamBuilder非同期処理の更新する変数が変化する度にウィジェットをbuildし直すBuilder FutureBuilder 上記の説明はこちら のページから拝借しました。
今回のアプリでは初回のデータ取得のみを待てばよいので、BuilderはFutureBuilder を利用しました。
おわりに今回はFlutter入門記事ということで、簡単ではありますがFirebaseを利用したアプリ開発をご紹介させて頂きました。
それにしてもDartという開発言語、Future社員の私にとってFuture型の存在がなにか特別な感情をもたらしてくれました。
Flutterでアプリ書いてるけど何回も"Future"って登場してて、知らぬ間に愛社精神が磨かれてる気がする。Dart...お前まさか...
— Yasuhiro Murata (@famipapamart)April 26, 2020 …という冗談はさておき、書いている間に楽しい気分にさせてくれる言語というのもモチベーションの一部だと思うので、この感情はこれからも大事にしていきたいと思います。
春の入門祭り🌸 はまだまだ続きます! Future技術ブログ始まって以来の超大型連載、ぜひぜひ最後までお付き合いください!!