ticktakclockの日記

技術ポエムを綴ったりします

BiometricPromptで指紋認証機能を実装する

こんにちは、tkyです。

今回はAndroidBiometricPromptを使って指紋認証機能を試してみたいと思います。

developer.android.com

こんな感じで動いてます。 コードと動作確認gifはgithubを参照してください。

github.com

f:id:ticktakclock:20190314144934p:plain:w200

BiometricPrompt

API 28から使用できる新しい認証用のフレームワークです。以前はFingerprintManagerというものでしたが、こちらは 非推奨 となりAPI28からはBiometricPromptを使用するようにとの公式からのお達しが出ております。

パーミッション

<uses-permission android:name="android.permission.USE_BIOMETRIC" />

コード

BiometricPromptのビルダーを使います。 ここにダイアログに使う情報をセットして、authenticateしますが、ダイアログの情報を先に入れないとインスタンス作れないのちょっとめんどくさい。

val cancellationSignal = CancellationSignal()
BiometricPrompt.Builder(context)
    .setTitle("生体認証")
    .setSubtitle("サブタイトルを添えて")
    .setDescription("詳細説明をここに記載します")
    .setNegativeButton("キャンセル", context.mainExecutor, DialogInterface.OnClickListener { dialog, which ->
        cancellationSignal.cancel()
    })
    .build()
    .authenticate(cancellationSignal, context.mainExecutor, object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
            super.onAuthenticationError(errorCode, errString)
        }
        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
        }

        override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
            super.onAuthenticationHelp(helpCode, helpString)
        }
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
            super.onAuthenticationSucceeded(result)
        }
    })

AndroidXのBiometricPrompt

前述でFingerprintManagerがAPI28から非推奨となりますが、P以上であればBiometricPrompt、未満であればFingerprintManagerという実装をすることになります。 BiometricPromptはsupportライブラリがないため、仮に実装の必要が出た場合、以下が必要になってくるというということです。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)

この課題を解決するために登場するのがAndroidX BiometricPromptです。 それぞれパッケージは以下のようになっています。

  • 通常のBiometricPrompt
    • android.hardware.biometrics.BiometricPrompt
  • androidxのBiometricPrompt
    • androidx.biometrics.BiometricsPrompt

コード

パーミッションは通常のBiometricPrompt同様で必要です。AndroidXにおいてはgradleに追記が必要です。

implementation 'androidx.biometric:biometric:1.0.0-alpha03'

AndroidXではダイアログを生成するBuilderと認証するためのクラスが分かれています。 BiometricPromptのコンストラクタに必要な情報を流して、authenticateする時にダイアログ情報をセットして認証させる流れです。

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("AndroidXによる生体認証")
    .setSubtitle("サブタイトルを添えて")
    .setDescription("詳細説明をここに記載します")
    .setNegativeButtonText("Negativeボタン")
    .build()
// context.mainExecutorはAPI28からなので自分でmainExecutorを作成
BiometricPrompt(activity, mainExecutor, object : BiometricPrompt.AuthenticationCallback(){
    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
        super.onAuthenticationError(errorCode, errString)
    }
    override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
    }
    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
        super.onAuthenticationSucceeded(result)
    }
}).authenticate(promptInfo)

動作確認

いろいろな端末で動作確認してみます。

XperiaZ3 (Android 5.0.2 API21)

API21でBiometricPromptを使おうとすると当たり前ですがNoClassDefFoundErrorが発生します。 githubの実装では落ちないようにcatchしてToast出すだけしています。

java.lang.NoClassDefFoundError: Failed resolution of: Landroid/hardware/biometrics/BiometricPrompt$Builder;

AndroidXのBiometricPromptであれば、ちゃんとコールできています。 が、指紋認証センサーは搭載されていないので、BiometricPrompt側で認証エラーが発生します。

HUAWEI SHT-AL09(Android8.0.0 API26)

APi26なので同様にBiometricPromptを使おうとするとNoClassDefFoundErrorとなりますが、 この子は指紋認証センサーが搭載されているので、AndroidXのBiometricPromptが動作していることが確認できました。

Pixel 3 (Android 9.0.0 API28)

言わずもがな、どちらも動作できました。

最後に

指紋認証を試してみました。今の所「その端末がユーザのものであることを確認する」以上の使い方が思いつきませんでした・・・ この流れでWebAuthnとかFIDO2とか踏み込むかどうか・・・

また、AndroidX中の実装を気になって見てみたら、バージョンで切り分けてBiometricPromptとFingerprintManagerをゴリゴリしてました😅僕らでバージョン分岐しなくて良くなったのは事実です!

参考

Fingerprint Authentication using BiometricPrompt Compat

DroidKaigi2019 2日目に行ってみた感想とか

こんにちは、tkyです。

2日目も行ってきました。1日目と同じようにセッション参加したtkyの個人的感想を述べる内容となっております。

droidkaigi.jp

1日目のレポートはこちらをご覧ください

ticktakclock.hatenablog.com

この日見た講演はなんと8講演。疲れた・・・けどとても濃い1日でした

  • Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上(10:30~)
  • Wi-Fi RTTによる屋内測位アプリを作ろう(11:20~)
  • All About Test of Flutter(12:50~)
  • Lifecycle, LiveData, ViewModels - The inner wiring(14:50~)
  • multi-module Androidアプリケーション(15:40~)
  • Navigation Architecture Component によるアプリ内遷移の管理(16:50~)
  • Android Thingsでのプロダクト開発(17:40~)
  • BLEアプリ設計パターン(18:30~)

最初は朝ごはん

会社に行くより早く電車に乗り、9:40くらいに会場到着。サンドイッチやコーヒー、紙パックジュースなどを堪能できました。

エキシビジョンルームにて朝食が配布されています。 f:id:ticktakclock:20190209140100j:plain

Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上

speakerdeck.com

www.youtube.com

Dialogflowはgooglehomeアプリを作成する際にも使われますので、興味アリアリです。

googlehomeのアプリをサンプルで作った時にDialogFlowを使ったことがあるのですが、Androidアプリからも使ってみたいですね!聞くところによるとv1のsdkは2019.9にDeplicatedになるので要注意だそうです。v2との使い勝手はどう異なるのだろうか。

DialogFlowについては本ブログでも別途触れてみたいなと思います。

やっぱり、固有名詞を検出するにはEntityを設定しまくるしかないのですね。音声認識の揺れを吸収するためのアノテーション作業などはやったことないので後ほど見てみたいと思います。

Wi-Fi RTTによる屋内測位アプリを作ろう

speakerdeck.com

www.youtube.com

WiFi RTT(Round Trip Time IEEE 802.11mc)全然知らないから気になってました。

基本的にFusedLocationを使って位置情報を取得します。これを使うことでGooglePlayServicesを経由して位置情報を取得するようにできます。 WifiRTTは屋内でも使用できる位置測位用の規格であるため、アクセスポイントを4つ使うと精度は1mとかなり良くなる状況だそうです。

アクセスポイントからの距離はわかるが、アクセスポイントがどこにあるかは知っておく必要がある、なるほど 👀 測位方法はピタゴラスの定理連立方程式を組み合わせて実現するようです。

少なくとも3点測位ポイントがあり、その3点のポイントを線で結んでできる空間内にいる必要があることが測位方法から見ても理解できるかと思われます。

