eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

FirestoreとMemorystoreとDatastoreと

Google Cloud Platformのサーバーデータはどこに保存するか

  • SQL系は月額固定課金なので、気軽には使えない(お金持ちを除く)
  • なので従量課金制の保存先としてDatastoreを気に入っていた

DatastoreとMemcache

  • DatastoreはFirestoreと両立できない
  • アプリがFirestoreを使っており、同じGCPプロジェクトにGAEが同居するとDatastoreは虚空へと消える
  • MemcacheがGoogle App Engineは標準APIで使えた
    • GAEのRuntimeが刷新されたことでMemcacheアクセスが行えなくなった

Memorystore

  • Memcacheの後継のように見えて、違う
  • コイツは月額固定だ
  • 最低環境で$50/monthが虚空へと消える
  • Private IPだけが割り振られるので、ローカルマシンからの開発がやりづらそう
    • そのうち解決しそうではある
    • Compute Engineのインスタンスを立てて、SSH経由で頑張ればつながるらしい
      • f1-micro料金が$5 / month

Firestore無料枠 VS Memorystore最低課金額

  • Read/Write共に50,000の無料枠がある
  • 250,000 Read/Write per Dayくらい使うと$50課金が発生する
  • もう直接アクセスでいいんじゃね?
    • 1msでも早くレスポンスを返したい場合を除く

Firestoreは安い

  • 安い
  • ガンガン使おう

Google App Engineで香川県ゲーム条例に対応する

雑感

  • 条例が最短で春に施行される
  • なるべく簡単に香川県スマホ禁止条例に対応しなければならない

X-AppEngine-City, X-AppEngine-CityLatLong

  • Google App Engineはリクエスト元のIPからざっくりとした国・都市情報を取得できる
  • X-AppEngine-City ヘッダをチェックし、香川県の都市だったら 403 Forbidden を返してしまうのが一番楽ではないだろうか
  • 万全を期すなら、 X-AppEngine-CityLatLong ヘッダをチェックし、香川付近の位置であれば弾くのも良い
  • コレであればコストは最低限になるのではないだろうか

ちゃんと対応するなら?

  • 香川県の人口は100万人弱
    • 日本人1億人として、日本を対象にしたサービスであればユーザーはざっくり1%
    • 1%のユーザーが、コストを上回るリターンを見込めるのであれば対応するほうが良い
  • クライアントごとに X-Allow-Kagawa-Game-Law みたいな設定を入れさせるのは非常に面倒だ
    • Chromeとか主要なエンジンが対応してくれないかな?

LiveDataを複数回observeしても1個のObserverしか登録されない現象と解法

前提

  • LiveDataのonActive/onInactiveで処理を行わせていた
  • 強制的にLiveDataをactiveにするため、次のようなExtensionを作った
    • LiveDataをactiveにしたいだけなので
    • Observerはナニも処理していない
// 強制的にLiveDataをactiveにする
fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) {
    observeForever(Observer { /* drop value. */ })
}

問題点

  • 2箇所以上から LiveData.forceActiveAlive() を呼び出す
  • 1箇所でもonDestroyが走ると、LiveDataがinactiveになる

理由

  • Kotlinのコンパイラが、 Observer { /* drop value. */ } をシングルトンとして最適化し、コンパイルしていた
  • そのため、2回め以降の LiveData.forceActiveAlive() で実行がキャンセルされていた

解法

  • 毎度newが走るようにしてあげれば想定通りに動く
private class ObserverWrapper<T>(private val observer: Observer<T>) : Observer<T> {
    override fun onChanged(t: T?) {
        observer.onChanged(t)
    }
}

fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) {
    observeForever(ObserverWrapper(Observer { /* drop value. */ }))
}

コンパイラの気持ちをわかってあげよう

  • コンパイラの気持ちを考えれば、このバグは回避できたかもしれないんだ

10年代の思い出 / JavaのゲームをBREWへ移植した思い出ばなし

BREWとは

BREWとの出会い

  • 新卒で入社し、手取り17万円で働いていた会社で、 Javaに似たへんな言語 で作られたゲームを、BREWへ移植する仕事が降ってきた
    • ゲームの内容自体は面白かったが、ソレは別な話だ
  • 当時docomoユーザーだった俺は、BREWやKCP/KCP+なんて全く知らない
  • なので、手探りで開発をスタートすることとなった
    • 会社的には、前任者のコードがサンプルとして存在していた
  • 2010年前後の、お話である

