Konifar's WIP

親方!空からどらえもんが!

DroidKaigi2018で発表したFlutterアプリの話のスライド補足

DroidKaigi2018で『コードで見るFlutterアプリの実装』というタイトルで話をしてきました。

speakerdeck.com

聞きに来ていただいた皆さん、資料を読んでフィードバックをくれた皆さん、運営の皆さん、発表前に場を温めていただいた @mhidakaさん、ありがとうございました。

スライドだけだと話がわかりづらいところもあると思うので、書き起こし形式で補足しておこうと思います。当日用のスライドを一部削ったり、アドリブの台詞を省いたりはしています。

ちなみにこのやり方は、@yanzmさんが去年今年にやっていてとてもよいなぁと思ったので真似させていただきました。

コードで見るFlutterアプリの実装

f:id:konifar:20180210110207p:plain

今日はFlutterアプリのコードの話をします。Flutter自体の内部の詳しい実装ではなく、Flutterでアプリを作る時にどうコードを書くのかという話です。よろしくお願いします。

自己紹介

f:id:konifar:20180210110357p:plain

konifarという名前でGitHubTwitterをやっています。

f:id:konifar:20180210110520p:plain

今は株式会社KyashAndroidエンジニアとして働いています。Kyashは、個人間で簡単に送金できるアプリです。

本題

f:id:konifar:20180210111300p:plain

それでは、本題に入ります。

サンプルアプリ

f:id:konifar:20180210111426p:plain

今日の話のサンプルとして、DroidKaigi2018のカンファレンスアプリをFlutterで作りました。 (実は会場内の半分くらいの方がiOSアプリを使ってくれていて驚きました)単純なTODOアプリなどよりは少し複雑です。今日はこれを題材にして話していきます。

今日話すこと

f:id:konifar:20180210111816p:plain

サンプルアプリが作れていることで、おそらく「Flutterを使ったらAndroid/iOSアプリが両方作れるんだな」ということはわかったと思います。でも、皆さんが知りたいのはもはやそういうことじゃないですよね。知りたいのは、「我々が普段の業務で使えるのか?」という点だと思います。

f:id:konifar:20180210112146p:plain

業務で使えるか検討する上で、きっと皆さん色々なことを考えると思います。

「来年になったらFlutterなくなっちゃってるんじゃないの?」とか、「複雑なレイアウトの実装はどうやるんだろう?」とか、「Push NotificationやAnalyticsなど運用に関わる要件に応えられるかな?」とか。検討事項は多いですよね。

f:id:konifar:20180210112738p:plain

時間も限られているので、残念ながら今日全てを説明することはできません。今回はここでいう実装の一部をメイントピックとしてお話したいと思います。

f:id:konifar:20180210112944p:plain

とはいえ、このあたりも気になるところだと思うので、一部かけ足でお話できればと思います。

今日のゴール

f:id:konifar:20180210113140p:plain

今日の話が終わった後で、Flutterを知らなかった人にとっては「なんかよさそうだな、おもしろそう、作ってみようかな」という気持ちになっていただくことを目標にしています。

また、すでにFlutterを知っていた人には「もしかしたら業務で使えるかも?」という気持ちになる、あるいは「業務で使えるか検討する手間がちょっと減ったな」くらいに感じていただければ今日の目標としては達成かなと思います。

今日話さないこと

f:id:konifar:20180210113454p:plain

逆にここに書いてある内容は話しません。

Dartの言語仕様の話。Javaに似てます。たぶん読めます。キャッチアップするときはlanguage-tourを一通りやれば大丈夫です。

Flutterのセットアップの話。公式ドキュメントに丁寧に書いてあります。自分は詰まるところなかったです。

Flutterの内部実装の話。もっといいスライドがあるのでそっちを見てください。

設計の話。これはReactとほとんど同じ議論なんですね。Flux、Reduxがよさそうという流れはありますが、Reactの設計の話を見て取り入れるといいと思います。Fluxでの検証を見てみたければ http://fluttersamples.com/ が参考になるかもしれないです。