API28からscanのAPIが色々変更担っているようでDeplicatedにもなっている模様

精度を出すための条件がなかなか厳しそうですね。アクセスポイントの設置場所や地図の選定など、やることは色々ありそうです。 他のセンサーも併用して使うとより精度の向上が図れそうと思いました。

ここでお昼ご飯

1日目のお弁当を撮影しそこねましたが、2日目はちゃんと撮影できました、この後のFlutterのセッションルームにて着席して昼食休憩です。

f:id:ticktakclock:20190209140452j:plain

All About Test of Flutter

docs.google.com

www.youtube.com

you brideという婚活支援サービスがFlutterで作成されているようです。

今回のgithubも要チェケです。

github.com

自分のコードに自身を持って確かなものにするためにテストを書く。いい話だ。

名言も飛び出ましたw

人間は超優秀全自動アサーション関数

自動テストにするか、人力テストどちらを取るかは、継続してコスト回収できるかどうかで判断すると良さそうです。

めちゃめちゃ良い発表スライドで、終わった後もガッツリ見てます!

質問時間にて、未実装部分についてはスキップ関数にするのか、Failさせるのが良いのか議論が最後に行われていました。 単純にFailした場合と未実装なのでFailなのかがパット見わからないので、メンバーのテストリテラシ状況に合わせて選択するのが良いのでは、という結論にいたりました。

また、テストレポートについては現状HTMLなどで見れるようなものはこのとき回答はありませんでした。コマンドライン上でもギリ見やすそう・・・?

小休憩

ちょっとだけ疲れたので小休憩です。

Clip Roomさんのブースでデザインガイドラインの一部を見せていただいたりしました。やっぱりデザイナーさんとエンジニアがコミュニケーションとれる状況良いですね。

そしてまたコーヒー・・・美味しいんですよね〜 f:id:ticktakclock:20190209140654j:plain

Lifecycle, LiveData, ViewModels - The inner wiring

www.youtube.com

architecture componentの講演となります。英語のセッションですが、同時翻訳なので安心?して聞けます。 Architecture ComponentのLifecycleコンポーネントなどなどの解説となります。

LiveDataはデータホルダーであり、streamが終わるという考えはなく、ライフサイクルで使われるため、本質が異なるということですね。 しばしばLiveData vs RxJavaというように比較される事が多いのですが、そもそも本質が異なるためにLiveDataとRxJavaがともに存在する、という認識が出てきました。

翻訳が非常に聞き取りやすく個人的に理解が進みました。

multi-module Androidアプリケーション

https://speakerdeck.com/sansanbuildersbox/multi-module-android-application

www.youtube.com

なんとかマルチモジュールの講演入れました!

SanSanのEightではモジュール40個で構成されている、すごい分割数だ・・・

モジュールに分けることでプロジェクト内のコンパイル単位を分割できたり依存管理できるのか良いところですね。

  • ビルドが高速化できる
  • コードの依存関係を強制できる
  • モジュールごとにテスト実行できる
  • Kotlinのinternal修飾子でモジュール内で可視性を定義できる
  • Dynamic Feature Moduleでインストール時の容量を下げる

色々メリットがありますね!

gradle plugin 3.0になってからのimplementation指定と過去のcompile指定の挙動の違いについてちゃんと理解していなかったのですが、解説を聞いてスッキリしました。

gradleのビルド結果は--scanオプションで見れる!へぇ! incrimental buildの場合だと並列マルチモジュールだとビルドが早いですね。Annotation processingの実行をコストが抑えられることがポイントのようです。

realmを使用したマルチモジュール化の高速化例も紹介いただいて理解が進みました。チーム全体のマルチモジュールについての理解の底上げも重要ですね〜。

どうマルチモジュール化したら良いのか

モジュール分けする際にレイヤーで分けるのか機能で分けるのか、結構迷うのですが、

app → data  → domain
  → ui       → domain

という構成でまとめると良さそう!

機能ごとに分けるときも画面遷移のIntentをinterfaceとしてapp側で持ってあげて各画面にDIすることで依存関係をフラットにすることができるので、1対多(画面)の構成が作れそうだと思いました。

eightでは機能ごとにモジュールを分けているみたいですね。

すごくためになるセッションでした!

Navigation Architecture Component によるアプリ内遷移の管理

speakerdeck.com

www.youtube.com

Architecture Componentに関する講演です。

画面遷移時の課題を解決できるようなライブラリとツール群のことですね。

Navigation Architectureにおいてはどの画面がスタート地点になる画面なのかをガイドラインとして定義されているようです。 deep linkについても「同じ画面にいるなら、同じ画面スタックが形成されているべき」というような原則が存在しています。

navigation を定義するxml内でfragmentを定義し、fragment layoutの中にapp:navandroid:nameの記述を書くことでNavigationの遷移を実現できるみたいです。

あとはJavaとKotlinでSafeArgsの指定の仕方が異なるようですね。JavaはBuilderパターン、Kotlinはnamed argumentで指定、という具合です。

最近のAndroidの開発に触れていないので、結構新鮮でした。

Android Thingsでのプロダクト開発

DroidKaigi2019 AndroidThingsでのプロダクト開発 - Google スライド

www.youtube.com

Android Thingsはgoogleが提供するIoT向けプラットフォームのことです。 Androidなので既存のシステムを利用できるところは強いですね。ハードウェアレイヤもGoogleがサポートしているようなのでこういったところも強みになりそうです。

最初はmuiの製品紹介から入ります。スマートディスプレイの代替としての製品のようです。 muiプロダクトデモを実際に見せていただいて実感がわきます・・・!!!

mui.jp

AndroidThingsとAOSP,Linuxとの違いについては、カーネルのカスタマイズができるかどうか、SoCの選択肢があるかどうか、OTAができるかどうか、が選定のポイントとなりそうです。

メリットは以下。

  • Peripheral I/O APIがある(I2CやUARTのラッパーAPIがある)
  • OTAできる
  • AndroidStudio使える
  • Android Frameworkも使える

一般のデベロッパーがさわれる領域にPeripherarl I/O APIの入出力をフレームワークに統合するためのUserDriver(Input Driver)というものが結構微妙(画面描画あたり、座標空間周り)らしいですね。UserDriver(InputDriver)とはI2CのIOをボタン押下イベントに変換するようなものらしいです。

基本カーネルフレームワークGoogleが完全に制御しているので一般デベロッパーは触ることができず。 先程まで強みだと思っていた点が回り回ってデメリットになってきました・・・カーネルをカスタマイズできない点については、ラズベリーパイで発生するclock stretching問題において重要な欠点のようです。というのもこの問題を暫定で解決するためにI2Cのボーレートを変更する必要があるそうですが、OTAでファームウェアを流し込んだ時にその編集部分のコードが上書きで消えてしまうそうです。事実上OTAできないじゃん!ということになるわけですね・・・闇が見えてきました

新規でThingsのプロダクト化が難しい状況で有ることがわかりました・・・w セッションが悲しい雰囲気に包まれてしょんぼりだったのですが、トライ&エラーの結果を共有していただけた非常にためになるセッションでした!!

BLEアプリ設計パターン

speakerdeck.com

www.youtube.com

BLEのつらみについては触れず、設計ノウハウにフォーカスしたお話です。