BREWのざっくりとした開発環境

  • Windowsで開発環境が構築できる
  • Windowsでは x86環境で動作するシミュレータ が提供されている
    • これが後に悲劇を起こす
  • 会社の公式開発環境はVisual C++ 6.0だった
    • どういうわけか、会社には 無償で使えるインストーラ が社内サーバーにあった
    • 新卒で入り、毎月17万円(手取り)ものお賃金をいただき、キラキラしていた俺は、ソレに目をつむった
    • Visual Studio 2008(無償版)を途中から使い始めた
  • 実機動作時はARM環境向けにクロスコンパイルを行う必要がある
    • このコンパイラは有償である
    • MACアドレスによる認証が行われるが、なぜかこの職場のプログラマ用PCには同じMACアドレスを指すハードウェアが認識されていた
    • 新卒で入り、毎月17万円(手取り)ものお賃金をいただき、キラキラしていた俺は、ソレに目をつむった
  • 開発言語は公式にはC言語のサブセット、コンパイラC/C++対応されていた
    • これが後に悲劇を起こす
  • ハードウェアの仕様上、float演算が行えないことが再三警告されていた
    • これが後に悲劇を起こす
  • もとのゲームはJavaプリプロセッサを適用するような当時のガラケーらしいJavaのような言語で開発されていた
    • これが後に悲劇を起こす
  • 詳細な公式ドキュメントは手に入らない
    • これが後に悲劇を起こす
  • アプリの容量(プログラムと、ゲームリソースの容量合計)が1.5MB前後
    • これが後に悲劇を起こす
  • アプリをリリースするためには、KDDIのストア審査が必須である
    • これが後に悲劇を起こす

起こった悲劇たち

アプリ容量天元突破

  • この問題にぶち当たるのは2度めである
    • 1度目は入社直後dojaでやらかしている
  • もともとの容量が大きいゲームだったため、プログラムもしくはグラフィックリソースを大幅に簡略化しなければならない
  • が、そんな開発リソースはない
  • そんなときに見つけたのが当時の2chの戦士たちである
    • すでにネットの海の藻屑になってしまったようだが、数年前まではBREW開発者Wikiがあった
  • BREWはメモリ保護がない (ので、下手なメモリにアクセスするとOSごとクラッシュもまれによくある) ことを生かして、
    1. プログラム全体をzip圧縮
    2. OSからは極小なブートローダーを起動
    3. ブートローダーがプログラムを解凍
    4. CPUの実行ポインタを解凍したプログラムの 特定アドレスに設定して強制実行
  • というワンダフルな手法が紹介されていたので、それを参考にさせてもらった

C言語のサブセットで開発するゲーム

  • 基本的にはC/C++言語であるが、現代に比べてハードウェア・ソフトウェア共に低性能だったため、言語のフル機能が使えるわけではなかった
  • 特に、 グローバル変数が使えない という点については頭を悩まさせてくれた
  • なにせ、もとのゲームプログラムがstatic変数もりもりマッチョマン(constではなく、書き換えもしてる)なので、この制限をどうにか突破しなければならなかった
  • 前述のように、このBREWが動く環境にはメモリ保護がない
  • 解凍したプログラムも、実行ポインタを書き換えれば普通に動く
  • なので、 自己書き換え もいけるんじゃね? というひらめきを実行した
// 実行バイナリに4byteぶんの領域を確保
// この先頭ポインタを int* にキャストして、無理やり自分自身を書き換える
// 普通にint変数を用意するとコンパイラが最適化しやがるので、文字列を使う
const char STATIC_MEMORY = "012"
  • この手段はARM(実機)ではうまくいくが、Windows シミュレータでは動作しない
    • シミュレータはWindows用のDLLファイルを作って読み込むので、メモリ保護がビンビンに働いてアクセスエラーになる
    • なので、 GLOBAL_VAR() SET_GLOGAL_VAR() GET_GLOBAL_VAR() のようなマクロを使ってアクセスすることで切り抜けた

Javaの配列.length問題

  • もとがJava言語ベースの実行環境であるため、配列は .length で長さを取得できる
  • ジグ配列もサポートされているし、バリバリ使われている
  • 移植先はC/C++やぞ、配列にそんなモダンな機能はない