既存アプリにFlutterを導入する話。これはやってないので話せません。仕組みとしてはできます。サンプルのplatform_viewを見てみてください。

iOSの話。DroidKaigiですからね。しません。リリースは大変とだけ覚えておけば大丈夫です。

1. Flutterのおさらい

f:id:konifar:20180210141403p:plain

では、まずはFlutterのおさらいからサッとやっていきましょう。

Flutterとは

f:id:konifar:20180210141450p:plain

Flutterは、OS推奨のデザインに合わせた綺麗なアプリを素早く作るためのクロスプラットフォームSDKです。Googleが作っていて、言語はDartです。レイアウトはxmlで書くのではなくコードでWidget Treeというのを書いて作ります。Widget Treeって何?というのは後で説明します。

ReactNativeとの違い

f:id:konifar:20180210141858p:plain

ReactNativeとどう違うの?と感じる方も多いと思います。違いは色々あるのですが、自分が大きく違うと感じたところを3つ書いてみました。

まずわかりやすいところだと、言語が違いますね。FlutterはDartで書きますが、ReactNativeはJavascriptで書きます。

Flutterはレンダリング部分は自前のエンジンを自前で持っているのに対して、ReactNativeはJavascriptからNativeのUIを呼び出す形です。

そして自分がかなり驚いたのは、FlutterはかなりたくさんのUIライブラリを公式が提供しているという点です。ReactNativeは基本的にUIの面倒は3rd partyのライブラリにお任せしているのでいくつかのライブラリの中から選定する必要がありますよね。Flutterではまずは公式のライブラリを使っておけばそれなりに綺麗なものができます。

豊富なWidget

f:id:konifar:20180210143148p:plain

ではどのくらい手厚くサポートしてくれているのかというと、MaterialDesignガイドラインに載っているデザインのパターンはほぼすべてサポートされています。本当にたくさんのWidgetがあります。だからこそ、まずはどんなWidgetがあってどう使うかをざっと知っておくことが高速に開発していく鍵となります。

例 : Scaffold

f:id:konifar:20180210172935p:plain

例として、Scaffold Widgetを見てみましょう。この画面は実はScaffoldという大きなひとつのWidgetで作られています。

f:id:konifar:20180210173541p:plain

appBarAppBar Widgetをセットするだけでこう表示されます。

f:id:konifar:20180210174950p:plain

drawerDrawer WidgetをセットするとDrawerが表示されます。すごく楽ですよね。

f:id:konifar:20180210175046p:plain

body にメインのコンテンツとなるWidgetをセットするとここに表示されます。

f:id:konifar:20180210175257p:plain

Scafflodクラスのプロパティを見ると、他にも色々あります。 floatingActionButtonFloatingActionButton をセットすると右下にFABが表示されます。 bottomNavigationBar もありますね。要するに、MaterialDesignで画面を作る時の雛形を用意してくれているわけです。こういうのがAndroidにも欲しい。

f:id:konifar:20180210175523p:plain

Scaffoldのように便利なWidgetが他にも本当にたくさんあるんですね。なので、再発明にならないよう、どんなWidgetがあるかをあらかじめ知っておくことが重要です。公式ページにWidgets Catalogというのがあるのでそれを見ておきましょう。

コードで見るFlutter

f:id:konifar:20180210181106p:plain

それでは、おさらいはこのくらいにして、もう少しFlutterアプリの実装を見ていきます。

Widget

f:id:konifar:20180210181538p:plain

まずは先ほどから触れているWidgetについてです。

Widgetの基本

f:id:konifar:20180210181647p:plain

FlutterではすべてのUIをWidgetで作ります。Widgetにはstatelessとstetefulの2種類があり、いくつかのWidgetをツリーのように組み合わせて作ります。

f:id:konifar:20180210182031p:plain

すべてはWidgetであるとはどういうことか。例を見ていきましょう。たとえばこのFavoriteの部分。この最小単位のWidgetはハートマークの Icon です。

f:id:konifar:20180210182142p:plain

このFavoriteをタップできるようにするために、 IconButton というWidgetでラップしています。ここまではわかりやすいと思います。

