DroidKaigi2018で『コードで見るFlutterアプリの実装』というタイトルで話をしてきました。
聞きに来ていただいた皆さん、資料を読んでフィードバックをくれた皆さん、運営の皆さん、発表前に場を温めていただいた @mhidakaさん、ありがとうございました。
スライドだけだと話がわかりづらいところもあると思うので、書き起こし形式で補足しておこうと思います。当日用のスライドを一部削ったり、アドリブの台詞を省いたりはしています。
ちなみにこのやり方は、@yanzmさんが去年、今年にやっていてとてもよいなぁと思ったので真似させていただきました。
コードで見るFlutterアプリの実装
今日はFlutterアプリのコードの話をします。Flutter自体の内部の詳しい実装ではなく、Flutterでアプリを作る時にどうコードを書くのかという話です。よろしくお願いします。
自己紹介
konifarという名前でGitHub、Twitterをやっています。
今は株式会社KyashでAndroidエンジニアとして働いています。Kyashは、個人間で簡単に送金できるアプリです。
本題
それでは、本題に入ります。
サンプルアプリ
今日の話のサンプルとして、DroidKaigi2018のカンファレンスアプリをFlutterで作りました。 (実は会場内の半分くらいの方がiOSアプリを使ってくれていて驚きました)単純なTODOアプリなどよりは少し複雑です。今日はこれを題材にして話していきます。
今日話すこと
サンプルアプリが作れていることで、おそらく「Flutterを使ったらAndroid/iOSアプリが両方作れるんだな」ということはわかったと思います。でも、皆さんが知りたいのはもはやそういうことじゃないですよね。知りたいのは、「我々が普段の業務で使えるのか?」という点だと思います。
業務で使えるか検討する上で、きっと皆さん色々なことを考えると思います。
「来年になったらFlutterなくなっちゃってるんじゃないの?」とか、「複雑なレイアウトの実装はどうやるんだろう?」とか、「Push NotificationやAnalyticsなど運用に関わる要件に応えられるかな?」とか。検討事項は多いですよね。
時間も限られているので、残念ながら今日全てを説明することはできません。今回はここでいう実装の一部をメイントピックとしてお話したいと思います。
とはいえ、このあたりも気になるところだと思うので、一部かけ足でお話できればと思います。
今日のゴール
今日の話が終わった後で、Flutterを知らなかった人にとっては「なんかよさそうだな、おもしろそう、作ってみようかな」という気持ちになっていただくことを目標にしています。
また、すでにFlutterを知っていた人には「もしかしたら業務で使えるかも?」という気持ちになる、あるいは「業務で使えるか検討する手間がちょっと減ったな」くらいに感じていただければ今日の目標としては達成かなと思います。
今日話さないこと
逆にここに書いてある内容は話しません。
Dartの言語仕様の話。Javaに似てます。たぶん読めます。キャッチアップするときはlanguage-tourを一通りやれば大丈夫です。
Flutterのセットアップの話。公式ドキュメントに丁寧に書いてあります。自分は詰まるところなかったです。
Flutterの内部実装の話。もっといいスライドがあるのでそっちを見てください。
設計の話。これはReactとほとんど同じ議論なんですね。Flux、Reduxがよさそうという流れはありますが、Reactの設計の話を見て取り入れるといいと思います。Fluxでの検証を見てみたければ http://fluttersamples.com/ が参考になるかもしれないです。
既存アプリにFlutterを導入する話。これはやってないので話せません。仕組みとしてはできます。サンプルのplatform_viewを見てみてください。
iOSの話。DroidKaigiですからね。しません。リリースは大変とだけ覚えておけば大丈夫です。
1. Flutterのおさらい
では、まずはFlutterのおさらいからサッとやっていきましょう。
Flutterとは
Flutterは、OS推奨のデザインに合わせた綺麗なアプリを素早く作るためのクロスプラットフォームSDKです。Googleが作っていて、言語はDartです。レイアウトはxmlで書くのではなくコードでWidget Treeというのを書いて作ります。Widget Treeって何?というのは後で説明します。
ReactNativeとの違い
ReactNativeとどう違うの?と感じる方も多いと思います。違いは色々あるのですが、自分が大きく違うと感じたところを3つ書いてみました。
まずわかりやすいところだと、言語が違いますね。FlutterはDartで書きますが、ReactNativeはJavascriptで書きます。
Flutterはレンダリング部分は自前のエンジンを自前で持っているのに対して、ReactNativeはJavascriptからNativeのUIを呼び出す形です。
そして自分がかなり驚いたのは、FlutterはかなりたくさんのUIライブラリを公式が提供しているという点です。ReactNativeは基本的にUIの面倒は3rd partyのライブラリにお任せしているのでいくつかのライブラリの中から選定する必要がありますよね。Flutterではまずは公式のライブラリを使っておけばそれなりに綺麗なものができます。
豊富なWidget
ではどのくらい手厚くサポートしてくれているのかというと、MaterialDesignガイドラインに載っているデザインのパターンはほぼすべてサポートされています。本当にたくさんのWidgetがあります。だからこそ、まずはどんなWidgetがあってどう使うかをざっと知っておくことが高速に開発していく鍵となります。
例 : Scaffold
例として、Scaffold Widgetを見てみましょう。この画面は実はScaffoldという大きなひとつのWidgetで作られています。
appBar
に AppBar
Widgetをセットするだけでこう表示されます。
drawer
に Drawer
WidgetをセットするとDrawerが表示されます。すごく楽ですよね。
body
にメインのコンテンツとなるWidgetをセットするとここに表示されます。
Scafflodクラスのプロパティを見ると、他にも色々あります。 floatingActionButton
に FloatingActionButton
をセットすると右下にFABが表示されます。 bottomNavigationBar
もありますね。要するに、MaterialDesignで画面を作る時の雛形を用意してくれているわけです。こういうのがAndroidにも欲しい。
Scaffoldのように便利なWidgetが他にも本当にたくさんあるんですね。なので、再発明にならないよう、どんなWidgetがあるかをあらかじめ知っておくことが重要です。公式ページにWidgets Catalogというのがあるのでそれを見ておきましょう。
コードで見るFlutter
それでは、おさらいはこのくらいにして、もう少しFlutterアプリの実装を見ていきます。
Widget
まずは先ほどから触れているWidgetについてです。
Widgetの基本
FlutterではすべてのUIをWidgetで作ります。Widgetにはstatelessとstetefulの2種類があり、いくつかのWidgetをツリーのように組み合わせて作ります。
すべてはWidgetであるとはどういうことか。例を見ていきましょう。たとえばこのFavoriteの部分。この最小単位のWidgetはハートマークの Icon
です。
このFavoriteをタップできるようにするために、 IconButton
というWidgetでラップしています。ここまではわかりやすいと思います。
実はこの IconButton
を右下に配置するため、Positioned
というWidgetでさらにラップしています。Androidの場合は、Button自体に layout_~
attributeを指定して位置を指定すると思いますが、Flutterの場合は位置を表すのもWidgetを使います。Widgetは目に見えるUIだけを表現するものではないということです。
さらに、このボタンはタップ時にちょっとしたアニメーションをつけているのですが、そのアニメーションもWidgetです。なんとなく『すべてはWidgetである』という意味がわかってきたでしょうか。
おそらく皆さんコードを見た方がイメージがつきやすいと思うのでコードを見てみましょう。
トップレベルにあるのは位置を決める Positioned
Widgetです。bottom と right を指定しています。実はこの親のWidgetで 16.0
のpaddingを指定しているので、ここでは -8.0
を指定して位置を調整しています。その child
プロパティにアニメーションのための ScaleTransition
Widgetを入れています。さらにその child
に IconButton
Widget、その下に Icon
Widget を入れています。
このように、レイアウトを指定するためのWidget、アニメーションをつけるためのWidgetといった具合に、目に見えるWidgetだけでなくさまざまなものをWidgetで実装していくんですね。
Stateless & Stateful
次に、Widgetの種類であるstatelessとstatefulについて。その名のとおり、状態を持たないWidgetはstatelessで、状態を持つWidgetはstatefulで実装します。
statefulの方は少しわかりにくいかもしれないですが、通信結果やユーザーの操作で動的に変更が起きる場合など、stateを持つべきWidgetはstatefulにします。
statefulの例: Loadingの表示
例として、ローディングの切り替えの実装を見てみましょう。AppBarの下部分は全体が大きなWidgetです。タブに表示するRoomの一覧をロードしてから表示されます。
Loadingの表示
ロード中かどうかのstateをboolで持ちます。
initState()
というのは、Androidでいう onCreate()
のようなものです。StatefulWidgetが初期化されるときに一度呼ばれます。
initState()
の中で、タブに表示するRoomのデータを取得します。async、awaitがあるのはいいですね。
終わったら onDataLoaded()
という関数が呼ばれます。
onDetaLoaded()
の中では setState()
が呼ばれ、そこで先ほどの _isLoading
の値が変更されます。ここが一番重要です。
StatefulWidgetの中でstateを書き換えるときは、必ず setState()
を使うようにします。
setState()
でstateが書き換わると build()
が呼ばれます。この中で、 _isLoading
の値を見て、ロード中であればプログレスバーのWidget、そうでなければRoomのタブのWidgetを返すようにしておきます。
こうすることで、stateを変更することで表示するWidgetを変更することができます。
StatefulWidgetのポイント
ReactNativeに触れている人はイメージしやすいかと思いますが、実装するときには『どのWidgetがなんのstateを持つべきかを決めておく』のが一番大事なポイントです。そのあたりの考え方については今回は説明を省略します。Adding Interactivity to Your Flutter App - Flutter を読んでいただくとイメージがつかみやすいかと思います。
Widget単体についてはなんとなくイメージがついたかと思います。では、実際にWidgetを使ってどのようにレイアウトを組み立てていくのかを説明していきます。
先ほど少し触れましたが、FlutterではWidgetをツリーのように入れ子にしてレイアウトを組み立てていきます。Flutterが推奨しているIntelliJプラグインがサポートしてくれます。
実際にどんなふうにやっているのか簡単なデモをやります。
Widget Tree デモ
設定画面ですね。ここは ListView
というWidgetを使っていて、 children
プロパティにWidgetをセットするとリストで表示されます。
この下にテキストを表示してみましょう。new Text("layout test",)
と書いて保存すると…
1秒で反映されました!すごいですね?
では次にこの文字の色を変えてみましょう。 style: const TextStyle(color: Colors.blue,)
を追加して保存すると…
青くなりました!Androidに戻るとなぜ俺はgradle buildで2分も待たなければいけないのか、という気持ちになってきますね?
次にここにpaddingをつけてみます。 AndroidだとTextViewのattributeにpaddingがありますが、FlutterではPaddingもWidgetでTextをラップする形で実装します。
Widgetをラップするときは、IntelliJのFlutterプラグインの機能を使います。Ctrl + Enter
で、 Wrap with new widget
というメニューが出てくるので再度Enter。
そうすると、自動的にWidgetでラップされます。WidgetをContainerに書き換えて、paddingをセットします。
paddingには、 EdgeInsets.all(16.0)
を指定します。CSSに似た書き方ですね。
さらに、FontSizeを変えてみたり。1秒で反映されるので、レイアウトエディタがなくても実機で確認しながら作っていくことができます。
さらに、左にアイコンを表示するために、横にWidgetを並べられる Row
Widgetでラップします。 Row
は複数のWidgetを持つので、プロパティの名前はchildではなくchildrenです。そのため、書き換えたすぐはエラーが消えません。
ここで Option + Enter
を押すと、Quick Fixが表示されます。 convert to children
を選択すると…
プロパティがchildrenに変換され、要素もリストになりました。エラーが消えましたね。
Icon
widgetを追加して、適当にアイコンを指定し、色も文字と同じ青を指定してみましょう。保存すればすぐに反映されますね。
Widget Treeまとめ
こんな感じで、IntelliJのプラグインの機能を使ってサクサクとWidget Treeを作っていきます。
とはいえ、先ほど指定した color
や padding
といったWidgetのプロパティは知らないとわかりませんよね。Androidのレイアウトxmlと同じでやっていくうちにだんだんとわかってきますが、わからなければWidgetのクラスの中を見て、コメントやプロパティの型を調べるとなんとなく使い方がわかると思います。DartはJavaと似ていて、おそらく皆さんなら読めると思うので大丈夫です。
データの扱い
レイアウトについてはこのへんにしておいて、次はデータの扱いについて見ていきます。
ここでいうデータの扱いというのは何かというと、ネットワーク経由でデータを取得して、レスポンスをモデルに変換して、ローカルにキャッシュして、といったよくある一連の流れの実装の仕方のことです。
Firebaseをつかう
Firebaseをつかう場合はとても簡単です。公式pluginが用意されているのでそれを使いましょう。
cloud_firestore、firebase_database、firebase_storage、揃ってます。
サンプルアプリでは、ユーザーがFavoriteに登録したデータをcloud_firestoreに保存しています。
データ構造は、usersの中にuserIdのリストを持ち、その下にfavorites
というコレクションがあって、sessionIdのリストの中にfavoriteというキーでboolを持っています。
cloud_firestoreからデータをロードするコードはこんな感じです。firestoreのインスタンスからsnapshotのstreamを取得して、任意のデータを取得します。これはcloud_firestoreの仕様そのままです。
httpライブラリをつかう
Firebaseを使わずAPIからデータを取得する場合も難しくありません。 dartの標準ライブラリであるhttpとconvertを使います。
最近のAndroidだとRetrofitやgson、moshiなどのライブラリが必要ですが、dartでは標準ライブラリを使うだけで実装できます。
たとえば、サンプルアプリではセッション一覧の取得はこのAPIで取得しています。
https://droidkaigi.jp/2018/sessionize/all.json
コードはこれだけです。 http.read()
で帰ってきたレスポンスを、convertライブラリを使ってJSON.decode()
すればjsonオブジェクトになります。
モデルへのマッピング
そのオブジェクトをどうやってモデルに変換するかという話です。
ひとつは、jsonオブジェクトから愚直にマッピングするやり方。今回のサンプルアプリでは、all.jsonで返ってくるjsonの形がわりとつらめだったのでこのやり方でやりました。
Entityに自動でマッピングしたい場合には、Googleの提供しているbuilt_valueというライブラリを使えばできます。
Preferenceへの値保存
Preferenceに保存したいときは、公式のshared_preferencesを使います。Android/iOSともに動きます。
サンプルアプリでは、前回開いていたタブ位置の保存と復元に使っています。
SharedPreferences.getInstance()
でインスタンスを取得して、値を保存するときは putInt()
、取得するときは getInt()
を呼ぶだけです。Androidでの実装とほとんど同じですね。
Database
DBに保存するときはsqliteを操作するsqfliteを使います。Android/iOSともに動きます。
生のSQLの実行と、Insert、Delete、UpdateなどのHelperが用意されています。
ここからは、Flutterのここどうなってるの?という部分を一問一答形式でかけ足で話していきます。
Q1. CIまわせる?
CIまわせるか?まわせます。ただし、当然ですがFlutterを実行できる環境を整える必要があります。
TravisCIだとこんな感じです。 addons
の中で環境を指定しています。他のCIサービスの場合には、Dockerで作ってしまった方がいいかもしれないです。
before_script
の中でflutterのインストールを行い、 script
の中でテストを実行しています。
Q2. Analyticsどうするの?
公式pluginのfirebase_analyticsを使います。
Flutterでは画面遷移をnavigatorという仕組みで実装します。スクリーンビューをトラッキングしたい場合には、FirebaseAnalyticsObserverを navigatorObservers
にセットすればできます。
ログを送信したいときは、analyticsオブジェクトから log~
メソッドを呼ぶだけです。これらはFirebaseAnalyticsの仕様そのままの命名ですね。
Q3. Push Notificationはどうする?
Push Notificationは、firebase_messaging pluginを使います。
Q4. Animationはサポートされてる?
Animationはかなりサポートされています。
Animations in Flutter - Flutter
MaterialDesignガイドライン内のAnimationやTransitionくらいならわりと楽に実装できます。
ただ、先ほど少し話したように、AnimationもWidgetで実装するのでAndroidでの実装との違いに最初は少し戸惑うかもしれません。
どのくらいサポートされているかというと、こういうAnimationもできるんだなぁと思っておいてください。ただし、複雑なものはやはりコードもそれなりに頑張らなければいけないです。
Q5. ユニットテストどう書くの?
Androidのjunitでのテストと似てます。mockitoとtest.dartを使って書きます。詳しくは Testing Flutter Apps - Flutter にまとまっています。
これはサンプルアプリでJsonからモデルにパースするメソッドのテストです。
expect()
で値を確認しています。実行は、 flutter test
コマンド叩くか、IntelliJの実行ボタンを押すだけです。
Q6. 多言語化どうするの?
多言語化するときは、自分でlanguage_codeごとのマップを作って実装するか、dart:intlを使います。
やり方は、Internationalizing Flutter Apps - Flutter に詳しく説明されていますが、Androidのstrings.xmlの仕組みと比べると言語追加後にスクリプトを流さなければならなかったりして少し面倒です。このあたりはIntelliJのpluginで今後楽にできるようになっていくのではないかと予想しています。
また、多言語化しているとWidgetのテストがコケるので、 tester.pump()
を呼んでおくというワークアラウンドが必要
だったりもします。
https://github.com/flutter/flutter/issues/1865
Q7. クラッシュログの収集方法は?
Firebase Crash Reporting か Sentryをつかうように書いてあります。
Flutterは独自でレンダリングエンジンを持っていて、Activityの中ではFlutterのViewが一枚いるだけです。
Crashlyticsではdartコードの中のクラッシュは検知できないので、自分でエラーログを収集するように実装しておく必要があります。
Q8. ライブラリを探すときは?
何か便利なライブラリがないか探す時には、公式plugin か dartのライブラリ から探してみましょう。pure dartライブラリの資産も使えます。
公式plugin、たくさんあります。名前を見るとどんなことができるかなんとなくわかります。Firebase関係が多いですね。
公式pluginも重複して載っていますが、他にもFlutterで使えるdartライブラリはたくさんあります。2018年2月8日時点で70個近くありました。グラフを表示するものや、markdownの文字をレンダリングするものなど、色々揃ってきていたので、一度さっと目を通しておくとできることがわかってよいかもしれません。
まとめ
ここまで少し駆け足でFlutterについて触れてきました。最後にまとめます。
Flutterを使って開発する時のポイントは3つです。
Widgetがたくさんあるので、どんなものがあってどう使うのかを知っておきましょう。
開発効率をあげるためにIntelliJを使いこなしましょう。これはAndroid開発に慣れている皆さんなら問題ないかと思います。
何かわからないことがあれば、公式ドキュメントとサンプルコードが充実しているので参考にしましょう。またSDK自体のコードを読むのもよいです。dartのコードはJavaと似ていて読みやすいと思います。
業務でつかえるのか?
では、"業務"でつかえるのか?という最初の話に戻ります。
自分でアプリを作ってリリースしてみた感想だと、つかえそうな気はしています。公式ドキュメントも充実していますし、変なエラーにめちゃくちゃハマって時間を食われまくるということもなかったです。
とはいえ、まだαで、最近βブランチが切られたくらいなのでFlutter自体もどうなるかはわかりません。変更もたくさん入ってくると思います。結局こういうのはやってみないとわからないのですよね。
何が言いたいかというと、あとは結局"覚悟"次第ということです。今から業務でつかうのであれば、「何かあっても俺が一緒にFlutterを育てる」という覚悟が必要です。
株式会社Kyashにて、Flutterのプロダクション導入の機会を虎視眈眈と伺っています。皆さんももし興味が湧いたなら、一緒にやっていきましょう。
ありがとうございました。
この発表の次の日、2018年2月10日にFlutter v0.1.0がリリースされました。
おそらくGoogle I/O 2018でもFlutterに関する発表があると思うので、リポジトリを監視しながら楽しみに待ちたいと思います。