eaglesakuraの技術ブログ

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

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")

色々つくったな

  • 来年は何を作ろうか