Androidはワンツーパンチ 三歩進んで二歩下がる

プログラミングやどうでもいい話

【AndroidX】Jetpack のNavigation で戻るボタンの処理をカスタマイズする(Backキーのイベントを拾う, OnBackPressedDispatcher, OnBackPressedCallback, ToolBarの戻るボタン)

AndroidJetpackのNavigationコンポーネントを使って画面遷移をハンドリングしているアプリで、戻るボタンが押された場合の処理をカスタマイズする必要が出た時に、最近出た新しい方法で戻るボタンの処理を書きました。
その時のメモです。
Navigationコンポーネント内の機能というわけではないので、Navigationを使っていないアプリでもonBackPressedメソッドをoverrideする代わりにその方法を使えます。
おまけでActionBarのToolBarでも戻る矢印のボタンを使っている場合のハンドリングについても書いています。

公式サイト

実行した環境

  • Android Studio 3.5
  • com.android.tools.build:gradle:3.5.0
  • Kotlin 1.3.50
  • androidx.appcompat:appcompat:1.1.0-rc01
  • androidx.navigation:navigation-fragment-ktx:2.2.0-alpha02
  • androidx.navigation:navigation-ui-ktx:2.2.0-alpha02

参考サイト様

何がやりたいのか

JetpackNavigation componentを使って戻るボタンの処理をNavigationに任せている場合でも、カスタマイズしたい時があります。

例えば何らかの処理を行わないでユーザーが画面を離脱しようとした時に、警告ダイアログを表示するなどが考えられます。

古くからある戻るボタンのハンドリング方法は、Activityクラスの中でonBackPressedメソッドをoverrideして、その中に処理を記述していましたが、個々のFragmentから見ると親のActivityに処理を任せなくてはならず使い勝手がよくありませんでした。

Jetpackで登場したComponentActivityOnBackPressedDispatcherを使うと、戻るボタンのイベントを受け取って処理を記述できるようになります。
Activityにコードを書かずに済むし、有効と無効の切り替えも簡単に出来ます。

onBackPressedメソッドのオーバーライドの代わりにこれを使って処理を書いてみます。

注意事項

  • AndroidX 移行済みの前提です。
  • Jetpackは進化が早いので、この記事も古くなっているかも知れません。参考にされる場合はご了承ください。
  • この記事では実装はKotlinのみを使用しています。Javaでの実装は扱っておりません。

必要なライブラリ

AndroidXのActivity が必要となります。
2019年4月25日にリリースされたandroidx.activity:activity:1.0.0-alpha07で大幅にアップデートされたバックボタンのハンドリングの機能を使います。

その後も変更やメソッド削除等があるので、androidx.activity:activity:1.0.0androidx.activity:activity-ktx:1.0.0の安定版以降を使うのがよいと思います。

公式サイトのAndroidXのActivityの導入ガイドはこちらです。
Activity  |  Android Developers

依存ライブラリのバージョンの確認

少し話がずれますが、androidx.activity:activityのライブラリは、
androidx.appcompat:appcompatandroidx.navigation:navigationからも依存されています。

そのためわざわざbuild.gradleのdependenciesに記述しなくてもandroidx.activity:activityのパッケージを使用出来ます。
ですが、少し古いリリースのものだとまだ実装されていないかもしれないので、一応バージョンの確認をしておくといいでしょう。

依存関係の確認方法の一例として、下記のコマンドをプロジェクトのルート配下で実行すると、depends.txtというテキストファイルが出来ます。
これを開いてバージョンを確認します。

$ ./gradlew app:dependencies > depends.txt

次の画像は手元で実行した時のスクリーンショットです。

f:id:sakura_bird1:20190919123514p:plain:w500

androidx.activityで検索すると、この例ではandroidx.activity:activity:1.1.0-alpha03 (*)となっています。
1.0.0以降ですのでバージョンはOKです。

色々なライブラリがandroidx.activityを参照していてバージョンが異なったとしても、Gradleの依存関係のルールでは参照されてる中で最も最新のものが全てに強制的に適用されます。(カスタマイズも可能)
Gradleの依存関係のルールについては公式サイトをご覧ください。