BLEはHTTPSのようなプロトコルとどう違うのか、実際にどのように設計しているのかをノウハウ共有いただけるセッションでした。Qrio Lockはいわゆるスマートロックで、BLE通信を使って鍵の開け締めを行っているようです。

通信という意味ではWifiという選択肢もありますが、省電力や近接検知ができるという観点で、BLEの選択肢が出てくるということですね。

Webの世界はRetrofitのようなライブラリを使うと非常に楽になったしますが、BLEは20バイト制限があるので、 20バイト以上のデータを扱うときはパケットを分割して受け取った側でくっつけるような、HTTPのプロトコルが担保する領域を自分で担保する必要があるようです。なるほど・・・

BLEが仕様で搭載しているセキュリティではできない自前でセキュリティを担保する必要も出てくるとのこと。なかなかつらみが出てきましたね!!

「ロック(スマートロック機器)の名前」は情報をデバイスで持つべきか、サーバで持つべきか、という観点も非常に興味深いです。

→結論的にはwebサーバ側で情報を持って、デバイス側は極力シンプルな構成にすることが良さそうです。

登場人物がスマートロックデバイススマホ、webサーバが存在しており、この3者がうまく同期している必要があるのもなかなか難しい課題です。 この際の通信の整合性を担保するためにべき等なAPIにする必要があることも重要ですね。

実際にアプリはMVVMで作成されているようですが、テストに関するメリットは高く、 しかしながら責務分けに関するベストプラクティスを探すようなコストも高くなってしまうこともあるとのことです。

さいごに

2日間有休を使って参加しましたが、最高に楽しかったです。こういうの参加するとやる気出ますね。来年も行きたい。。。行けるようにがんばります。 色々と新しい単語もピックアップできたのでこれからも継続して勉強していきましょー

いただいたものたち f:id:ticktakclock:20190209141110j:plain

DroidKaigi2019 1日目に行ってみた感想とか

こんにちは、tkyです。

DroidKaigi2019に行ってきました。1日目のレポです。

droidkaigi.jp

2日目のレポはこちらをご覧ください。

ticktakclock.hatenablog.com

行ってきた講演は以下です。お昼後、yanzmさんの講演見たかったのですが、満席御礼で入れず・・・

  • マテリアルデザインの起源とベースとなる哲学(13:50~)
  • 辛いと評判のAndroid BLEを頑張って使い続けた話(14:00~)
  • Chrome Custom Tabsの仕組みから学ぶプロセス間通信(14:50~)
  • Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発(15:40~)
  • Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題(17:10~)

お昼に到着

いざ! f:id:ticktakclock:20190209134738j:plain

yanzmさんの「LiveData と Coroutines で実装する DDD の戦術的設計」を見たかったのですが、なんと満員!さすがというべきか入れませんでした・・・あとで配信見ます!

会場の様子です。

f:id:ticktakclock:20190209130852j:plain

そして急遽見たかったもう一つの講演に向かいました。

マテリアルデザインの起源とベースとなる哲学

speakerdeck.com

www.youtube.com

ちょうどマテリアルデザインを取り入れたUIを作っているプロダクトの最中だったので、興味アリアリの内容です。

そもそも誰がマテリアルデザインを作ったのかさえ知らなかったので恐縮ですが、すごい面白い講演でした。

そもそも誰が作ったのか・・・・ googleのデザイン責任者であるマティアス・デュアルテさんが作ったそうですね。昔のですが記事が出てました。

https://www.gizmodo.jp/2014/07/_android.html

この方の歴史的背景や知識を見ることでマテリアルデザインの思想と哲学を見ていく、という内容でした。

マテリアルデザインについては私は根本的なところで理解がかけていたようで、本講演を受けて改めてマテリアルデザインガイドを査読していきたいです。

辛いと評判のAndroid BLEを頑張って使い続けた話

www.slideshare.net

www.youtube.com

BLEつらいねーーー!!!!wBLEを多少触ったことがある身分としては皆さん同じようなところにつらみを感じているらしく、共感でしたww

BLEは接続のためには数ステップやることがありますが、AndroidではそれらすべてのAPIが非同期のコールバックとして返ってきます。今であればRxやKotlinコルーチンを使うことで被害を最小限に抑えられそうです。

突然切断される現象やサービス接続からいきなり通信するとエラーになる現象などはタイマーやリトライ処理をしてあげることでできる限り救う。

つらみのなかでやりようを見出していくそのスタイルに共感です。

Android バージョン x BTチップ x BTプロトコル・スタック x 独自省電力 x デバイス側のチップ相性 =可能性無限大ですね!!!!😇

RxAndrodiBleというライブラリでBLEの処理をRxで書くことができるのですが、昔なかなかRx2に対応されなくてRx2にラップする処理を自前で書いていたりしました。。 しかし今はRx2対応されているようなのでまた見てみても良いかもですね!

Chrome Custom Tabsの仕組みから学ぶプロセス間通信

www.slideshare.net

www.youtube.com

Chrome Custom Tabsの仕組みにおいては別にGoogleChromeだけが保つ機能ではなく、CustomTabsと呼ばれるサービスに対応したアプリが提供できる機能となっているようです。

こういった仕組みを提供、実現するためにはAIDLという他のアプリケーション(サービス、=別プロセス)とやり取りするためのインターフェースを定義・実装することで実現できます。

実装面でいうとCustomTabsServiceっていうAbstractクラスがあって、それを継承したクラスを作ればとりあえずCustomTabsを提供するサービスが作れるみたいです、こう聞くと少しハードルが下がる・・・?😅

最近モダンな開発にフォーカスが当たる中、Androidフレームワークに関する機能にふれる機会が減ってきていたので非常に勉強になる講演でした。

Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発

speakerdeck.com

www.youtube.com

リリースされて間もない機能なので要チェケです。

  • どのようにコルーチンを使うか、学ぶか
  • どのようにコルーチンを適用するか
  • コルーチンをいつ使うか

という点にフォーカスを当てた講演でした。

Concurrency and Parallels(平行性と並列性)というワードが印象に残っています。 コルーチンスコープについては基本的にアプリ内のライフサイクルに基づいてスコープを定義して動作させるのが良さそうです。

また、ほかアーキテクチャへの適用として重要になる考え方としてモジュール間の関心に着目するという点です。 データフローが統一するならRx、独立性の高い開発モデルを主眼とするならモジュール間でコルーチンを適用することを検討しても良さそうです。

このあたり私も初見の類に入るので自分の中でしっかりと理解して行きたいです。

小休憩

少し疲れたので休憩がてらにコーヒーを飲みに来ました。 このアルファベッティカフェさんがDroidKaigiに出張に来てくださり、プロのバリスタさんによるコーヒーを飲むことができてよかったです。普通にお店に行きたい・・・

f:id:ticktakclock:20190209135302j:plain

yahoo japanブースにてTRILLグッズを頂いたり、その他様々なブースを覗いたりしました。

f:id:ticktakclock:20190209135135j:plain

Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題

speakerdeck.com

www.youtube.com

本日ラスト講演です。

パスワードについての現状の課題、コストを考え人間が適用できる認証方式を考えた結果のWeb Authentication APIという1つの選択肢のようですね。

正直なところ私が無知すぎて単語を追うのが精一杯でした・・・ 😇

