DockerでLaravel開発環境構築
こんにちはtkyです。
最近業務でPHPを使った開発をしているのですが、Laravelの開発環境をメモとして残しておきます。
※そろそろAndroidやりたい・・・!!どこかにAnroid開発転がってないですかね? 🤔
- 本記事でできるようになること
- 本記事で触れないこと
- とりあえずdocker-compose.yml見せて
- cloneしたらやること
- browser syncの導入
- sassのコンパイルについて
- ついでにAPI仕様も
- 終わりに
本記事でできるようになること
- Laravel開発するためのDocker開発環境テンプレートが構築できる
- swaggerでAPI仕様書も一緒に書ける状態にする
本記事で触れないこと
- Laravelとは,、Dockerとは、のような説明
- DBの構築については各プロダクトによるので割愛します
- Laravelのコードは各プロダクトによるのでgithubには入れていません。
環境はgithubで公開しています。基本的にREADME見たらここから先の手順を見なくても構築できるようになっています。
Docker Compose で以下のような環境を構築します。
ローカルマシン上の server/
配下と各Dockerコンポーネントをマウントして、Laravel環境を構築してnginxで公開するイメージです。
nodeも含めて全部Dockerで完結できるようにしました。API作るので一緒にswaggerも入れました。
私が開発環境するに当たり、参考にさせていただきました。ありがとうございます。 qiita.com
とりあえずdocker-compose.yml見せて
そうなりますよね。私もそう思います。別途 .env
ファイルを用意してください。github上に .env-sample
を用意していますのでrenameすればOKです。
version: '3.7' services: php: container_name: ${PHP_NAME} build: ${PHP_CONTAINER_DIR} ports: - ${PHP_PORT}:${PHP_PORT} volumes: - ${LOCAL_DIR}:/var/www nginx: image: nginx container_name: ${NGINX_NAME} ports: - ${NGINX_PORT}:${NGINX_PORT} volumes: - ${LOCAL_DIR}:/var/www - ${NGINX_CONF}:/etc/nginx/conf.d/default.conf depends_on: - php db: container_name: ${DB_NAME} build: ${DB_CONTAINER_DIR} environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_DATABASE} MYSQL_USER: ${DB_USERNAME} MYSQL_PASSWORD: ${DB_PASSWORD} TZ: 'Asia/Tokyo' restart: always volumes: - ./docker/db/data:/var/lib/mysql - ./docker/db/sql:/docker-entrypoint-initdb.d ports: - ${DB_PORT}:${DB_PORT} swagger: image: swaggerapi/swagger-editor volumes: - ./docker/swagger/${SWAGGER_YAML}:/usr/share/nginx/html/${SWAGGER_YAML} environment: API_URL: ${SWAGGER_YAML} ports: - ${SWAGGER_PORT}:${SWAGGER_PORT} node: container_name: node build: context: ./ dockerfile: ${NODE_DOCKERFILE} tty: true volumes: - ${LOCAL_DIR}:/app ports: - ${NODE_PORT}:${NODE_PORT} - ${NODE_UI_PORT}:${NODE_UI_PORT}
cloneしたらやること
.envファイルを用意します。.env-sampleをrenameすれば良いです。各々の環境によって値は変更して大丈夫です。
$ cp .env-sample .env
コンテナたちを起動していきます。
$ docker-compose up -d
phpのコンテナに入って、laravelのプロジェクトを作成します。
$ docker-compose exec php bash
$ laravel new
ここまで来たら http://localhost/
にアクセスするとLaravelのページが開いているかと思われます。
このhttp://localhost/
はnginxによって立てられているサーバーとなります。
これでLaravel環境構築としては完了です。 ここからは実際に開発を効率化するためにもう少し手を加えていきたいと思います。
browser syncの導入
webpack.min.js
を以下のように書きます。 webpack.mix-sample.js
をコピーしても良いです。
webpack.min.js
mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .browserSync({ proxy: { target: 'http://nginx', //nginxのコンテナサービス名と一致させる }, files: [ './resources/**/*', './app/**/*', './config/**/*', './routes/**/*', './public/**/*', ], open: false, reloadOnRestart: true, });
nodeのコンテナに入ります。
$ docker-compose exec node /bin/sh
$ npm i
$ npm run hot
run hotしたときにいくつかのnode_modulesが新規インストールされます。コマンドが終了してしまうのでもう一度 npm run hot します。
この状態で http://localhost:3000/
を見に行くとLaravelのページが開いているかと思われます。
PHPのコードを編集して保存するとブラウザリロードがかかって即時ページが反映されているかと思われます。
あとはゴリゴリ書くだけ!!!
sassのコンパイルについて
実は npm run hot
だとsassコンパイルがうまくできません。
そのため今の開発案件では npm run watch
を使用しています。
メモリ上に展開されるファイルをserveするhotコマンドとローカルマシン上のディレクトリに展開されるファイルをserveするwatchコマンドという違いがあります。
nodeコンテナ上で
$ docker-compose exec node /bin/sh
$ npm i node-sass
$ npm run watch
こうすると /server/public/
配下にコンパイルされたsassやjsが展開されるようになり、sass更新時、即時ブラウザ反映されるようになります。
またこのとき、常々public配下のファイルが変更されるため、 /serverpublic/css
と /serverpublic/js
は .gitignore
で管理対象外にすることをおすすめします。
ついでにAPI仕様も
API仕様書もかけるようにswaggerも入れておきました。くらいです。 不要であれば消してください。
http://localhost:8080/
で仕様書が見れるようになります。
終わりに
今回、初のPHPチャレンジでLaravelを触ったりDockerでの環境構築にトライしたりいろいろと勉強になりました。 Dockerfileの書き方も結構理解が進みました。
FormData.get()を使う時は対応ブラウザに気をつけよう
こんにちは、tkyです。
最近ReactのSPAやっていて、今日はその開発の中でハマったjavascriptのことを書きます。
- 何があったの
- やろうとしていたことは何か
- 何が原因だったの
- あれ?でも別のiPhoneだと動くんだけど・・・? 🤔
- iOS11.2と11.3を境に何が起こったのか
- 対応ブラウザとバージョンには気をつけよう
何があったの
APIで mutipart/form-data
なPOSTメソッドを叩く時って、要件によりしばしばあると思いますが、
ある日社内の検証端末(iPad)で動作確認していた時、POSTできない不具合が発生しました。
formData.get is not a function
やろうとしていたことは何か
- formdataに特定のパラメータが入っているかどうかPOST前に確認したかった
- 基本Google Chromeの検証ツール上で動作確認していた
※ブラウザの対応要件は chromeとsafariの2種でした。iOS、Android上でも動作すること、がブラウザ要件となります。ieとedgeは要件から切り捨てています 😎
何が原因だったの
実はiOSのsafari (以降、Safari on iOS)でForm.get()が非サポートのため使用できなかった、というものでした。
あれ?でも別のiPhoneだと動くんだけど・・・? 🤔
そうなのです。特定の端末だとPOSTできるのです。
では状況を整理します。端末が少なかったのでエミュレータでの確認となります。
あるiOSバージョンを境にして切り分けができました。
iOSバージョン | 結果 |
---|---|
10.2 | NG |
11.1 | NG |
11.2 | NG |
11.3 | OK |
11.4 | OK |
12.0 | OK |
iOS11.2と11.3を境に何が起こったのか
iOS11.3に搭載されているのはSafari11.1ですが、変更点は以下でした。
Service Workerの対応が一番しっくりきますかね。ServiceWorkerの実装が入ったことでWeb Worker側の基本実装も入り、Web Workerを利用するFormData側にも良い方向で影響があったのだろう。が今の見解ですが、あくまで推測なので違っていたらスミマセン。
対応ブラウザとバージョンには気をつけよう
開発時、ググって実装して動いたーでコミットしてしまう時、ありますよね。 Form.get()だけでなく、ちゃんと各ブラウザの対応状況は確認しておく癖をつけておくのが良いなと思いました。
BiometricPromptで指紋認証機能を実装する
こんにちは、tkyです。
今回はAndroidのBiometricPrompt
を使って指紋認証機能を試してみたいと思います。
こんな感じで動いてます。 コードと動作確認gifはgithubを参照してください。
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をゴリゴリしてました😅僕らでバージョン分岐しなくて良くなったのは事実です!
参考
DroidKaigi2019 2日目に行ってみた感想とか
こんにちは、tkyです。
2日目も行ってきました。1日目と同じようにセッション参加したtkyの個人的感想を述べる内容となっております。
1日目のレポートはこちらをご覧ください
この日見た講演はなんと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くらいに会場到着。サンドイッチやコーヒー、紙パックジュースなどを堪能できました。
エキシビジョンルームにて朝食が配布されています。
Dialogflowによる自然言語処理(NLP)を用いたボイスコマンド音声認識の精度向上
Dialogflowはgooglehomeアプリを作成する際にも使われますので、興味アリアリです。
googlehomeのアプリをサンプルで作った時にDialogFlowを使ったことがあるのですが、Androidアプリからも使ってみたいですね!聞くところによるとv1のsdkは2019.9にDeplicatedになるので要注意だそうです。v2との使い勝手はどう異なるのだろうか。
DialogFlowについては本ブログでも別途触れてみたいなと思います。
やっぱり、固有名詞を検出するにはEntityを設定しまくるしかないのですね。音声認識の揺れを吸収するためのアノテーション作業などはやったことないので後ほど見てみたいと思います。
Wi-Fi RTTによる屋内測位アプリを作ろう
WiFi RTT(Round Trip Time IEEE 802.11mc)全然知らないから気になってました。
基本的にFusedLocationを使って位置情報を取得します。これを使うことでGooglePlayServicesを経由して位置情報を取得するようにできます。 WifiRTTは屋内でも使用できる位置測位用の規格であるため、アクセスポイントを4つ使うと精度は1mとかなり良くなる状況だそうです。
アクセスポイントからの距離はわかるが、アクセスポイントがどこにあるかは知っておく必要がある、なるほど 👀 測位方法はピタゴラスの定理と連立方程式を組み合わせて実現するようです。
少なくとも3点測位ポイントがあり、その3点のポイントを線で結んでできる空間内にいる必要があることが測位方法から見ても理解できるかと思われます。
API28からscanのAPIが色々変更担っているようでDeplicatedにもなっている模様
精度を出すための条件がなかなか厳しそうですね。アクセスポイントの設置場所や地図の選定など、やることは色々ありそうです。 他のセンサーも併用して使うとより精度の向上が図れそうと思いました。
ここでお昼ご飯
1日目のお弁当を撮影しそこねましたが、2日目はちゃんと撮影できました、この後のFlutterのセッションルームにて着席して昼食休憩です。
All About Test of Flutter
you brideという婚活支援サービスがFlutterで作成されているようです。
今回のgithubも要チェケです。
自分のコードに自身を持って確かなものにするためにテストを書く。いい話だ。
名言も飛び出ましたw
人間は超優秀全自動アサーション関数
自動テストにするか、人力テストどちらを取るかは、継続してコスト回収できるかどうかで判断すると良さそうです。
めちゃめちゃ良い発表スライドで、終わった後もガッツリ見てます!
質問時間にて、未実装部分についてはスキップ関数にするのか、Failさせるのが良いのか議論が最後に行われていました。 単純にFailした場合と未実装なのでFailなのかがパット見わからないので、メンバーのテストリテラシ状況に合わせて選択するのが良いのでは、という結論にいたりました。
また、テストレポートについては現状HTMLなどで見れるようなものはこのとき回答はありませんでした。コマンドライン上でもギリ見やすそう・・・?
小休憩
ちょっとだけ疲れたので小休憩です。
Clip Roomさんのブースでデザインガイドラインの一部を見せていただいたりしました。やっぱりデザイナーさんとエンジニアがコミュニケーションとれる状況良いですね。
そしてまたコーヒー・・・美味しいんですよね〜
Lifecycle, LiveData, ViewModels - The inner wiring
architecture componentの講演となります。英語のセッションですが、同時翻訳なので安心?して聞けます。 Architecture ComponentのLifecycleコンポーネントなどなどの解説となります。
LiveDataはデータホルダーであり、streamが終わるという考えはなく、ライフサイクルで使われるため、本質が異なるということですね。 しばしばLiveData vs RxJavaというように比較される事が多いのですが、そもそも本質が異なるためにLiveDataとRxJavaがともに存在する、という認識が出てきました。
翻訳が非常に聞き取りやすく個人的に理解が進みました。
multi-module Androidアプリケーション
https://speakerdeck.com/sansanbuildersbox/multi-module-android-application
なんとかマルチモジュールの講演入れました!
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 によるアプリ内遷移の管理
Architecture Componentに関する講演です。
画面遷移時の課題を解決できるようなライブラリとツール群のことですね。
Navigation Architectureにおいてはどの画面がスタート地点になる画面なのかをガイドラインとして定義されているようです。 deep linkについても「同じ画面にいるなら、同じ画面スタックが形成されているべき」というような原則が存在しています。
navigation を定義するxml内でfragmentを定義し、fragment layoutの中にapp:nav
やandroid:name
の記述を書くことでNavigationの遷移を実現できるみたいです。
あとはJavaとKotlinでSafeArgsの指定の仕方が異なるようですね。JavaはBuilderパターン、Kotlinはnamed argumentで指定、という具合です。
最近のAndroidの開発に触れていないので、結構新鮮でした。
Android Thingsでのプロダクト開発
DroidKaigi2019 AndroidThingsでのプロダクト開発 - Google スライド
Android Thingsはgoogleが提供するIoT向けプラットフォームのことです。 Androidなので既存のシステムを利用できるところは強いですね。ハードウェアレイヤもGoogleがサポートしているようなのでこういったところも強みになりそうです。
最初はmuiの製品紹介から入ります。スマートディスプレイの代替としての製品のようです。 muiプロダクトデモを実際に見せていただいて実感がわきます・・・!!!
AndroidThingsとAOSP,Linuxとの違いについては、カーネルのカスタマイズができるかどうか、SoCの選択肢があるかどうか、OTAができるかどうか、が選定のポイントとなりそうです。
メリットは以下。
一般のデベロッパーがさわれる領域にPeripherarl I/O APIの入出力をフレームワークに統合するためのUserDriver(Input Driver)というものが結構微妙(画面描画あたり、座標空間周り)らしいですね。UserDriver(InputDriver)とはI2CのIOをボタン押下イベントに変換するようなものらしいです。
基本カーネルやフレームワークはGoogleが完全に制御しているので一般デベロッパーは触ることができず。 先程まで強みだと思っていた点が回り回ってデメリットになってきました・・・カーネルをカスタマイズできない点については、ラズベリーパイで発生するclock stretching問題において重要な欠点のようです。というのもこの問題を暫定で解決するためにI2Cのボーレートを変更する必要があるそうですが、OTAでファームウェアを流し込んだ時にその編集部分のコードが上書きで消えてしまうそうです。事実上OTAできないじゃん!ということになるわけですね・・・闇が見えてきました
新規でThingsのプロダクト化が難しい状況で有ることがわかりました・・・w セッションが悲しい雰囲気に包まれてしょんぼりだったのですが、トライ&エラーの結果を共有していただけた非常にためになるセッションでした!!
BLEアプリ設計パターン
BLEのつらみについては触れず、設計ノウハウにフォーカスしたお話です。
BLEはHTTPSのようなプロトコルとどう違うのか、実際にどのように設計しているのかをノウハウ共有いただけるセッションでした。Qrio Lockはいわゆるスマートロックで、BLE通信を使って鍵の開け締めを行っているようです。
通信という意味ではWifiという選択肢もありますが、省電力や近接検知ができるという観点で、BLEの選択肢が出てくるということですね。
Webの世界はRetrofitのようなライブラリを使うと非常に楽になったしますが、BLEは20バイト制限があるので、 20バイト以上のデータを扱うときはパケットを分割して受け取った側でくっつけるような、HTTPのプロトコルが担保する領域を自分で担保する必要があるようです。なるほど・・・
BLEが仕様で搭載しているセキュリティではできない自前でセキュリティを担保する必要も出てくるとのこと。なかなかつらみが出てきましたね!!
「ロック(スマートロック機器)の名前」は情報をデバイスで持つべきか、サーバで持つべきか、という観点も非常に興味深いです。
→結論的にはwebサーバ側で情報を持って、デバイス側は極力シンプルな構成にすることが良さそうです。
登場人物がスマートロックデバイス、スマホ、webサーバが存在しており、この3者がうまく同期している必要があるのもなかなか難しい課題です。 この際の通信の整合性を担保するためにべき等なAPIにする必要があることも重要ですね。
実際にアプリはMVVMで作成されているようですが、テストに関するメリットは高く、 しかしながら責務分けに関するベストプラクティスを探すようなコストも高くなってしまうこともあるとのことです。
さいごに
2日間有休を使って参加しましたが、最高に楽しかったです。こういうの参加するとやる気出ますね。来年も行きたい。。。行けるようにがんばります。 色々と新しい単語もピックアップできたのでこれからも継続して勉強していきましょー
いただいたものたち
DroidKaigi2019 1日目に行ってみた感想とか
こんにちは、tkyです。
DroidKaigi2019に行ってきました。1日目のレポです。
2日目のレポはこちらをご覧ください。
行ってきた講演は以下です。お昼後、yanzmさんの講演見たかったのですが、満席御礼で入れず・・・
- マテリアルデザインの起源とベースとなる哲学(13:50~)
- 辛いと評判のAndroid BLEを頑張って使い続けた話(14:00~)
- Chrome Custom Tabsの仕組みから学ぶプロセス間通信(14:50~)
- Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発(15:40~)
- Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題(17:10~)
お昼に到着
いざ!
yanzmさんの「LiveData と Coroutines で実装する DDD の戦術的設計」を見たかったのですが、なんと満員!さすがというべきか入れませんでした・・・あとで配信見ます!
会場の様子です。
そして急遽見たかったもう一つの講演に向かいました。
マテリアルデザインの起源とベースとなる哲学
ちょうどマテリアルデザインを取り入れたUIを作っているプロダクトの最中だったので、興味アリアリの内容です。
そもそも誰がマテリアルデザインを作ったのかさえ知らなかったので恐縮ですが、すごい面白い講演でした。
そもそも誰が作ったのか・・・・ googleのデザイン責任者であるマティアス・デュアルテさんが作ったそうですね。昔のですが記事が出てました。
https://www.gizmodo.jp/2014/07/_android.html
この方の歴史的背景や知識を見ることでマテリアルデザインの思想と哲学を見ていく、という内容でした。
マテリアルデザインについては私は根本的なところで理解がかけていたようで、本講演を受けて改めてマテリアルデザインガイドを査読していきたいです。
辛いと評判のAndroid BLEを頑張って使い続けた話
www.slideshare.net
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
Chrome Custom Tabsの仕組みにおいては別にGoogleChromeだけが保つ機能ではなく、CustomTabsと呼ばれるサービスに対応したアプリが提供できる機能となっているようです。
こういった仕組みを提供、実現するためにはAIDLという他のアプリケーション(サービス、=別プロセス)とやり取りするためのインターフェースを定義・実装することで実現できます。
実装面でいうとCustomTabsServiceっていうAbstractクラスがあって、それを継承したクラスを作ればとりあえずCustomTabsを提供するサービスが作れるみたいです、こう聞くと少しハードルが下がる・・・?😅
最近モダンな開発にフォーカスが当たる中、Androidのフレームワークに関する機能にふれる機会が減ってきていたので非常に勉強になる講演でした。
Understanding Kotlin Coroutines コルーチンで進化するアプリケーション開発
リリースされて間もない機能なので要チェケです。
- どのようにコルーチンを使うか、学ぶか
- どのようにコルーチンを適用するか
- コルーチンをいつ使うか
という点にフォーカスを当てた講演でした。
Concurrency and Parallels(平行性と並列性)というワードが印象に残っています。 コルーチンスコープについては基本的にアプリ内のライフサイクルに基づいてスコープを定義して動作させるのが良さそうです。
また、ほかアーキテクチャへの適用として重要になる考え方としてモジュール間の関心に着目するという点です。 データフローが統一するならRx、独立性の高い開発モデルを主眼とするならモジュール間でコルーチンを適用することを検討しても良さそうです。
このあたり私も初見の類に入るので自分の中でしっかりと理解して行きたいです。
小休憩
少し疲れたので休憩がてらにコーヒーを飲みに来ました。 このアルファベッティカフェさんがDroidKaigiに出張に来てくださり、プロのバリスタさんによるコーヒーを飲むことができてよかったです。普通にお店に行きたい・・・
yahoo japanブースにてTRILLグッズを頂いたり、その他様々なブースを覗いたりしました。
Chrome + WebAuthnで実現できるパスワードレスなユーザ認証体験と開発者の課題
本日ラスト講演です。
パスワードについての現状の課題、コストを考え人間が適用できる認証方式を考えた結果のWeb Authentication APIという1つの選択肢のようですね。
正直なところ私が無知すぎて単語を追うのが精一杯でした・・・ 😇
パスワードの方式として「忘れた、パスワード再設定フローもない」はいわゆる詰んだ状態になりますが、web authnの仕組み(要はデバイスを使った指紋認証と組み合わせたもの)だと「スマホをなくした/壊れた」ら詰んだ状態になる、ということなので、詰んだ状態からどのように回避するかが課題になりそうです。
アフターパーティー
いよいよアフターパーティです 🕺
なお写真はpixel3で撮影したものです。ポートレートめっちゃよく撮れる😎
お食事も美味しかったし、登壇者のかたと会話できましたし良い体験ができました。
終わりに
2日目は朝ご飯食べに早めに行こうと思います!引き続き2日目のレポートもどうぞ!
頂いたものたち
Flutter Androidのソースコードを読む(FlutterActivity編)
こんにちは、tkyです。
前回FlutterApplication.javaのコードを読んで、Dartを動作させるための初期設定を査読しました。
今回はFlutterActivity.javaのコードを読みながら、アプリ起動するまでを追ってみようと思います。 調べた軌跡を残しているので今回もそれなりに長い記事となっております。
前回も書きましたが・・・今回読んでいくのはこれ
flutter.jar
Android Studioにはjarのclassファイルに定義ジャンプすることができるので⌘ + クリック(winの場合は ctrl + クリック)で読めますが、 ここで読めるのはあくまでjavaで書かれた領域だけなのでネイティブコード(C/C++の領域のことを指します)も見ることを考慮して flutter/engineもcloneしておきます。
目次
- 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_android、iosには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 (6000円くらい)
- SwitchBot Hub Plus (7000円くらい)
- SwitchBot(4500円くらい)
Google Home
音声でスイッチを入れるために使用します。厳密に言うとSwitchBot Hub Plusにアクセスするために使用します。
音声操作が不要であれば、「Google Assistantアプリ」または「SwitchBotアプリ」があれば大丈夫です。
SwitchBot Hub Plus
色々できることはあるのですが、以下のことをするために使用します。 - 外から(インターネット経由で)アクセスする - 赤外線コードを発信する(今回の場合エアコンのリモコン) - SwitchBotを操作する
SwitchBot
この子には我が家の追い焚きスイッチを押してもらいます。 SwitchBotと専用アプリはBLE通信で行うため、外に出ているとアプリから直接SwitchBotが操作できません。 そのため、SwitchBot Hub Plusを介して制御することになります。
!!!!注意!!!!
本品を購入する前に本記事のSwitchBotの取り付け例(画像)を見てください。 スイッチの形状や家の構造によって取り付けられないケースもあります。
各アプリはインストールしておきましょう
- SwitchBot
エアコン操作
まず、SwitchBot Hub Plusでエアコンの操作からしていきます。 SwitchBot Hub Plusを開封して、電源をつなぎリモコン操作できそうな位置に配置します。
SwitchBot側
続いて、アプリ側です。アプリを開くとすでにデバイスが見つかっている状態になっているかと思われます。 この辺のつなげ方は取扱説明書見たほうが早いです。 『Hub Plus D0』と書かれているところの丸アイコン部分をタップします。
「新しいデバイスを追加する」をタップします。
エアコンを使いたいのでエアコンを選びます。
SwitchBot Hub Plusに向かってエアコンのリモコンの暖房ボタンでも押しましょう。
1つリモコンボタンを送信すると、エアコンがONになるかと思われます。
アプリからリモコン操作してみます。この時点でエアコンがONにならない場合、正しく設定できていないか、非対応(さすがに少ないでしょうが・・・)な可能性が出てきます。
単純にアプリから操作するだけで良い場合は、この状態で家の外からエアコンONできますのでここで完結です。追い焚き操作に移りましょう。
GoogleHomeから操作したい場合は次の手順に進んでください。
Google Home側
Google Homeアプリを開くとはじめにGoogle Homeデバイスとの接続を行う必要があります。
こちらは画面のUIに沿ってボタンを押していくだけでセットアップが完了しますので、テンポよく設定していきます。
続いて、SwitchBotとリンクしていきます。設定からデバイスを追加(プラスマーク)、を選びます。
サービス一覧から『SwitchBot』を探し出します。検索するとすぐに見つかります。
ログインを求められる場合はログインしましょう。
そうすると、今SwitchBotで使用できるデバイス一覧が表示され、どの機器をGoogleHomeとリンクするか尋ねてきます。まぁSwitchBot Hubしかないので1つ選びましょう。
ここで設定する部屋の名前は「リビング」、デバイス名を「エアコン」としましょう。 これで完了です。 tky「OKグーグル、エアコンつけて」 Google Home「はい、リビングエアコンをONにします」 が実現できるようになります。
お風呂の追い焚き操作
SwitchBot側
いよいよ追い焚きを操作してみたいと思います。こちらはSwitchBotデバイスを使用します。
まず、デバイスの電源を入れましょう。その後、SwitchBotアプリで見てみます。
いましたね。画面に表示されているデバイスのアイコン部分を押してみましょう。 デバイスがウィーンと動きましたか?動作確認OKです。 また、Google Homeから操作を受け付けるために、「Hub Plus D0」=>「SwitchBot」=>「お風呂(デバイス名です)」からGoogle Homeからのアクセス許可をつけておきましょう。
これをこのように取り付けます。裏はシールになっているため、貼り付けられるだけの面積が必要です。 3.5cm x 4.5cmくらいの領域があれば十分に貼り付けられるでしょう。取り付け例です。
Google Home側
すでに、SwitchBotサービスをリンクしているので使えるのかな?と思いきや実はリンクした時点のデバイスだけが使えるようで、その後追加したデバイスを使用できるようにするには、一度リンク削除して、再度リンクする必要があるみたいです。(微妙・・・) 仕方なし、前回同様、一度リンクを削除して、再度サービス一覧から「SwitchBot」を選び直します。 そうすると、今SwitchBotで使用できるデバイス一覧が表示されます。SwitchBot Hub PlusとSwitchBot両方あることが確認できます。 もちろん両方選択します。
追い焚きデバイスは「お風呂」と命名しました。これで完了です。
tky「OKグーグル、お風呂つけて」 Google Home「はい、お風呂をONにします(ウィ〜ン ポチ」 が実現できるようになります。
快適に利用するために
これはSwitchBotアプリだけでしか使えないのですが、スケジュール機能があります。 簡単に言うと毎朝7:00にエアコンをONにしたり、毎晩19:00に追い焚きをONにしたり、そういうことができるようになります。 控えめに言って最高だと思います。
おわりに
いかがでしたか?プログラミング要素なくても案外家電操作できる良い時代になりましたね。 リモコンについては汎用性が高く、TV、扇風機、LED照明、(そこまでしなくても・・・wという感はありますが)色々と楽しめそうです!