戻るボタンの処理を記述する

OnBackPressedDispatcherにコールバックを追加する

公式サイトより引用します。

class MyFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // This callback will only be called when MyFragment is at least Started.
        val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
            // Handle the back button event
        }

        // The callback can be enabled or disabled here or in the lambda
    }
    ...
}

説明(ほぼ公式サイトより)

FragmentActivityAppCompatActivityの継承元クラスであるComponentActivityでは、getOnBackPressedDispatcher()を呼び出して取得できるOnBackPressedDispatcherを使用して戻るボタンのハンドリングを行うことが出来ます。

OnBackPressedDispatcheraddCallback()メソッドを呼び出されるとOnBackPressedCallback オブジェクトを返してhandleOnBackPressed()メソッド内で戻るボタンのイベントをキャッチした時の処理を書くことが出来ます。

コールバックの引数にはLifecycleOwnerを渡します。
OnBackPressedCallbackLifecycleOwnerの状態がLifecycle.State.STARTEDになってから追加されます。

複数のコールバックを登録できます。
登録された順の逆の順序で有効なコールバックが呼び出されます。

コールバックの有効と無効を切り替える

isEnabled()メソッドで切り替えます。デフォルトでtrueになっています。

callback.isEnabled = false

コールバックはChain of Responsibility パターンに従っています。

コールバックの有効と無効を切り替えることは、呼び出し順序を維持するためにも一時的な変更にすることがおすすめです。
特に複数のネストされたLifecycleOwnerにコールバックが追加されている場合は特に重要です。

OnBackPressedCallbackを削除する

OnBackPressedCallbackを完全に削除する場合は、remove()メソッドを呼び出す必要があります。
ただし、コールバックは関連付けられているLifecycleOwnerが 破棄されると自動的に削除されるため、通常は必要ありません。

ActivityのonBackPressedメソッド

ActivityonBackPressedメソッドをoverrideする方法をとっている場合は、
OnBackPressedCallbackを代わりに使うのがおすすめです。
この変更を行うことができない場合は、次の規則が適用されます。

  • addCallbackで登録されたコールバックは、super.onBackPressed()を呼び出した時に評価される
  • onBackPressed()はOnBackPressedCallbackのインスタンスに関係なく常に呼び出される。

Navigationで一つ前の画面に戻る

Navigationのバックスタックを一つ遡るには、NavController.popBackStack()メソッドを使います。
特定の画面に遷移したい場合は公式サイトをご覧ください。

        requireActivity().onBackPressedDispatcher.addCallback(this@MyFragment) {
            // Handle the back button event
            ...

            findNavController().popBackStack()
        }

ActionBarのUpボタンの処理(おまけ)

NavigationUI.setupActionBarWithNavController()を使用してNavigationコンポーネントとアクションバーの挙動を結びつけると、NavigationがToolBarの領域に戻る矢印のUpボタンなど状況に応じて適切なナビゲーションを表示してくれるようになります。 (レイアウト内のfragmentタグで「app:defaultNavHost="true"」となっている前提です)

f:id:sakura_bird1:20190920223516p:plain:w300

参考コード

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // レイアウト内のfragmentタグで「app:defaultNavHost="true"」となっている前提

        setContentView(main_activity)

        val host: NavHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
        val navController = host.navController

        setSupportActionBar(binding.toolbar)

        val appBarConfiguration = AppBarConfiguration(navController.graph)

        setupActionBarWithNavController(navController, appBarConfiguration)
    }

    override fun onSupportNavigateUp(): Boolean =
        findNavController(R.id.nav_host_fragment).navigateUp()
}

Upボタンのイベントを取得して処理をカスタマイズする場合は
Fragment内でonOptionsItemSelectedメソッドをオーバーライドしてMenuItemのidが android.R.id.homeの時にUpボタンが押された時の処理を記述します。

class MyFragment : Fragment() {

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                // Upボタンが押された時の処理

                // falseを指定すると、Upボタンの処理が続行されて前の画面に戻る
                // trueを指定した時は自分でfindNavController().popBackStack()などを実行して前の画面に戻る
                false
            }
            R.id.action_share -> {
                // 何らかの処理
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}