パスワードの方式として「忘れた、パスワード再設定フローもない」はいわゆる詰んだ状態になりますが、web authnの仕組み(要はデバイスを使った指紋認証と組み合わせたもの)だと「スマホをなくした/壊れた」ら詰んだ状態になる、ということなので、詰んだ状態からどのように回避するかが課題になりそうです。

アフターパーティー

いよいよアフターパーティです 🕺

f:id:ticktakclock:20190209133619j:plain

f:id:ticktakclock:20190209134334j:plain

なお写真はpixel3で撮影したものです。ポートレートめっちゃよく撮れる😎

お食事も美味しかったし、登壇者のかたと会話できましたし良い体験ができました。

終わりに

2日目は朝ご飯食べに早めに行こうと思います!引き続き2日目のレポートもどうぞ!

頂いたものたち f:id:ticktakclock:20190209134402j:plain

Flutter Androidのソースコードを読む(FlutterActivity編)

こんにちは、tkyです。

前回FlutterApplication.javaのコードを読んで、Dartを動作させるための初期設定を査読しました。

今回はFlutterActivity.javaのコードを読みながら、アプリ起動するまでを追ってみようと思います。 調べた軌跡を残しているので今回もそれなりに長い記事となっております。

前回も書きましたが・・・今回読んでいくのはこれ

flutter.jar

Android Studioにはjarのclassファイルに定義ジャンプすることができるので⌘ + クリック(winの場合は ctrl + クリック)で読めますが、 ここで読めるのはあくまでjavaで書かれた領域だけなのでネイティブコード(C/C++の領域のことを指します)も見ることを考慮して flutter/engineもcloneしておきます。

github.com

目次

  • FlutterActivity.javaを探す

FlutterActivity

以下にそれぞれあります。engine側のコードがビルドされてflutter.jarになります。

  • engine側
    • engine/shell/platform/android/io/flutter/app/FlutterActivity.java
  • flutter.jar側
    • io/flutter/app/FlutterActivity.java

コンストラクタを見てみましょう。FlutterActivityDelegateにほぼ集約されてそうな感じです。

public class FlutterActivity extends Activity implements Provider, PluginRegistry, ViewFactory {
    private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
    private final FlutterActivityEvents eventDelegate;
    private final Provider viewProvider;
    private final PluginRegistry pluginRegistry;

    public FlutterActivity() {
        this.eventDelegate = this.delegate;
        this.viewProvider = this.delegate;
        this.pluginRegistry = this.delegate;
    }

FlutterActivityDelegateとは

名前が名前だけあって、Activityがやる処理の委譲クラスです。 ここでは大体以下のことをしているようでした。

  • flutterViewの保持
  • ライフサイクルメソッド(onCreate, onStart, onResume等)の処理
  • Intentの処理

ライフサイクルに従って適切にViewに状態を伝える役割をしているようですね。

onCreateで何をしているのか

FlutterMain.ensureInitializationComplete()、flutterViewの生成とIntentの処理をしていました。ここではflutterViewにフォーカスを当てて見てみます。

ensureInitializationComplete

初期化完了させるための確定処理、という意味で捉えます。

FlutterApplicationの処理にて初期化した、shared ibraryのpathやvm snapshot, isolateのpathを使い、vm shellの起動パラメータの生成処理を行います。 その後、nativeInit()を呼び出しています。

このインターフェースの定義はどこにあるのかというと以下にあります。

engine/shell/platform/android/flutter_main.cc
{
    .name = "nativeInit",
    .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/"
           "lang/String;Ljava/lang/String;Ljava/lang/String;)V",
    .fnPtr = reinterpret_cast<void*>(&Init),
},

flutter_main.ccはJNIのインターフェース定義を行っています。 flutter_main.ccはSystem.loadLibrary("flutter");にてlibflutter.soがロードされたときに、 engine/shell/platform/android/library_loader.ccのonLoad内でflutter_main.ccが実行されます。 ※要するに、JNIの定義設定処理はFlutterApplication.javaのonCreate処理で行われているということになります。

nativeInitの実際の処理はdartのキャッシュに関する処理をしているようですが、今の私の理解を超えていたので一旦おいておきます。。

flutterViewの生成

ViewFactoryはFlutterActivityに実装されていますが、デフォルト実装でnullが返るようになっているのでonCreateで必ずFlutterViewを生成することになりそうです。

this.flutterView = this.viewFactory.createFlutterView(this.activity);
if (this.flutterView == null) {
    FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
    this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
    this.flutterView.setLayoutParams(matchParent);
    this.activity.setContentView(this.flutterView);
    this.launchView = this.createLaunchView();
    if (this.launchView != null) {
        this.addLaunchView();
    }
}

viewFactory.createFlutterNativeView

FlutterViewはSurfaceViewを継承しているViewであることはわかりますね。flutterで作られたアプリはこのsurfaceView上に描画されている、という理解です。

ではFlutterNativeViewは何なのか?実装を見るとJNI側に処理を渡すためのクラスになっているようです。

FlutterNativeViewにはnative修飾子のメソッドがいくつか定義されています。このメソッドの実装はC++側の実装となり、flutter.jarだと此処から先は確認することができません。

この先はengine側のソースコードを見ることになりますが、後ほど見ていくこととします。

あとのFlutterViewの処理は標準のプラグインの初期化処理らしき実装があるのがわかります。 この辺は自身でプラグインを作成したらわかりそうな気がする・・・!!ので機会を見てプラグイン作成にもチャレンジしてみたいと思います。

this.mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
this.mFlutterNavigationChannel = new MethodChannel(this, "flutter/navigation", JSONMethodCodec.INSTANCE);
this.mFlutterKeyEventChannel = new BasicMessageChannel(this, "flutter/keyevent", JSONMessageCodec.INSTANCE);
this.mFlutterLifecycleChannel = new BasicMessageChannel(this, "flutter/lifecycle", StringCodec.INSTANCE);
this.mFlutterSystemChannel = new BasicMessageChannel(this, "flutter/system", JSONMessageCodec.INSTANCE);
this.mFlutterSettingsChannel = new BasicMessageChannel(this, "flutter/settings", JSONMessageCodec.INSTANCE);
PlatformPlugin platformPlugin = new PlatformPlugin(activity);
MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);

そしてこのflutterViewをactivityでsetContentViewしてsurfaceViewが全面に表示されるようになります。

surfaceCallback

ではここからsurfaceCallbackの処理を追っかけてみたいと思います。いきなりネイティブ インターフェースがが出てきましたね。

this.mSurfaceCallback = new Callback() {
    public void surfaceCreated(SurfaceHolder holder) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceCreated(FlutterView.this.mNativeView.get(), holder.getSurface());
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceChanged(FlutterView.this.mNativeView.get(), width, height);
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        FlutterView.this.assertAttached();
        FlutterView.nativeSurfaceDestroyed(FlutterView.this.mNativeView.get());
    }
};

nativeSurfaceCreatedの実装はどこにあるのか

FlutterViewに直接定義があったのですが、engine側では以下にあることがわかりました。

engine/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

これはengineのコードとflutter.jarのコードは若干異なることをしめしていますが、もともとflutter.jarは逆コンパイルしているため、最適化されているのだろうと想定しています。