f:id:konifar:20180210182335p:plain

実はこの IconButton を右下に配置するため、Positioned というWidgetでさらにラップしています。Androidの場合は、Button自体に layout_~ attributeを指定して位置を指定すると思いますが、Flutterの場合は位置を表すのもWidgetを使います。Widgetは目に見えるUIだけを表現するものではないということです。

f:id:konifar:20180210182800p:plain

さらに、このボタンはタップ時にちょっとしたアニメーションをつけているのですが、そのアニメーションもWidgetです。なんとなく『すべてはWidgetである』という意味がわかってきたでしょうか。

f:id:konifar:20180210204546p:plain

おそらく皆さんコードを見た方がイメージがつきやすいと思うのでコードを見てみましょう。

トップレベルにあるのは位置を決める Positioned Widgetです。bottom と right を指定しています。実はこの親のWidget16.0 のpaddingを指定しているので、ここでは -8.0 を指定して位置を調整しています。その child プロパティにアニメーションのための ScaleTransition Widgetを入れています。さらにその childIconButton Widget、その下に Icon Widget を入れています。

このように、レイアウトを指定するためのWidget、アニメーションをつけるためのWidgetといった具合に、目に見えるWidgetだけでなくさまざまなものをWidgetで実装していくんですね。

Stateless & Stateful

f:id:konifar:20180210211013p:plain

次に、Widgetの種類であるstatelessとstatefulについて。その名のとおり、状態を持たないWidgetはstatelessで、状態を持つWidgetはstatefulで実装します。

statefulの方は少しわかりにくいかもしれないですが、通信結果やユーザーの操作で動的に変更が起きる場合など、stateを持つべきWidgetはstatefulにします。

statefulの例: Loadingの表示

f:id:konifar:20180210211702p:plain

例として、ローディングの切り替えの実装を見てみましょう。AppBarの下部分は全体が大きなWidgetです。タブに表示するRoomの一覧をロードしてから表示されます。

Loadingの表示

f:id:konifar:20180210212032p:plain

ロード中かどうかのstateをboolで持ちます。

f:id:konifar:20180210212151p:plain

initState()というのは、Androidでいう onCreate() のようなものです。StatefulWidgetが初期化されるときに一度呼ばれます。

f:id:konifar:20180210212615p:plain

initState() の中で、タブに表示するRoomのデータを取得します。async、awaitがあるのはいいですね。

f:id:konifar:20180210212748p:plain

終わったら onDataLoaded() という関数が呼ばれます。

f:id:konifar:20180210212836p:plain

onDetaLoaded() の中では setState() が呼ばれ、そこで先ほどの _isLoading の値が変更されます。ここが一番重要です。

StatefulWidgetの中でstateを書き換えるときは、必ず setState() を使うようにします。

f:id:konifar:20180210213209p:plain

setState() でstateが書き換わると build() が呼ばれます。この中で、 _isLoading の値を見て、ロード中であればプログレスバーWidget、そうでなければRoomのタブのWidgetを返すようにしておきます。

こうすることで、stateを変更することで表示するWidgetを変更することができます。

StatefulWidgetのポイント

f:id:konifar:20180210213453p:plain

ReactNativeに触れている人はイメージしやすいかと思いますが、実装するときには『どのWidgetがなんのstateを持つべきかを決めておく』のが一番大事なポイントです。そのあたりの考え方については今回は説明を省略します。Adding Interactivity to Your Flutter App - Flutter を読んでいただくとイメージがつかみやすいかと思います。

f:id:konifar:20180210213819p:plain

Widget単体についてはなんとなくイメージがついたかと思います。では、実際にWidgetを使ってどのようにレイアウトを組み立てていくのかを説明していきます。

f:id:konifar:20180210213950p:plain

先ほど少し触れましたが、FlutterではWidgetをツリーのように入れ子にしてレイアウトを組み立てていきます。Flutterが推奨しているIntelliJプラグインがサポートしてくれます。

実際にどんなふうにやっているのか簡単なデモをやります。

Widget Tree デモ

f:id:konifar:20180210223628p:plain