方法1, コンパイラの手を借りる

  • ソースコードにベタ打ちされた配列は sizeof() でbyte数を取得できる
  • sizeof(array) / sizeof(array[0]) とすると、 配列全体のbyte数 / 1要素のbyte数 = 配列の要素数 を求めることができる
  • なので、定数的に定義されているジグ配列はチクチクとこの方法でlenを取り出すようにマクロへ置き換えた

方法2, new演算子オーバーロード

  • 動的に確保している配列のlengthは、sizeofじゃとれない
    • 例えばint配列はnewで確保すると int* なので、sizeofしても容量はポイント1個分なのだ
  • なので、メモリアロケータを加工して次のようなデータを返すようにした
    • [隠しヘッダ ここにbyte数とか書いとく][実データ]
    • new関数のなかで、ヘッダ + 実データ容量を確保して、実データの先頭アドレスを返す
    • lengthにアクセスしたいときは、隠しヘッダの中身を見て計算する
    • ついでに、Javaの初期化を再現するためにゼロ値埋めも行っている

これらを簡易的に行うためにtemplateを使う

  • templateで↑を自動的に扱えるように、ラッパーを作成した
    • .length() のようなメソッドを提供したり
  • おかげで、lengthに関わる移植が終わる頃にはだいぶコードがスッキリとしていた
  • template便利だな

  • そして次の悲劇が起きる

templateサポートの悲劇 / シミュレータはエミュレータじゃない

  • BREW用のARMコンパイラはtemplateもサポートしていた
  • なので、例えば vector3<int> みたいなtemplateを作り、それを別なtemplateの引数(templateの入れ子)をしていた
  • ある日、大量のコンパイルエラーが発生
  • シミュレータ(Windows環境)ではMicrosoft様の優秀なC++コンパイラによってサポートされていたtemplate入れ子が、ARMコンパイラでは非サポートだった
    • ドキュメントに書いとけボケぇ!
    • すまん、言える立場じゃなかったな。。。
  • template入れ子を排除すべく、ひたすらマクロに入れ替える日々が始まった
  • シミュレータはシミュレータであって、ARM実機を再現するわけじゃないと、このとき強く意識に根付いた
    • なので、iOS シミュレータ に対しては言いたいことあるが、まあコイツほど クソ 独特じゃないのでとても快適である

float演算が使えない とは

  • たしかに、当時のARM CPUはfloat演算が行えない
  • 代わりに、同等のソフトウェア命令にコンパイラによって置き換えられる
  • そんなコンパイラの知識なんてなかったので、 float変数を作ったら実機でコンパイルエラーになる と思い込んでた
  • セコセコと固定小数演算したり
  • 気づいたのは、だいぶ後の祭りである

公式ドキュメントが手に入らない

  • 正確には、正規コンテンツプロバイダーが手に入るようなドキュメントが手に入らない
  • 零細ゲーム会社なんてそんなもんだ
    • 手取り17万円しか払えないんだぞ
  • なので、普通にネット上からDLできるざっくりとしたドキュメントを使用した
  • ほんとざっくりだったなお前
  • 例えば、ネットワーク通信時にAPIに渡す構造体のメモリをいつ開放していいのか、失敗時のハンドリングどうするのか、ほとんど書かれてなかった
    • 仕方ないので四六時中保持してた

そして悲劇が起こる

  • 課金APIの詳細な動作仕様が手に入らなかったことにより、課金で問題を起こす
  • 多大な残業と休日出勤と、パブリッシャーサイドの寛大な対応により、なんとか許しを得たはず
    • なお、残業代も割増賃金も辞退した
    • 自責なので仕方がないよね、と新卒のキラキラした目の僕は自分に言い聞かせた
    • んなわけない
    • 17万円しか払えない会社やぞ
    • 発生するわけないやろ

文字が描画できない

  • グラフィックス系のAPIさわってたら、「せやな」程度だけど
  • 当時のdojaのプログラムは、3Dの描画をした上に文字を描画できた
  • BREWは文字描画が専用APIで、3Dの描画と排他仕様だった記憶がある
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない
  • 頑張ってそれっぽく見せるようにした思い出

半透明が描画できない

  • BREWの2D APIの仕様である
    • dojaはそこそこ高速に半透明が2D APIで描画できたのである
  • なんか、全部3Dに入れ替えたんだっけかな
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない
  • 頑張ってそれっぽく見せるようにした思い出

KDDI検証

  • 俺はエクセルを書くためにゲーム会社に入社したわけではなかったんだ
  • ゲーム仕様をひたすらエクセル方眼紙に書き出し、検証資料を作り上げた
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない

実機実行の壁

  • 当時のガラケーはどいつもコイツも実機でプログラムを実行するのが大きな壁だった
  • dojaはネットワーク越しでしかインストールできないため、ゲーム開発のパケ代が自腹である
  • BREWは安っぽいケーブルを接続して転送できるが、実機を転送モードにできるのは正規コンテンツプロバイダーだけである
    • なので、実機で画像ペラ1枚出ただけでも「うううおおおおおおおおおおお!!!!!」みたいな感動があった

そして、2010年代

  • そんな状態だったので、 誰でも無償で開発できて、誰でもマーケットへ公開できる Androidに心惹かれたのは当然であり、きっとBREWに触ってなかったら別なことを思っていたかもしれない
  • Androidに注力した2010年以降は、手取り17万円からは想像もできないほど金も時間も心も豊かになった
  • BREW、君は僕の2010年代を豊かにする大きなターニングポイントの役割を果たしてくれたのかもしれない

  • 今はただ、安らかに眠れ

  • それとこいつら、KCP+のカレンダーAPIの不具合じゃね?

補足

  • 2010年1月現在の手取り、正確には17.6万円弱だった
    f:id:eaglesakura:20200102011815p:plain
    当時の給与明細

続 RecyclerView in FragmentContainerView in MotionLayoutでリストが描画されない問題

Androidバージョンの切り分け

  • これはAndroid 8.x以上で発生する
  • Android 7.xでは発生しない

問題点1, そもそもRecyclerViewのWidth/Heightが0dpである

  • デフォルトのvisibilityがGONEの場合、VISIBLEに切り替えてもレイアウトのサイズが再計算されない

ワークアラウンド

  • GONEではなく、INVISIBLEを使うことでレイアウトのサイズが計算される

問題点2, adapterをsetしてもinvalidateしてもコンテンツ数がいくつあっても再描画されない

  • メインの問題
  • notifyDatasetChangedとかinsertとかdeleteとか考えつく限りのイベント通知をしたけどダメ
  • setLayoutParamsとかしなおしたけどダメ
  • invalidate()とかしたけどダメ

ワークアラウンド

  • RecyclerViewには dispatchLayout() という描画開始を行うためのpackage privateメソッドがある
  • それがコールされないらしい
    • 描画効率のため、いろんな判定が入ってるけどどこかに不具合があるんかな?
  • リフレクション使って、adapterをセットした直後とか、強制描画したいタイミングでコールしてあげると描画される
// こんなふうにするといい
fun RecyclerView.forceDispatchLayout() {
    val method = RecyclerView::class.java.getDeclaredMethod("dispatchLayout")
    method.isAccessible = true
    method.invoke(this)
}

  • Androidバージョンが8.x以上で発生するので、結構クリティカルになってきてる
  • 頑張れJetpack

flutter buildで "Target kernel_snapshot failed" が出た場合の対処

エラー内容

flutter clean
flutter build aot
Wrong full snapshot version, expected '20e5c4f7dc44368ac5a17643b93665f6' found '8343f188ada07642f47c56e518f1307c'


Building AOT snapshot in release mode (android-arm-release)...         0.1s
Target kernel_snapshot failed: Exception: Errors during snapshot creation: null
Failed to build aot.

原因

  • flutterのキャッシュが壊れている
  • キャッシュを全部削除するか、flutterツール自体を再度インストールすれば直る
rm -rf path/to/flutter/bin/cache
flutter pub cache repair
flutter clean

遠因

  • channelをstable/beta/masterあたりで入替えすぎたかな?

2019年に作ったAndroid用ライブラリと、作った割に役立たなかったライブラリ

  • 2019年、自分用に色々作っては「まあええやん」「コレあかん」という感じで新しいものを試したり壊したりしてきた
  • ライブラリを作るのは個人的な趣味であり、いろんな設計を試せる娯楽であり、勉強でもある

armyknife-*

  • armyknifeシリーズ
  • 自分用の十徳ナイフ、ちょっとしたことを楽に書くために作ってる
  • だいたい拡張関数、ときどきObject
  • 拡張関数なので、ライブラリをリンクしたくないときはコピペでもだいたい動くようになってる
  • 作ったあとにKotlin標準/Jetpack標準で同等機能が生えたりして意味がなくなったりしてる
    • その時は気づいたタイミングでDeprecatedにしたり。