// engine/shell/platform/android/platform_view_android_jni.cc
{
  .name = "nativeSurfaceCreated",
  .signature = "(JLandroid/view/Surface;)V",
  .fnPtr = reinterpret_cast<void*>(&shell::SurfaceCreated),
},

PlatformView.SurfaceCreated

Androidにはplatform_view_androidiosにはplatform_view_iosがそれぞれplatform_viewを継承している構成になっています。

// engine/shell/platform/android/platform_view_android.cc
void PlatformViewAndroid::NotifyCreated(
    fml::RefPtr<AndroidNativeWindow> native_window) {
  if (android_surface_) {
    InstallFirstFrameCallback();
    android_surface_->SetNativeWindow(native_window);
  }
  PlatformView::NotifyCreated();
}
// engine/shell/common/platform_view.cc
void PlatformView::NotifyCreated() {
  delegate_.OnPlatformViewCreated(CreateRenderingSurface());
}
// engine/shell/common/shell.cc   
void Shell::OnPlatformViewCreated(std::unique_ptr<Surface> surface) {
・・・省略・・・
    if (rasterizer) {
      rasterizer->Setup(std::move(surface));
    }
・・・省略・・・
}

このRasterrizerというクラスでなにかしていそうです。これのincludeを見てみるとskiaで描画している雰囲気がバンバン出ています。

