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だけが割り振られるので、ローカルマシンからの開発がやりづらそう
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
ヘッダをチェックし、香川付近の位置であれば弾くのも良い - コレであればコストは最低限になるのではないだろうか
ちゃんと対応するなら?
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とは
- 最近ではSnapdragonで有名なクアルコムがかつて開発していたOS
- 日本人的には auの KCP/KCP+ だ
- ガラケー全盛のauを支えようとしていた、かつての
クソOSだ - 2010年代が終わり、記憶が消えてしまう前に、彼について記録と記憶を留めておこうと思う
BREWとの出会い
- 新卒で入社し、手取り17万円で働いていた会社で、 Javaに似たへんな言語 で作られたゲームを、BREWへ移植する仕事が降ってきた
- ゲームの内容自体は面白かったが、ソレは別な話だ
- 当時docomoユーザーだった俺は、BREWやKCP/KCP+なんて全く知らない
- なので、手探りで開発をスタートすることとなった
- 会社的には、前任者のコードがサンプルとして存在していた
- 2010年前後の、お話である
BREWのざっくりとした開発環境
- Windowsで開発環境が構築できる
- Windowsでは
x86環境で動作するシミュレータ
が提供されている- これが後に悲劇を起こす
- 会社の公式開発環境はVisual C++ 6.0だった
- どういうわけか、会社には
無償で使えるインストーラ
が社内サーバーにあった - 新卒で入り、毎月17万円(手取り)ものお賃金をいただき、キラキラしていた俺は、ソレに目をつむった
- Visual Studio 2008(無償版)を途中から使い始めた
- どういうわけか、会社には
- 実機動作時はARM環境向けにクロスコンパイルを行う必要がある
- 開発言語は公式にはC言語のサブセット、コンパイラはC/C++対応されていた
- これが後に悲劇を起こす
- ハードウェアの仕様上、float演算が行えないことが再三警告されていた
- これが後に悲劇を起こす
- もとのゲームはJavaにプリプロセッサを適用するような当時のガラケーらしいJavaのような言語で開発されていた
- これが後に悲劇を起こす
- 詳細な公式ドキュメントは手に入らない
- これが後に悲劇を起こす
- アプリの容量(プログラムと、ゲームリソースの容量合計)が1.5MB前後
- これが後に悲劇を起こす
- アプリをリリースするためには、KDDIのストア審査が必須である
- これが後に悲劇を起こす
起こった悲劇たち
アプリ容量天元突破
- この問題にぶち当たるのは2度めである
- 1度目は入社直後dojaでやらかしている
- もともとの容量が大きいゲームだったため、プログラムもしくはグラフィックリソースを大幅に簡略化しなければならない
- が、そんな開発リソースはない
- そんなときに見つけたのが当時の2chの戦士たちである
BREWはメモリ保護がない
(ので、下手なメモリにアクセスするとOSごとクラッシュもまれによくある) ことを生かして、- というワンダフルな手法が紹介されていたので、それを参考にさせてもらった
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問題
方法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
シミュレータ
に対しては言いたいことあるが、まあコイツほどクソ独特じゃないのでとても快適である
- なので、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年代を豊かにする大きなターニングポイントの役割を果たしてくれたのかもしれない
今はただ、安らかに眠れ
補足
- 2010年1月現在の手取り、正確には17.6万円弱だった
続 RecyclerView in FragmentContainerView in MotionLayoutでリストが描画されない問題
Androidバージョンの切り分け
問題点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) }
謎
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
よく使う機能
// 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
- Google Play Service拡張ライブラリ
- github
よく使う機能
// 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のタイミングでプロセス再起動されていたら困る
- 例えば。。。
- ユーザー一覧を開く
- ユーザーをクリックして別Activityでユーザー詳細が開いて、連絡先を選択させる
- 連絡先をIntentで受け取って、非同期でサーバーで処理させる
- DialogFragmentで「パターンA/Bどっちにする?」と聞く
- 処理前にRuntime Permissionで必要なパーミッションも取得する
- サーバーの結果をUIに反映する(ここまで一連の処理)
- 画面を開いたり、一時的なステートがいっぱいある
- firearm-channelとかでcoroutineに閉じ込められるけど、プロセス再起動とか画面回転とかしたら死ぬ
- 一時的なイベント用ステートがいっぱいある
- こんなとき、一時的なステートを保存する機能を提供したり、プロセス再起動してもちゃんとフローを継続できるようにする
面倒だけど使う機能
- 詳細は省くが、色々機能はあるけど面倒だ
- 結局の所Androidでは「プロセスやActivityが死ぬことを前提に一連の処理を作っていくのは面倒だ」という点に集約された
- 詳細はUnitTestをみてくれ
firearm-experimental
割といい感じだった機能 / 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")
色々つくったな
- 来年は何を作ろうか