armyknife-runtime

  • Kotlin/Java 標準runtimeのみでだいたい動くライブラリ
  • github

よく使う機能

// 32文字のa-z, A-Z, 0-9で構成される乱数を作成する, UnitTestとかに。
Random.string(32)

// base64変換に。Java8, Java7(Android)両対応
ByteArray.encodeBase64()

// Channelの受け手にエラーを投げる。非同期処理でキャンセル以外の例外を投げたいときに。
Channel<E>.cancelByError(e: Throwable)

// Receiveするか、例外として処理する。非同期処理で例外以外のエラーを受け取りたいときに。
Channel<E>.receiveOrError()

armyknife-jetpack

  • Android Jetpack/Google Play Serviceの機能拡張ライブラリ
  • github
  • ほぼ全部のjetpackを参照しているが、利用側が強制リンクされないように細工してる
    • なので、依存関係は自分で解決することを前提にしている
    • 強制で CameraX とかリンクされてもやだし、excludeも面倒だし、全部ライブラリ分けても面倒、という理由

よく使う機能

// 実行中のRuntimeをチェックする
ApplicationRuntime.runIn(flags /*RUNTIME_ROBOLECTRIC とか*/) 

// Lifecycleが終わったらCoroutineをキャンセルする
// LifecycleOwnerに同等のExtensionが公式で生えたりしたので、チクチクと切り替えたりしてる
CoroutineContext.with(lifecycle: Lifecycle)

// LiveDataの変形
// MediatorLiveDataをラップしたもので、最大6個のLiveDataを集約して別なLiveDataを作ったりできる
// ViewModelでModelのLiveDataを受け取って表示用のLiveDataに変換したりするのに使う
LiveDataFactory.transform()
LiveDataFactory.transformNullable()
LiveDataFactory.transformInto()
LiveDataFactory.transformNullableInto()

// Activity/FragmentをLiveData<Context>に変形する
// onDestroyされたらnullを入れて殺す
// LiveDataFactory.transform()と組み合わせて、ViewModelの表示データ構築に利用
LiveDataFactory.contextFrom()

// ParcelableのオブジェクトをByteArray経由でインスタンスコピーする
// UnitTestで正しくコピーできることの検証にも使ったり。
Parcelable.deepCopy()

// Intentに強制的に独自Parcelableオブジェクトを挿入する
// AndroidのIntentは独自Parcelableを突っ込んでSelectorが出ると(対応Intent-Filterが複数あると)Intentを一旦OSがハンドリングする
// このとき、OS標準以外のparcelableが入っているとクラッシュするので、ソレを防止するために一旦ByteArrayに変換して突っ込む
Intent.putMarshalParcelableExtra(name: String, value: Parcelable?)
ntent.getMarshalParcelableExtra(name: String)

// Google Play Service APIを読んだときのTaskを待ち合わせる
// awaitだと名前が競合する
// awaitだとblockingするが、こっちはsuspendで、CoroutineがキャンセルされたらTaskもキャンセルして抜ける
Task<T>.awaitInCoroutines()
PendingResult<T>.awaitInCoroutines()

// ほかいろいろ

armyknife-gms

よく使う機能

// Classが見つからなかったらnullを返すアクセサ
// RobolectricではClassNotFoundExceptionが発生するので、それを防いでUnitTestしやすくしたりする
Firebase.app
Firebase.remoteConfig

// ライブラリモジュールのInstrumentation TestでFirebaseへの接続を実現する
// google-services.jsonをandroidTest/assetsに入れてパスを与えると、JSONをパースして強制的にFirebaseを初期化する
// これでUnitTestでもFirestoreとかにつないだりできる
Firebase.provideFromAssets()
Firebase.provideFromGoogleServiceJson()

// Firebaseの状態をLiveDataとして管理する
// RemoteConfigが更新されたり、
// FirebaseAuthの状態が変更されたり、
// トークンが変更されたりしたらLiveDataで通知される
// 変化を見る場合はRxKotlinのObservableとかに変換してる
// Debugモードの場合、Firebase Authのリフレッシュは5分、RemoteConfigは10分で自動リフレッシュする
FirebaseContext.getInstance().observe()

armyknife-android-junit4

  • Robolectric/Instrumentation TestのJUnit4動作を補助する
  • github
  • そろそろJUnit5に移行したいところ

よく使う機能