#include "third_party/skia/include/core/SkEncodedImageFormat.h"
#include "third_party/skia/include/core/SkImageEncoder.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
#include "third_party/skia/include/core/SkSerialProcs.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/core/SkSurfaceCharacterization.h"
#include "third_party/skia/include/utils/SkBase64.h"
・・・省略・・・
// engine/shell/common/rasterizer.cc
void Rasterizer::Setup(std::unique_ptr<Surface> surface) {
  surface_ = std::move(surface);
  compositor_context_->OnGrContextCreated();
}
・・・省略・・・
void Rasterizer::DoDraw(std::unique_ptr<flow::LayerTree> layer_tree) {
・・・省略・・・
bool Rasterizer::DrawToSurface(flow::LayerTree& layer_tree) {
・・・省略・・・

nativeSurfaceChangedを見てみる

nativeSurfaceChangedも同様に処理を追っていきます。先程のようにJNIまで探り当てて、platform_view_android側の処理を見つけ出します。

// engine/shell/platform/android/platform_view_android_jni.cc
static void SurfaceChanged(JNIEnv* env,
    jobject jcaller,
    jlong shell_holder,
    jint width,
    jint height) {
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyChanged(
      SkISize::Make(width, height));
}
// engine/shell/platform/android/platform_view_android.cc
void PlatformViewAndroid::NotifyChanged(const SkISize& size) {
  if (!android_surface_) {
    return;
  }
  fml::AutoResetWaitableEvent latch;
  fml::TaskRunner::RunNowOrPostTask(
      task_runners_.GetGPUTaskRunner(),  //
      [&latch, surface = android_surface_.get(), size]() {
        surface->OnScreenSurfaceResize(size);
        latch.Signal();
      });
  latch.Wait();
}

調べていくとsurface_glにたどり着いてしまったのですが、ライフサイクル的にはsurfaceが変更されたときのイベントなのでネイティブ的にはsurfaceの初期化などをしていそうですね。

// engine/shell/platform/android/android_surface_gl.cc
bool AndroidSurfaceGL::OnScreenSurfaceResize(const SkISize& size) const {
  FML_DCHECK(onscreen_context_ && onscreen_context_->IsValid());
  return onscreen_context_->Resize(size);
}

nativeSurfaceDestroyed

こちらも調べていきます。おそらくラスタライザのteardownにつながってるんじゃないかなぁ、という推測をしながら調べてみます。

// engine/shell/platform/android/platform_view_android_jni.cc
static void SurfaceDestroyed(JNIEnv* env, jobject jcaller, jlong shell_holder) {
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyDestroyed();
}
// engine/shell/common/platform_view.cc
void PlatformView::NotifyDestroyed() {
  delegate_.OnPlatformViewDestroyed();
}
// engine/shell/common/rasterizer.cc

void Shell::OnPlatformViewDestroyed() {
・・・省略・・・
    if (rasterizer) {
      rasterizer->Teardown();
    }
・・・省略・・・
}

ビンゴですね〜!

おわりに

少しflutter androidがどんな感じで描画しようとしているのか雰囲気がわかったような、わからないような・・・ もう少しsurfacecreatedから実際にdrawが実行されるまでの処理を追いかけて順次記事更新していこうと思います。

(プログラミング不要)Google Home + SwitchBotでエアコンとお風呂のスイッチを入れる

こんにちは、tkyです。

プログラミング要素なしです。Google Home経由でエアコンの操作とお風呂の追い焚きスイッチを入れてみようと思います。 ちなみにGoogle HomeがなくてもSwitchBot Hub PlusとSwitchBotがあれば家の外からスマホ経由でエアコンのスイッチ入れられます。

何作ったの

これができるようにしました。

シナリオ1:
tky「OKグーグル、エアコンつけて」
googlehome「はい、エアコンをONにします」
シナリオ2:
tky「OKグーグル、お風呂つけて」(追い焚きの意味)
googlehome「はい、お風呂をONにします」
サブシナリオ:帰宅途中
tky「(家は寒い/暑いからなぁ・・・)エアコンと、追い焚きをしておこう(ポチポチ」
〜帰宅〜
tky「家あったけ/涼しーー!!!すぐ風呂入れるーーー!!!」

用意するもの

サブシナリオだけであればGoogle Homeは不要です。

Google Home

音声でスイッチを入れるために使用します。厳密に言うとSwitchBot Hub Plusにアクセスするために使用します。

音声操作が不要であれば、「Google Assistantアプリ」または「SwitchBotアプリ」があれば大丈夫です。

SwitchBot Hub Plus

色々できることはあるのですが、以下のことをするために使用します。 - 外から(インターネット経由で)アクセスする - 赤外線コードを発信する(今回の場合エアコンのリモコン) - SwitchBotを操作する

SwitchBot

この子には我が家の追い焚きスイッチを押してもらいます。 SwitchBotと専用アプリはBLE通信で行うため、外に出ているとアプリから直接SwitchBotが操作できません。 そのため、SwitchBot Hub Plusを介して制御することになります。

!!!!注意!!!!

本品を購入する前に本記事のSwitchBotの取り付け例(画像)を見てください。 スイッチの形状や家の構造によって取り付けられないケースもあります。

各アプリはインストールしておきましょう

play.google.com

Google Home

Google Home

  • Google LLC
  • ライフスタイル
  • 無料

  • SwitchBot

play.google.com

SwitchBot

SwitchBot

  • wonderlabs, Incorporated
  • ライフスタイル
  • 無料

エアコン操作

まず、SwitchBot Hub Plusでエアコンの操作からしていきます。 SwitchBot Hub Plusを開封して、電源をつなぎリモコン操作できそうな位置に配置します。

SwitchBot側

続いて、アプリ側です。アプリを開くとすでにデバイスが見つかっている状態になっているかと思われます。 この辺のつなげ方は取扱説明書見たほうが早いです。 『Hub Plus D0』と書かれているところの丸アイコン部分をタップします。

f:id:ticktakclock:20190113185928p:plain:w200

「新しいデバイスを追加する」をタップします。

f:id:ticktakclock:20190113190135p:plain:w200

エアコンを使いたいのでエアコンを選びます。

f:id:ticktakclock:20190113190148p:plain:w200

SwitchBot Hub Plusに向かってエアコンのリモコンの暖房ボタンでも押しましょう。

f:id:ticktakclock:20190113190332p:plain:w200

f:id:ticktakclock:20190113194750p:plain:w200

1つリモコンボタンを送信すると、エアコンがONになるかと思われます。

f:id:ticktakclock:20190113190346p:plain:w200

アプリからリモコン操作してみます。この時点でエアコンがONにならない場合、正しく設定できていないか、非対応(さすがに少ないでしょうが・・・)な可能性が出てきます。

単純にアプリから操作するだけで良い場合は、この状態で家の外からエアコンONできますのでここで完結です。追い焚き操作に移りましょう。

GoogleHomeから操作したい場合は次の手順に進んでください。

Google Home

Google Homeアプリを開くとはじめにGoogle Homeバイスとの接続を行う必要があります。

こちらは画面のUIに沿ってボタンを押していくだけでセットアップが完了しますので、テンポよく設定していきます。

続いて、SwitchBotとリンクしていきます。設定からデバイスを追加(プラスマーク)、を選びます。

f:id:ticktakclock:20190113191532p:plain:w200 f:id:ticktakclock:20190113191911p:plain:w200 f:id:ticktakclock:20190113191926p:plain:w200

サービス一覧から『SwitchBot』を探し出します。検索するとすぐに見つかります。

f:id:ticktakclock:20190113192049p:plain:w200

ログインを求められる場合はログインしましょう。

f:id:ticktakclock:20190113192117p:plain:w200

そうすると、今SwitchBotで使用できるデバイス一覧が表示され、どの機器をGoogleHomeとリンクするか尋ねてきます。まぁSwitchBot Hubしかないので1つ選びましょう。

f:id:ticktakclock:20190113192148p:plain:w200

ここで設定する部屋の名前は「リビング」、デバイス名を「エアコン」としましょう。 これで完了です。 tky「OKグーグル、エアコンつけて」 Google Home「はい、リビングエアコンをONにします」 が実現できるようになります。

お風呂の追い焚き操作

SwitchBot側

いよいよ追い焚きを操作してみたいと思います。こちらはSwitchBotデバイスを使用します。

まず、デバイスの電源を入れましょう。その後、SwitchBotアプリで見てみます。

いましたね。画面に表示されているデバイスのアイコン部分を押してみましょう。 デバイスがウィーンと動きましたか?動作確認OKです。 また、Google Homeから操作を受け付けるために、「Hub Plus D0」=>「SwitchBot」=>「お風呂(デバイス名です)」からGoogle Homeからのアクセス許可をつけておきましょう。

f:id:ticktakclock:20190113193639p:plain:w200 f:id:ticktakclock:20190113193654p:plain:w200

これをこのように取り付けます。裏はシールになっているため、貼り付けられるだけの面積が必要です。 3.5cm x 4.5cmくらいの領域があれば十分に貼り付けられるでしょう。取り付け例です。

f:id:ticktakclock:20190113194714p:plain:w200

Google Home

すでに、SwitchBotサービスをリンクしているので使えるのかな?と思いきや実はリンクした時点のデバイスだけが使えるようで、その後追加したデバイスを使用できるようにするには、一度リンク削除して、再度リンクする必要があるみたいです。(微妙・・・) 仕方なし、前回同様、一度リンクを削除して、再度サービス一覧から「SwitchBot」を選び直します。 そうすると、今SwitchBotで使用できるデバイス一覧が表示されます。SwitchBot Hub PlusとSwitchBot両方あることが確認できます。 もちろん両方選択します。

追い焚きデバイスは「お風呂」と命名しました。これで完了です。

tky「OKグーグル、お風呂つけて」 Google Home「はい、お風呂をONにします(ウィ〜ン ポチ」 が実現できるようになります。

快適に利用するために

これはSwitchBotアプリだけでしか使えないのですが、スケジュール機能があります。 簡単に言うと毎朝7:00にエアコンをONにしたり、毎晩19:00に追い焚きをONにしたり、そういうことができるようになります。 控えめに言って最高だと思います。

おわりに

いかがでしたか?プログラミング要素なくても案外家電操作できる良い時代になりましたね。 リモコンについては汎用性が高く、TV、扇風機、LED照明、(そこまでしなくても・・・wという感はありますが)色々と楽しめそうです!

Flutter Androidのソースコードを読む(FlutterApplication編)

こんにちは、tkyです。

今日はFlutter Androidソースコードを読んで、調べた軌跡を残したいと思います。

本記事は確定的は情報はなく、tkyが右往左往しながら調べたことと感じたことをまとめたもので、仕組みを解説するような内容ではないことを予めここに明記致します。

また、リアルタイムで勉強しながら記事を書いているので誤りがある場合、随時訂正していきます。

(というか正直難しい・・・いろいろ読み進めてみて、dartdart VMのことを知らないと理解できないのではないかと感じました。)

今回読んでいくのはこれ

flutter.jar

Android Studioにはjarのclassファイルに定義ジャンプすることができるので⌘ + クリック(winの場合は ctrl + クリック)で読めますが、 ここで読めるのはあくまでjavaで書かれた領域だけなのでネイティブコード(C/C++の領域のことを指します)も見ることを考慮して flutter/engineもcloneしておきます。

github.com

目次

  • まずはAndroidManifest.xmlから
  • そもそもflutter.jarはどこにあるのか?
  • FlutterApplication
  • FlutterMain
    • startInitialization
      • VM snapshotとは
      • instrって何
      • isolateって何
      • app.soとは
      • app.flxって何

FlutterActivityについては別途調べて見ようと思います。

まずはAndroidManifest.xmlから

とにもかくにもこれから見るのが最初の足がかりとしては良いかと思います。 どうやらFlutterApplicationMainActivityによって構成されていることがわかりました。 MainActivityFlutterActivityを継承しているようですね。

AndroidManifest.xml抜粋

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="kasikari_memo"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

MainActivity.kt抜粋

class MainActivity: FlutterActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
  }
}

ここで新しいワードが3つ出てきています。上から順番に調べていきますが、本記事ではFlutterApplicationだけフォーカスします。 - FlutterApplication - FlutterActivity - GeneratedPluginRegistrant

このクラス郡がflutter.jar内に格納されている、ということですね。

そもそもflutter.jarはどこにあるのか?

調べたところ、こちらにあるようでした。android-armの部分はアーキテクチャによってパスが変わりそうです。

Androidの場合:
/Users/${username}/flutter/bin/cache/artifacts/engine/android-arm

iOSの場合:
/Users/t.takenaka/flutter/bin/cache/artifacts/engine/ios

試しにJD-GUIツール(jarファイル内の.classファイルの可視化ツール)で見るとたしかに、ちゃんとFlutterActivityなどの存在が確認できました。 f:id:ticktakclock:20190105225406p:plain

FlutterApplication

onCreateの処理はこの様になっていました。何かの初期化処理をしているようです。

    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }

FlutterMain

ここから一気に難しくなります。startInitialization()はstaticメソッドですね。

startInitialization()

このメソッドではざっくり4つの処理をしていました。3種類の初期化メソッドと、libflutter.soファイルのロード処理のようです。

            initConfig(applicationContext);
            initAot(applicationContext);
            initResources(applicationContext);
            System.loadLibrary("flutter");

この辺のinit処理で1つ1つコードを見ていくと、理解を超えそうでしたので、

initConfig()内に記述されていた、要所要所の単語を調べていく形で理解していこうと思います。

  • vm_snapshot_data
  • vm_snapshot_instr
  • isolate_snapshot_data
  • isolate_snapshot_instr
  • app.so
  • app.flx

libflutter.soとは

flutter engineの以下のネイティブコードのライブラリですね。BUILD.gnmakefileになります。 FlutterJNI.javaJava側の定義、 platform_view_android_jni.hC++側の定義です。

$ ls shell/platform/android/io/flutter/embedding/engine/
FlutterEngine.java      FlutterJNI.java         dart                    renderer

$ls shell/platform/android/
AndroidManifest.xml                     android_surface.cc                      io
BUILD.gn                                android_surface.h                       library_loader.cc
android_context_gl.cc                   android_surface_gl.cc                   platform_message_response_android.cc
android_context_gl.h                    android_surface_gl.h                    platform_message_response_android.h
android_environment_gl.cc               android_surface_software.cc             platform_view_android.cc
android_environment_gl.h                android_surface_software.h              platform_view_android.h
android_external_texture_gl.cc          android_surface_vulkan.cc               platform_view_android_jni.cc
android_external_texture_gl.h           android_surface_vulkan.h                platform_view_android_jni.h
android_native_window.cc                apk_asset_provider.cc                   vsync_waiter_android.cc
android_native_window.h                 apk_asset_provider.h                    vsync_waiter_android.h
android_shell_holder.cc                 flutter_main.cc
android_shell_holder.h                  flutter_main.h

VM snapshotとは

wikiにちゃんと書いてあるんですね・・・すごい。VMDart VMのことですね。

アプリの起動を高速化するため仕組みのようです。

dartコードを構文解析して得られたバイナリデータのようです。dartコードをロードしたオブジェクトが書き込まれている感じでしょうか。 そのスナップショットを実行すると短い起動時間で処理を再現できるというものみたいです。

僕らが書いたmain.dartはapkビルド時にsnapshotになって、snapshotをDart VMに食わせているのかと推測していますが、間違っていたらスミマセン。。 github.com

以下のドキュメントも参考にしました。 https://www.cresc.co.jp/tech/java/Google_Dart/DartLanguageGuide.pdf

instrって何

instructions(命令?)のことらしいですが、スナップショット自体と処理を別々に管理しているのでしょうか。(ヒープスナップショットと処理スナップショットとか?)

isolateって何

このことであってますかね。Flutter (dart)は基本的にはシングルスレッドですが、 isolateというライブラリを使用することで並列処理が実現できるようになるようです。

その並列の単位をisolate(アイソレート)と呼称し、dart vm上では必ず1アイソレート立ち上がる、と認識しています。 api.dartlang.org

app.soとは

libflutter.soと同じような感じで、libapp.soがapkの中にあるのかなと思いきや存在しない・・・

f:id:ticktakclock:20190106174512p:plain

調べても全然出てこないので、そもそもこのapp.soが使われていない気もしてきました。 一応AOTビルドコマンドが存在します。 github.com

早速試してみましょう。

$ flutter build aot
Building AOT snapshot in release mode (android-arm-release)... 14.4s
Built to build/aot/.
$ ls build/aot/
app.dill            kernel_compile.d
frontend_server.d       kernel_compile.d.fingerprint
gen_snapshot.d          snapshot.d.fingerprint
isolate_snapshot_data       vm_snapshot_data
isolate_snapshot_instr      vm_snapshot_instr

app.soないじゃん 😇 コード検索してもそんなに出てこないし、もう使用されていないのか、根本的になにか勘違いしているのか 🤔

app.flxって何

Flutter SDK側を調べるとそれっぽいものが見つかりました。 ios側のビルドで使用するものっぽいです(flxはFiLe eXtensionの略称であっているのか?)

github.com

そもそもapp.flx自体がapkの中に入っていないですね。(app.soのときのapkのキャプチャ参照)

また、flutter buildでビルドできるコマンドを見てみるとflx Deprecatedと非推奨になっているので昔の名残なのかもしれませんね。

$ flutter build
Flutter build commands.

Usage: flutter build <subcommand> [arguments]
-h, --help    Print this usage information.

Available subcommands:
  aot      Build an ahead-of-time compiled snapshot of your app's Dart code.
  apk      Build an Android APK file from your app.
  bundle   Build the Flutter assets directory from your app.
  flx      Deprecated
  ios      Build an iOS application bundle (Mac OS X host only).

Run "flutter help" to see global options.

おわりに

新年の調べもごと『Flutterってどうやって動いているのか?』を知りたくてFlutter Android側のコードを眺めてみました。 FlutterApplicationではlibflutter.soを使用するための準備とFlutterVMを起動する前の設定値やファイルの準備を行っているようでした。 調べてもそんなに出てこないし、まだまだわからないことが多くてツラミ感じていますが、次はMainActivity側のコードを読んで行こうと思います。

Flutterに入門してみて理解したところやハマったところ

前説

こんにちは、tkyです。

2018/10/08 に開催された技術書典5で購入(知人経由で・・・)したFlutterの入門本でいよいよ私もFlutter入門者になりました。 その時学習した内容とハマりポイントなどを知見として残しておこうと思います。

今回学習に際し、mBaaSとしてFirebase Cloud Firestoreを使用しています。

  • Flutter
  • Dart
  • Firebase Cloud Firestore

なお、このときFlutter1.0がリリースしたてだったのでそれなりにハマるだろうな〜という想定はしていましたが、そこまでズボズボではなかったですw

本記事ではFlutterのインストール手順や各プラットフォームのセットアップ方法などは記載しません。

flutter doctor しまくってエラーに記載されているコマンドをひたすら実行していったら環境整えられたので調べる手間が省けて楽でした。

何作ったの

『Flutte x firebaseで始めるモバイルアプリ開発』という本で解説している貸し借りアプリを書籍ともに作りました。

booth.pm

f:id:ticktakclock:20181215192411p:plain
つくったもの

良かったところ

iOSにはもちろんXCodeは必要ですし、エミュレータも同様ですが、デプロイがAndroid Studio上からできて、hot reloadもちゃんと効くのはまじで便利なのでは?

f:id:ticktakclock:20181216171224p:plain

学習してみて

まだDartの言語仕様部分だったり、どう作る?みたいな深掘りはできていませんが、こんな感じでFlutterって動いてるんだ〜という入門程度には学習できたかと思われます。

今後はWidgetの学習とレイアウトの特訓、アーキテクチャBLoCを使ったアプリ開発あたりに触れて知識の深掘りをしていこうと思います。

今回の気付き

  • Widget充実しすぎて便利
  • publicとprivateの区分方法がアンダースコア(_)
  • 直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい
  • 画面遷移がpush,pop形式でわかりやすい
  • 画面は基本的にStatefulで良さそう?

Widget充実しすぎて便利

FlutterはWidgetと呼ばれるUIオブジェクトを配置することで描画しますが、かなり種類が豊富なのでは?と感じています。

AndroidのUIっぽくMaterial Componentsが、iOSのUIっぽくCupertinoという種別のUIがありました。 書籍ではMaterial Componentsで実装し、Android, iOSどちらも同じUIとなっています。

flutter.io

publicとprivateの区分方法がアンダースコア(_)

Dartにはpublicやprivateのような修飾子はなく、基本publicの扱いとなります。 アンダースコア(_)を変数、関数、クラスにの先頭につけることで同じような働きができるようになります。

Dart公式ドキュメントにはアンスコつけたらそのライブラリだけで見れるようになるよ、と書いてあります。(この"ライブラリ"のスコープがわからん・・・) www.dartlang.org

start with an underscore (_) are visible only inside the library.

直感的にレイアウトを置ける、onPressedなどのイベントが書きやすい

例えばボタンを配置するときに、以下のように記述します。

ログアウトボタンを押したとき、Topページに戻る

              FlatButton(
                child: const Text("ログアウト"),
                onPressed: () {
                  _auth.signOut();
                  Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false);
                },
              ),

