eaglesakuraの技術ブログ

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

Android定型コードをスッキリさせるための Wofkflow Dispatcher ライブラリ 1.0.0を公開

Wofkflow Dispatcherとは

GitHub - eaglesakura/workflow-dispatcher

  • Androidアプリ開発でよくある 画面やActivityやプロセスを跨ぐ可能性のある非同期処理 をなるべく簡単に扱うためのライブラリ
  • Annotation Processorを使って、定形処理を出力する
    • startActivityForResult/onActivityResultの分岐を書く必要がなくなる
    • Runtime Permissionの処理を書く必要が無くなる
    • DialogFragmentのコールバックに気を使う必要が無くなる
    • 一時的な情報をメンバ変数ではなく引数として扱うことができる
      • 個人的にコレが重要
    • Activity再生成(回転とかLowMemoryとか)しても大丈夫
  • 初めてAnnotation Processorを実装した
    • 以前は否定的だったけど、十分にマシンスペックが上がったので心の中で解禁

似たライブラリ

使うとどう書けるのか

  • 普通に書けるライブラリはいろいろあるが、例えば onActivityResult に戻ってきたときに一時変数の値を使いたい場合は結構気を使う
  • このサンプルでは、ブラウザを開いた時刻を保存しておき、onActivityResultでToast表示に使っている
  • AndroidではActivityやFragmentが再生成される(新しいインスタンスが作られる)場合があるため、save/resotreには気を使う
    • 別Activityで開く -> Runtime Permissionで権限を得る -> ダイアログでYES/NOを質問するとかを順番にやると一時変数が結構増えていく
  • ライブラリが自動的に一時情報を保存・レストアして、コールバック時に渡してくれるのでActivityとかFragmentをクリーンに保てる

Before

class ExampleBeforeActivity : AppCompatActivity() {

    private var tempDialogStartDate: Date? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if(savedInstanceState == null) {
            tempDialogStartDate = startDate
            startActivityForResult(
                Intent(
                    Intent.ACTION_VIEW,
                    Uri.parse("https://google.com")
                ), REQUEST_SHOW_BROWSER
            )
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putSerializable("startDate", tempDialogStartDate)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        tempDialogStartDate = savedInstanceState.getSerializable("startDate") as? Date
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            REQUEST_SHOW_BROWSER -> {
                // read temporary data, clear temporary
                val startDate = tempDialogStartDate!!
                tempDialogStartDate = null

                // show toast
                Toast.makeText(this, "done workflow, startDate='$startDate'", Toast.LENGTH_SHORT)
                    .show()
            }
            else -> super.onActivityResult(requestCode, resultCode, data)
        }
    }

    companion object {
        const val REQUEST_SHOW_BROWSER = 0x0011
    }
}

After

class ExampleAfterActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if(savedInstanceState == null) {
            // show browser
            // ExampleAfterActivity.showWebsite() is generated by kapt(workflow-dispatcher-processor).
            // auto save/restore/clear `startDate` state.
            showWebsite(
                Intent(Intent.ACTION_VIEW,Uri.parse("https://google.com")),
                startDate = Date()
            )
        }
    }

    @OnActivityResultFlow("showWebsite")
    fun onShowWebsiteResult(result: ActivityResult, startDate: Date) {
        // show toast
        Toast.makeText(this, "startDate='$startDate'", Toast.LENGTH_SHORT).show()
    }
}

どうやって実現しているのか

  • startActivityとかするタイミングで、ライブラリが WorkflowProviderFragment をchildFragmentManagerに差し込む
  • WorkflowProviderFragmentの中でstartActivityとかpermissionとか処理をして、結果をコールバックしている
  • 一時データの保存もWorkflowProviderFragmentの中に持っているViewModelに持たせているので、呼び出し側を汚染しない

保存できるステート

  • 生成されるコードを見るとわかるが、内部的にはBundleに突っ込んでいるので、Bundleに保存可能な値であれば @OnActivityResultFlow の引数として受け取ることができる
  • ステートが不要な場合は result: ActivityResult とか第1引数だけでメソッドを宣言すれば良い

使い続けられるのか?

  • Jetpack Navigation ライブラリが同じ機能をサポートしたらコイツは役目を終える
  • 公式、ガンバ・・・