// src/test配下にあるけどInstrumentationにもUnitTestとしてとしてディレクトリが追加されてるとき、
// instrumentationだけ、robolectricだけ、両方という切り分けを簡単にする
// なおかつ、blockの中はsuspend関数で実行したい
// 自動的に ShadowLog.stream = System.out みたいな互換性チェックもしてる
instrumentationBlockingTest {}
localBlockingTest {}
compatibleBlockingTest {}

// トップレベルプロパティ
// コイツらのアクセスを補助
// InstrumentationRegistry.getInstrumentation().context
// InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
// InstrumentationRegistry.getInstrumentation().targetContext
val targetApplication
val targetContext
val testContext

// UnitTest用に正規ルートでViewModelインスタンスを作る
// Fragmentに紐付いたり、Activityに紐付いたり。
makeActivityViewModel()
makeFragmentViewModel()

// ViewModelのLiveDataをすべてactive状態にする
// MediatorLiveDataはactiveじゃないと変換処理が走らない仕様なので、UnitTest中はActiveであってほしい
// 本来はDataBindingによってActiveなので、UnitTestだけ必要
ViewModel.activeAllLiveDataForTest()

// ほかいろいろ

armyknife-android-bundle

  • Bundleに便利機能を追加する
  • github
  • 作った割に、「あ、あんまり使わないわ」みたいになった

よく使うと思って作った割に使わなかった機能

// BundleへのアクセスをKotlinのDelegate機能で実現する
// Bundleのラッパークラスを作ったときの補助にしようかとおもった
// 実際のところBundleを直接ラップするクラスを結構作ったのだが、この機能はあまり使っていない
// こんな感じで使う想定だったのだが、
// 「無理にdelegateするよりget()set()作ったほうが後々みやすいだろうか?」という悩みが垣間見える
// val foo: Int by bundle.delegateIntExtra("EXTRA_FOO", 0)
Bundle.delegateIntExtra()
Bundle.delegateStringExtra()

armyknife-reactivex

  • RxKotlinとChannel/Lifecycle管理用ブリッジを提供する
  • github
  • 結局の所、 firearm-event との組み合わせでしか使ってない

よく使う機能と思ったけど、用途が局所的

  • 省略

armyknife-jetpack-lifecycle

  • Lifecycleのなかで、たまに面倒と思う処理を提供する
  • github

まれによく使う機能

// LiveDataの中にLiveDataを抱えていて、
// LiveDataのなかのLiveDataをViewModelを使いたいとき
// 特にLiveDataのなかのLiveDataがMediatorLiveDataだと、ちゃんとactivateしないと値が更新されないので、それを回避したい
/**
 * class ExampleRepository {
 *      val url: LiveData<String> = ...
 * }
 *
 * val exampleRepository: LiveData<ExampleRepository>
 * val url: LiveData<String> = InternalLinkLiveData(exampleRepository, { it.url })
 */
InternalLinkLiveData<T>

armyknife-widgets

たまに役立つ

// DialogFragmentを使うほどのことじゃないけどDialogを出したくて
// Coroutinesの中で呼び出して次の処理をハンドリングしたい
// そんなときにDialogをsuspend関数として扱う
// Dialog表示中に画面回転しても気にしなくてもいいタイミングとかで使う
// Channelが返ってくるので、receive()するとユーザーの選択肢が取得できる
buildAlertDialogChannel(): Channel<DialogResult>

armyknife-jetpack-camera

  • CameraXをちょっと便利にする
  • github
  • CameraX自体がまだ不安定だったり、機能が増えていって不要になったりしてる

まれに使う

// Firebase ML Visionのanalyzerを使う
ImageAnalysis.setVisionAnalyzer

// BarcodeDetectorのanalyzerを使う
ImageAnalysis.setVisionBarcodeDetectorAnalyzer

firearm-*

firearm-channel

  • startActivityForResult/RuntimePermission/DialogをChannelとして利用する
  • github
  • annotation-processorが要らなかったり、ほかのAPT系と競合しない
  • Channelを使うので、suspend関数の中で終了待ちとか戻り値が取得できる
  • Channelの特性上、Activityが破棄されたりプロセスがシャットダウンされると死ぬ
    • 画面回転、low memoryにとにかく弱い
  • ご利用は計画的に

計画的に使う機能

val activityDispatcher = ActivityResultDispatcher.get(fragment)

suspend fun example() {
    // これでstartActivityForResultして、onActivityResult()の結果を受け取るまで待ち合わせる
    val result = activityDispatcher.startActivityForResultWithResult(intent)

    result.result // 戻り値をRESULT_OKかどうか確認したり
    result.data // 戻り値のIntentを見たり
    // なんかしたりする
}