設定画面ですね。ここは ListView というWidgetを使っていて、 children プロパティにWidgetをセットするとリストで表示されます。

この下にテキストを表示してみましょう。new Text("layout test",) と書いて保存すると…

f:id:konifar:20180210224033p:plain

1秒で反映されました!すごいですね?

では次にこの文字の色を変えてみましょう。 style: const TextStyle(color: Colors.blue,) を追加して保存すると…

f:id:konifar:20180210224415p:plain

青くなりました!Androidに戻るとなぜ俺はgradle buildで2分も待たなければいけないのか、という気持ちになってきますね?

次にここにpaddingをつけてみます。 AndroidだとTextViewのattributeにpaddingがありますが、FlutterではPaddingもWidgetでTextをラップする形で実装します。

f:id:konifar:20180210224644p:plain

Widgetをラップするときは、IntelliJのFlutterプラグインの機能を使います。Ctrl + Enter で、 Wrap with new widget というメニューが出てくるので再度Enter。

f:id:konifar:20180210224944p:plain

そうすると、自動的にWidgetでラップされます。WidgetをContainerに書き換えて、paddingをセットします。

f:id:konifar:20180210225127p:plain

paddingには、 EdgeInsets.all(16.0) を指定します。CSSに似た書き方ですね。

f:id:konifar:20180210225502p:plain

さらに、FontSizeを変えてみたり。1秒で反映されるので、レイアウトエディタがなくても実機で確認しながら作っていくことができます。

f:id:konifar:20180210225606p:plain

さらに、左にアイコンを表示するために、横にWidgetを並べられる Row Widgetでラップします。 Row は複数のWidgetを持つので、プロパティの名前はchildではなくchildrenです。そのため、書き換えたすぐはエラーが消えません。

f:id:konifar:20180210225756p:plain

ここで Option + Enter を押すと、Quick Fixが表示されます。 convert to children を選択すると…

f:id:konifar:20180210225856p:plain

プロパティがchildrenに変換され、要素もリストになりました。エラーが消えましたね。

f:id:konifar:20180210230108p:plain

Icon widgetを追加して、適当にアイコンを指定し、色も文字と同じ青を指定してみましょう。保存すればすぐに反映されますね。

Widget Treeまとめ

f:id:konifar:20180210230327p:plain

こんな感じで、IntelliJプラグインの機能を使ってサクサクとWidget Treeを作っていきます。

とはいえ、先ほど指定した colorpadding といったWidgetのプロパティは知らないとわかりませんよね。Androidのレイアウトxmlと同じでやっていくうちにだんだんとわかってきますが、わからなければWidgetのクラスの中を見て、コメントやプロパティの型を調べるとなんとなく使い方がわかると思います。DartJavaと似ていて、おそらく皆さんなら読めると思うので大丈夫です。

データの扱い

f:id:konifar:20180210230800p:plain

レイアウトについてはこのへんにしておいて、次はデータの扱いについて見ていきます。

f:id:konifar:20180210230857p:plain

ここでいうデータの扱いというのは何かというと、ネットワーク経由でデータを取得して、レスポンスをモデルに変換して、ローカルにキャッシュして、といったよくある一連の流れの実装の仕方のことです。

Firebaseをつかう

f:id:konifar:20180210231106p:plain

Firebaseをつかう場合はとても簡単です。公式pluginが用意されているのでそれを使いましょう。

cloud_firestorefirebase_databasefirebase_storage、揃ってます。

f:id:konifar:20180210231359p:plain

サンプルアプリでは、ユーザーがFavoriteに登録したデータをcloud_firestoreに保存しています。

データ構造は、usersの中にuserIdのリストを持ち、その下にfavoritesというコレクションがあって、sessionIdのリストの中にfavoriteというキーでboolを持っています。

f:id:konifar:20180210231704p:plain

cloud_firestoreからデータをロードするコードはこんな感じです。firestoreのインスタンスからsnapshotのstreamを取得して、任意のデータを取得します。これはcloud_firestoreの仕様そのままです。

httpライブラリをつかう

f:id:konifar:20180210232409p:plain