FlatButtonのプロパティにイベントを記述する書き方は結構好きです。 またNavigatorを使用して画面遷移を実現しています。画面遷移はルーティングで行うため、画面設計は必要そうですね。

画面遷移がpush,pop形式でわかりやすい

先程の例で説明しましたがNavigator.push or popで画面遷移を実現しています。それなりにわかりやすいかなと思いました。

// Topページに遷移する

Navigator.pushNamedAndRemoveUntil(context, "/", (_) => false);

// /newというルートを作成して、InputForm画面に遷移する
Navigator.push(
                context,
                MaterialPageRoute(
                    settings: const RouteSettings(name: "/new"),
                    builder: (BuildContext context) => InputForm(null)));

// 1つ前の画面に遷移する
Navigator.pop(context);

画面は基本的にStatefulで良さそう?

Flutterにはスプラッシュ画面など動きがない固定画面で使用するStatelessと、

内部でデータを保持し動的に表示を行う画面などで使用するStatefulという考え方があるようです。

ん〜、基本内部で状態を保持して画面更新するパターンのほうが多い気がするのですがStafefulで良さそうな感じでしょうか?もう少し学びながら掴んでいきたいと思います。

ハマりポインツ

AndroidについてはAndroid Studioで開発できる点でほぼ何もハマることなく開発できました。

