自作のViewGroupをRobolectricでテストしつつ開発する その2
つづきです: akihito104.hatenablog.com
fragment-testingを使おう
前回の記事では空のActivityにカスタムビューをアタッチしてからテストしているが、androidxのfragment-testingというライブラリを使うのが楽そうというか正しそうなのでこちらを使いましょう。
やることは、前回の記事で空のActivityを作ってやっていることを空のFragmentに置き換えてやるだけ。
おまけ: Robolectric.buildAttributeSet()
レイアウトリソースでカスタムアトリビュートを指定したときの挙動をテストしたいということがある。ただ、テストでテスト用のレイアウトリソースファイルを読み込ませるのも一苦労なので、Viewに渡すAttributeSetのモックオブジェクトを作るのがいいらしい。Robolectricにはそのためのビルダーがあり、属性のIDと値のテキストをペアで渡す。
Robolectric.buildAttributeSet() .addAttribute(R.attr.foo, "bar") .addAttribute(R.attr.foo_color, "@color/foo_color") // レイアウトリソースに書く時のように指定する .build()
投稿する写真をカメラアプリで撮って送るついでにギャラリーアプリから見えるようにする(API Level 22+)
今回やりたいこと
- 投稿する画像をギャラリーから選ぶかカメラで撮るかして取得したいのでIntent Chooserを出したい
- いい機会なので
ActivityResultContract
を使いたい - tweet input: enable sending media · Issue #130 · akihito104/UdonRoad2 · GitHub
前提
- target SDK version: 29
- Android 10で動作を確認した
androidx.activity:activity-ktx:1.2.0-beta01
androidx.fragment:fragment-ktx:1.3.0-beta01
カメラアプリを起動するインテント
カメラアプリのインテントはActivityResultContracts.TakePicture
を使えばよい。
インテントを作るときに渡すUri
はFileProvider.getUriForFile()
を使って作る。content://
からはじまるUri
が取得できる。カメラアプリは撮影に成功してもファイルパスを返しては来ないのでこのUri
は保存しておく。FileProvider
の設定はリファレンスが詳しい。
今回はexternal-media-path
と、古いバージョン(API Level 21未満)のためにexternal-file-path
も指定した。また、authority
の名前にアプリケーションIDを使いたかったのでアプリモジュールで実装してDIで注入する形にした。
カメラアプリにファイルの書き込み権限を渡したい
リファレンスによると二つの方法があるらしい。
FileProvider | Android Developers
結論から言うと二つ目のInclude the Permission in an Intentの方法は、Intent Chooserを出したい場合にはうまく動かない様子。
したがって一つ目のGrant Permission to a Specific Packageの方法で実現することになる*1。ただし、権限を渡したいアプリのパッケージ名が必要なので何とかして入手しなければならない。API Level 22以上であればIntent.createChooser(Intent, CharSequence, IntentSender)
を使うことで、Intent Chooserで選択されたアプリがBroadcastReceiver
で取得できる。
API Level21以下で同じことをしなければならない時は、選択される可能性のあるカメラアプリをPackageManager.queryIntentActivities()
で取得し、それらすべてに対して許可するしかないと思われる。いずれの場合においても権限をrevokeしそびれることが無いよう注意する。
画像をギャラリーから見えるようにする
この記事を書いている現在、公式の Take photos | Android Developers に書いてあるIntent.ACTION_MEDIA_SCANNER_SCAN_FILE
を投げる方法は非推奨になっている。なので、今回はMediaScannerConnection.scanFile()
を使った。これだとContext
が必要なのでActivityResultContract.parseResult()
とかの中でちょろっとやってしまうのは難しいかもしれない。また、これに渡すファイルパスはcontent://の形式のものではなく、FileProvider.getUriForFile()
に渡したFileのパスを渡す。
このほかにも、カスタムのContractを作ったらワケあってDI(Dagger)でFragmentにContractを注入することになり、registerForActivityResult()
をonAttach()
のなか(というかAndroidSupportInjection.inject()
のあと)で呼ぶ羽目になったとかいろいろ細かいことはあるが、細かいのでこの辺にしておく。
*1:StackOverflowの回答ではqueryIntentActivities()をやって明示的なIntentにして解決しているがこっちの方法では試していない。こっちでうまくいくならその方がいいかも。
fluxとかMVIみたいな構造のアプリを作ってみたかった その4
前の記事: akihito104.hatenablog.com
このツイートに関連して困ったことが起きている。
layout inspectorを起動するとActivityが再生成されるのかいろいろバグってしまうしそれによってViewの様子も変わってしまうので結局何がしたかったのかわからないという感じになってしまう
— まつだあきひと (@akihito104) 2020年10月26日
以前載せたクラス図の左下にNavigationDelegateというクラスがある。
このNavigationDelegateはandroidxのNavControllerを持っており、ViewStatesで行った処理の結果を受けて画面遷移を行う。また、NavControllerのOnDestinationChangedListenerを介して現在のNavHostFragmentの変更通知を受け取り、これを監視可能な形でViewStatesに提供している。
NavigationDelegateはDIからActivityをコンストラクタインジェクションされ、必要になった時にActivityからNavControllerを取り出している。クラス図を見てわかる通り、NavigationDelegateは間接的にViewModelの持ち物になっている。これが問題で、Activityの再生成が起きるとき、NavigationDelegateが弱参照で持っているActivityは無くなって参照できなくなるのでNavigationDelegateごと作り変える必要があるのだが、ViewModelはActivityのライフサイクルを超えて再利用されるのでNavigationDelegateの再生成は行われない。
思いつく解決策としては次の三つ。
- ViewModelをやめる: 根本的な解決策だがすべてのデータ管理を確認する必要がありちょっと大変。
- ApplicationLivecycleCallbacks経由で新しいActivityを受け取れるようにする: Activityを注入する代わりにApplicationLivecycleCallbacksを実装しているクラスを注入して常に最新のActivityインスタンスを参照できるようにする。既存のコードはあんまり変えなくてよさそうだけどうまくいくのかよくわからん。
- NavigationDelegateはActivityに持たせて、ViewStatesから発行されたナビゲーションに関するイベントを待ち受ける。NavHostの状態はRepositoryとかに適当に突っ込む。よう知らんけどAndroidアプリらしいような気がする。
2020/11/01 解決策についていくらか追記した。