firearm-event

  • ViewModelで発生したEventをpublishする
  • github
  • 内部はRxKotlinでできている
  • UnitTestするとき、Channelに変換して「この処理をしたらこのイベントが発生する」といったUnitTestを書ける
  • ViewModelで発生したイベントと、イベントに対するUI動作をFragmentやActivity等の外部で行いたいときに使う
    • ViewModel内でハンドリングするとしても、FragmentやActivityで発生イベントをハンドリングできたほうが便利な場合は多い
    • Notificationを出したり、別なFragmentやViewModelにアクションを行ったり

よく使う機能

// 発生するEventは最初にValidatorを定義する
// これは「なんのEventが来るか、後々仕様がわからなくなる」ことを防ぐために強制している
val EVENT_CLICK_DONE_BUTTON = EventId("EVENT_CLICK_DONE_BUTTON ")
val EVENT_CLICK_OK_BUTTON = EventId("EVENT_CLICK_OK_BUTTON")
data class OnDataLoadError(cause: Int): Event

val event = EventStream { event ->
    when(event) {
         EVENT_CLICK_DONE_BUTTON  -> true
         is OnDataLoadError-> true
         else -> false
    }
}

// DataBindingで、ボタンクリックでコイツが呼ばれると、イベントを発生させる
// event.subscribe()してるやつに画面遷移をまかせる
fun onClickDone() {
    event.next(EVENT_CLICK_DONE_BUTTON)
}

// ロードボタンを押したら非同期処理する
fun onClickLoad() {
   launch {
       // けど失敗してしまった
       // エラー表示はevent.subscribe()してるやつにまかせる
       event.next(OnDataLoadError(1))
   }
}

// OKボタンが押された
fun onClickOk() {
    // しかしValidatorで弾かれてアプリが落ちる
    // UnitTestでコレに気づくはずである
    event.next(EVENT_CLICK_OK_BUTTON)
}

firearm-di

  • Factory/BuilderパターンのDependency Injectionを行う
  • github
  • Kotlinは言語機能が強力だし、Dagger等のDIって実際のところ無理に使うほどでもないな、という個人的見解
    • 不安定だったり、ASバージョンアップで死んだり、色々あったから
  • UnitTestだけちょっと挙動を変更したりMockを返したい場合に使う

よく使う機能

object FooClassFactory {
     val provider = ProviderRegistry.newProvider<FooClass, Builder> { /* this = this@Builder */
         // Builderが渡されるので、オブジェクトを作って返す
         return Foo(this.context)
     }

     // Builderは引数の集約だけを行う。
     // 実際のオブジェクト生成はprovider()に行わせる
     class Builder(var context : Context) {
         fun build() : Foo = provider(this)
     }
}

// 通常はこうなる
val builder = FooClassFactory.Bulider(context)
val foo =  builder.build()
// UnitTestでちょっとFooを変更したり
// 固定オブジェクトを返したりしたい
FooClassFactory.provider.overwrite { /* this = this@Builder */
    FooForTest(this.context)
}

val builder = FooClassFactory.Bulider(context)
val foo =  builder.build() // FooForTestのインスタンスが返される

// けどテスト中に条件によっては本来のオブジェクトに戻したい
FooClassFactory.provider.reset()

val builder = FooClassFactory.Bulider(context)
val foo =  builder.build() // Factoryがもとに戻ったので、Fooのインスタンスが返される

firearm-workflow

  • firearm-channelがActivityの再生成やプロセス再起動に弱いため、それを克服するライブラリ
  • github
  • startActivityForResultやDialogFragmentやRuntime Permissionをプロセス再起動を前提に復旧可能にしている
  • FragmentやActivityにonActivityResult()のハンドラを書かなくてもいい
    • APTは使ってないので、他のAPT系ライブラリと共存できる
  • startActivityとかDialogFragmentを起動する際に、一時的に値を保存する機能がある
    • コレは一時的なイベントの途中のステートなので、メンバ変数に保存したくない
    • けれどstartActivityForResultのタイミングでプロセス再起動されていたら困る
    • 例えば。。。
      1. ユーザー一覧を開く
      2. ユーザーをクリックして別Activityでユーザー詳細が開いて、連絡先を選択させる
      3. 連絡先をIntentで受け取って、非同期でサーバーで処理させる
      4. DialogFragmentで「パターンA/Bどっちにする?」と聞く
      5. 処理前にRuntime Permissionで必要なパーミッションも取得する
      6. サーバーの結果をUIに反映する(ここまで一連の処理)
    • 画面を開いたり、一時的なステートがいっぱいある
      • firearm-channelとかでcoroutineに閉じ込められるけど、プロセス再起動とか画面回転とかしたら死ぬ
      • 一時的なイベント用ステートがいっぱいある
    • こんなとき、一時的なステートを保存する機能を提供したり、プロセス再起動してもちゃんとフローを継続できるようにする