Firebaseを使わずAPIからデータを取得する場合も難しくありません。 dartの標準ライブラリであるhttpとconvertを使います。

最近のAndroidだとRetrofitやgson、moshiなどのライブラリが必要ですが、dartでは標準ライブラリを使うだけで実装できます。

f:id:konifar:20180210232542p:plain

たとえば、サンプルアプリではセッション一覧の取得はこのAPIで取得しています。

https://droidkaigi.jp/2018/sessionize/all.json

f:id:konifar:20180210232826p:plain

コードはこれだけです。 http.read() で帰ってきたレスポンスを、convertライブラリを使ってJSON.decode()すればjsonオブジェクトになります。

モデルへのマッピング

f:id:konifar:20180210233258p:plain

そのオブジェクトをどうやってモデルに変換するかという話です。

ひとつは、jsonオブジェクトから愚直にマッピングするやり方。今回のサンプルアプリでは、all.jsonで返ってくるjsonの形がわりとつらめだったのでこのやり方でやりました。

Entityに自動でマッピングしたい場合には、Googleの提供しているbuilt_valueというライブラリを使えばできます。

Preferenceへの値保存

f:id:konifar:20180210235907p:plain

Preferenceに保存したいときは、公式のshared_preferencesを使います。Android/iOSともに動きます。

f:id:konifar:20180211000932p:plain

サンプルアプリでは、前回開いていたタブ位置の保存と復元に使っています。

SharedPreferences.getInstance()インスタンスを取得して、値を保存するときは putInt()、取得するときは getInt() を呼ぶだけです。Androidでの実装とほとんど同じですね。

Database

f:id:konifar:20180211002003p:plain

DBに保存するときはsqliteを操作するsqfliteを使います。Android/iOSともに動きます。

生のSQLの実行と、Insert、Delete、UpdateなどのHelperが用意されています。


f:id:konifar:20180211002443p:plain

ここからは、Flutterのここどうなってるの?という部分を一問一答形式でかけ足で話していきます。

Q1. CIまわせる?

f:id:konifar:20180211002832p:plain

CIまわせるか?まわせます。ただし、当然ですがFlutterを実行できる環境を整える必要があります。

f:id:konifar:20180211003005p:plain

TravisCIだとこんな感じです。 addons の中で環境を指定しています。他のCIサービスの場合には、Dockerで作ってしまった方がいいかもしれないです。

before_script の中でflutterのインストールを行い、 script の中でテストを実行しています。

Q2. Analyticsどうするの?

f:id:konifar:20180211003230p:plain

公式pluginのfirebase_analyticsを使います。

f:id:konifar:20180211003331p:plain

Flutterでは画面遷移をnavigatorという仕組みで実装します。スクリーンビューをトラッキングしたい場合には、FirebaseAnalyticsObserverを navigatorObservers にセットすればできます。

f:id:konifar:20180211003642p:plain

ログを送信したいときは、analyticsオブジェクトから log~ メソッドを呼ぶだけです。これらはFirebaseAnalyticsの仕様そのままの命名ですね。

Q3. Push Notificationはどうする?

f:id:konifar:20180211071039p:plain

Push Notificationは、firebase_messaging pluginを使います。

Q4. Animationはサポートされてる?

f:id:konifar:20180211071234p:plain

Animationはかなりサポートされています。

Animations in Flutter - Flutter

MaterialDesignガイドライン内のAnimationやTransitionくらいならわりと楽に実装できます。

ただ、先ほど少し話したように、AnimationもWidgetで実装するのでAndroidでの実装との違いに最初は少し戸惑うかもしれません。

f:id:konifar:20180211071720g:plain

どのくらいサポートされているかというと、こういうAnimationもできるんだなぁと思っておいてください。ただし、複雑なものはやはりコードもそれなりに頑張らなければいけないです。

Q5. ユニットテストどう書くの?

f:id:konifar:20180211071917p:plain

Androidjunitでのテストと似てます。mockitoとtest.dartを使って書きます。詳しくは Testing Flutter Apps - Flutter にまとまっています。

f:id:konifar:20180211072055p:plain