iOSがちょっとハマりました。

  • iOSCould not build Objective-C module 'Firebase 問題
  • iOScloud_firestore/CloudFirestorePlugin.h' file not found 問題

iOSCould not build Objective-C module 'Firebase 問題

最初の問題でした。エラーの内容通り、Ob-cのFirebaseモジュールがSwiftではビルドできない、と言われていますが、なぜ???という感じですね。

Ob-cのモジュールをinstallしてしまったのだろう、という感覚レベルで、対応法がぐぐったらすぐに出てきたので解決でした。

簡単に言うと、「Podfile 系でinstallしたやつ全部消してもっかいinstallし直す」です。

pippi-pro.com

iOScloud_firestore/CloudFirestorePlugin.h' file not found 問題

こいつにずっと悩まされました。。

f:id:ticktakclock:20181215190413p:plain
cloudfirestoreplugin file not found

試行錯誤した内容は以下です。まぁ解決できませんでした。

  • pod install しなおす
  • flutter clean してみる
  • コマンドでビルドしてみる「flutter build ios
  • flutter packages getしてみる

また、「flutter firestore」のチュートリアル等を確認して自分の手順と違うところを確認したり。 Firebase for Flutter

Podfileはこんな感じで書いてました。

target 'Runner' do
  use_frameworks!

  pod 'Firebase/Core'
  pod 'Firebase/Firestore'
end

この問題2つの共通点

よくよくハマった2点を考えてみるとどちらもPodでした。実はFlutterをやるまでcocoapodsについては未導入だったので今回初めてインストールしたのですが、 setupコマンドを打っていないことに気が付きました。

$pod setup

本にも書いてあるので、ちゃんと本に書いてあることは読みましょう、というn度目の教訓を得ました・・・

setupしたあとにxcode起動したらエラーは消えて無事に動作確認できました。

podfileに書かれていることはまだ理解できていないので、「なぜエラーが消えたのか?」は微妙にわからないままなので要勉強です。

僕はそもそもiOSをやったことなかった

Cordovaを使ったアプリ開発の経験はあるので、XCodeiOSプロジェクトをビルドするとかはできるのですが、 iOSネイティブをやったことなかったことで基礎的な問題の解決に少し時間がかかりました。

しかしながら、開発の効率が非常に高そうなFlutterは今後とも触れて行きたいと思います。

付録(バージョン情報など)

開発時点でのFlutter doctor内容を載せておきます。

$ flutter doctor -v
[✓] Flutter (Channel beta, v1.0.0, on Mac OS X 10.13.6 17G2307, locale ja-JP)
    • Flutter version 1.0.0 at /Users/ticktakclock/flutter
    • Framework revision 5391447fae (2 weeks ago), 2018-11-29 19:41:26 -0800
    • Engine revision 7375a0f414
    • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

[✓] Android toolchain - develop for Android devices (Android SDK 28.0.3)
    • Android SDK at /Users/ticktakclock/Library/Android/sdk
    • Android NDK at /Users/ticktakclock/Library/Android/sdk/ndk-bundle
    • Platform android-28, build-tools 28.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)
    • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 9.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 9.2, Build version 9C40b
    • ios-deploy 1.9.4
    • CocoaPods version 1.5.3

[✓] Android Studio (version 3.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 31.1.1
    • Dart plugin version 181.5656
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)

[!] VS Code (version 1.29.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[!] Connected device
    ! No devices available

pod setupしたときのPodfileの内容も載せておきます。

# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def parse_KV_file(file, separator='=')
  file_abs_path = File.expand_path(file)
  if !File.exists? file_abs_path
    return [];
  end
  pods_ary = []
  skip_line_start_symbols = ["#", "/"]
  File.foreach(file_abs_path) { |line|
      next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
      plugin = line.split(pattern=separator)
      if plugin.length == 2
        podname = plugin[0].strip()
        path = plugin[1].strip()
        podpath = File.expand_path("#{path}", file_abs_path)
        pods_ary.push({:name => podname, :path => podpath});
      else
        puts "Invalid plugin specification: #{line}"
      end
  }
  return pods_ary
end

target 'Runner' do
  use_frameworks!

  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
  # referring to absolute paths on developers' machines.
  system('rm -rf .symlinks')
  system('mkdir -p .symlinks/plugins')

  # Flutter Pods
  generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
  if generated_xcode_build_settings.empty?
    puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
  end
  generated_xcode_build_settings.map { |p|
    if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
      symlink = File.join('.symlinks', 'flutter')
      File.symlink(File.dirname(p[:path]), symlink)
      pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
    end
  }

  # Plugin Pods
  plugin_pods = parse_KV_file('../.flutter-plugins')
  plugin_pods.map { |p|
    symlink = File.join('.symlinks', 'plugins', p[:name])
    File.symlink(p[:path], symlink)
    pod p[:name], :path => File.join(symlink, 'ios')
  }
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['ENABLE_BITCODE'] = 'NO'
    end
  end
end