面倒だけど使う機能

  • 詳細は省くが、色々機能はあるけど面倒だ
  • 結局の所Androidでは「プロセスやActivityが死ぬことを前提に一連の処理を作っていくのは面倒だ」という点に集約された
  • 詳細はUnitTestをみてくれ

firearm-experimental

  • 実験的な機能やアイディアをとりあえず試す
  • github
  • (ピコーン)これいいんじゃね!!で試してから冷静になるまでの間、このリポジトリに閉じ込める

割といい感じだった機能 / ViewModelSession

// ViewModelの所有者と現在のライフサイクルを明確にする
// 所有者がonDestroyされるとViewModleSessionがnullになる
// ViewModelに画面用のLifecycleが必要だったり、Contextが必要なときや
// 所有者が確定・不定になるタイミングで処理したいときに使う
// ViewModel自体のclear(再生成されないことが確定する)のとは違うライフサイクルをチェックできる
class ExampleViewModel : ViewModel() {
      val session = ViewModelSession<Fragment>()
      val context: LiveData<Context>
          get() = session.context

      private val observeSessionToken = Observer<ViewModelSession.Token<Fragment>> {
          val token = it ?: return
          token.launch(Dispatchers.IO) {
              // do initialize.
          }
      }
 }

 class ExampleFragment : Fragment() {
      val viewModel: ExampleViewModel by lazy {
          val viewModel: ExampleViewModel  = // ViewModelProvidersあたりから取得
          viewModel.session.refresh(this)  // 所有者を確定させる, onDestroyで自動的に開放される
      }
 }

冷静になってDeprecatedした機能 / SingleTask

// SingleTask.run {} でタスクを実行する
// SingleTask.run {}実行中に別な処理を SingleTask.run{} すると、後勝ちになるため、先の処理していたCoroutineがまるっとキャンセルされる
// 作った割には、ふつうにKotlinのSemaphore使ったほうがいいという結論に至る
SingleTask.run {
    // 排他処理したい機能
}

転生した機能 / LiveTaskCounter

// LiveDataでタスク実行数を管理するようにした機能
// 非同期処理が複数箇所あり、どこか1箇所でも処理していたらProgress UIを出したい、とかの場合に使う
// Kotlin標準にあるやつだとLiveDataではないので、LiveDataとして扱うとUIにリンクしやすい
val counter =  LiveTaskCounter()

// DataBindingで、Progress UIの表示可否を紐付ける
val progressVisibility: LiveData<Int> =
     LiveDataFactory.transform(counter) { snapshot->
         if(snapshot.count > 0) {
             // 何かしら非同期タスクを実行中なので、UIを表示
             View.VISIBLE
         } else {
             // なにも処理してないので、UIを非表示
             View.GONE
         }
     }

 suspend fun asyncWrite() {
       counter.withCount {
           // ブロックの中ではカウンタがインクリメントされる, 抜けたらデクリメント
       }
  }
 
 suspend fun asyncRead() {
      counter.withCount {
           // ブロックの中ではカウンタがインクリメントされる, 抜けたらデクリメント
      }
 }

こいつら使うとき

  • build.gradleにrepositoryを追加
allprojects {
    repositories {
        maven(url = "https://dl.bintray.com/eaglesakura/maven/")
    }
  • ライブラリをimplementation
"implementation"("com.eaglesakura.armyknife.{リポジトリ名}:{リポジトリ名}}:{バージョン名}")

// armyknife-jetpackの場合
// 全部 `v1.4.2` とかタグ打ってるので、そのmajor.minor.build を入れる
// bintrayとかも参照で
// https://bintray.com/eaglesakura/maven/armyknife-jetpack
"implementation"("com.eaglesakura.armyknife.armyknife-jetpack:armyknife-jetpack:1.4.2")

色々つくったな

  • 来年は何を作ろうか