これはサンプルアプリでJsonからモデルにパースするメソッドのテストです。

expect() で値を確認しています。実行は、 flutter test コマンド叩くか、IntelliJの実行ボタンを押すだけです。

Q6. 多言語化どうするの?

f:id:konifar:20180211072530p:plain

多言語化するときは、自分でlanguage_codeごとのマップを作って実装するか、dart:intlを使います。

やり方は、Internationalizing Flutter Apps - Flutter に詳しく説明されていますが、Androidのstrings.xmlの仕組みと比べると言語追加後にスクリプトを流さなければならなかったりして少し面倒です。このあたりはIntelliJのpluginで今後楽にできるようになっていくのではないかと予想しています。

また、多言語化しているとWidgetのテストがコケるので、 tester.pump() を呼んでおくというワークアラウンドが必要
だったりもします。

https://github.com/flutter/flutter/issues/1865

Q7. クラッシュログの収集方法は?

f:id:konifar:20180211073002p:plain

Firebase Crash ReportingSentryをつかうように書いてあります。

github.com

Flutterは独自でレンダリングエンジンを持っていて、Activityの中ではFlutterのViewが一枚いるだけです。

Crashlyticsではdartコードの中のクラッシュは検知できないので、自分でエラーログを収集するように実装しておく必要があります。

Q8. ライブラリを探すときは?

f:id:konifar:20180211073347p:plain

何か便利なライブラリがないか探す時には、公式plugindartのライブラリ から探してみましょう。pure dartライブラリの資産も使えます。

f:id:konifar:20180211073633p:plain

公式plugin、たくさんあります。名前を見るとどんなことができるかなんとなくわかります。Firebase関係が多いですね。

f:id:konifar:20180211073807p:plain

公式pluginも重複して載っていますが、他にもFlutterで使えるdartライブラリはたくさんあります。2018年2月8日時点で70個近くありました。グラフを表示するものや、markdownの文字をレンダリングするものなど、色々揃ってきていたので、一度さっと目を通しておくとできることがわかってよいかもしれません。

まとめ

f:id:konifar:20180211074417p:plain

ここまで少し駆け足でFlutterについて触れてきました。最後にまとめます。

f:id:konifar:20180211074501p:plain

Flutterを使って開発する時のポイントは3つです。

Widgetがたくさんあるので、どんなものがあってどう使うのかを知っておきましょう。

開発効率をあげるためにIntelliJを使いこなしましょう。これはAndroid開発に慣れている皆さんなら問題ないかと思います。

何かわからないことがあれば、公式ドキュメントサンプルコードが充実しているので参考にしましょう。またSDK自体のコードを読むのもよいです。dartのコードはJavaと似ていて読みやすいと思います。

業務でつかえるのか?

f:id:konifar:20180211075512p:plain

では、"業務"でつかえるのか?という最初の話に戻ります。

f:id:konifar:20180211075548p:plain

自分でアプリを作ってリリースしてみた感想だと、つかえそうな気はしています。公式ドキュメントも充実していますし、変なエラーにめちゃくちゃハマって時間を食われまくるということもなかったです。

f:id:konifar:20180211075809p:plain

とはいえ、まだαで、最近βブランチが切られたくらいなのでFlutter自体もどうなるかはわかりません。変更もたくさん入ってくると思います。結局こういうのはやってみないとわからないのですよね。

f:id:konifar:20180211075959p:plain

何が言いたいかというと、あとは結局"覚悟"次第ということです。今から業務でつかうのであれば、「何かあっても俺が一緒にFlutterを育てる」という覚悟が必要です。

f:id:konifar:20180211080216p:plain

株式会社Kyashにて、Flutterのプロダクション導入の機会を虎視眈眈と伺っています。皆さんももし興味が湧いたなら、一緒にやっていきましょう。

f:id:konifar:20180211080334p:plain

ありがとうございました。


この発表の次の日、2018年2月10日にFlutter v0.1.0がリリースされました。

おそらくGoogle I/O 2018でもFlutterに関する発表があると思うので、リポジトリを監視しながら楽しみに待ちたいと思います。