この記事はKotlin Advent Calendar 2020の4日目の記事です。
Androidアプリ開発初心者ですが、空きがあったので勢いだけで参加してみました。
はじめに
Navigation Architecture Componentを使ってアプリを作成した際、
素早く操作した場合や戻るボタンの連打などで、NavControllerと表示されているFragmentがズレてしまい、エラー(IllegalArgumentException
)が頻発しました。
これからライブラリ自体のアップデートで解消されていく可能性はありますが、現時点では、自前で対策する必要があります。
普通の画面遷移
findNavController().navigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
findNavController().popBackStack()
// または
findNavController().popBackStack(R.id.MainFragment, false)
特にpopBackStack()を使った場合は、連打するとNavController内で管理されているBackStackがズレやすくなります。(戻りすぎる)
対策済み
Extensionを利用しFragmentに画面遷移用のメソッドを生やします。
(画面遷移前にヒストリとの整合性確認を行う処理を追加しています)
package com.sample.extensions
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.navigation.*
import androidx.navigation.fragment.DialogFragmentNavigator
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.findNavController
/**
* ==============================================
* Fragment Extensions
* ==============================================
*/
/**
* 現在のFragmentがヒストリの最新と一致しているか
*/
fun Fragment.isCurrentDestination(): Boolean {
// ヒストリから現在のFragmentの情報を取得
val currentDestination: NavDestination = findNavController().currentDestination ?: return false
when (currentDestination) {
is DialogFragmentNavigator.Destination -> {
if (currentDestination.className != this.javaClass.name) {
// ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
return false
}
return true
}
is FragmentNavigator.Destination -> {
if (currentDestination.className != this.javaClass.name) {
// ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
return false
}
return true
}
else -> {
return false
}
}
}
/**
* safeNavigate
*/
fun Fragment.safeNavigate(
resId: Int,
args: Bundle? = null,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
if (!isCurrentDestination()) {
return
}
findNavController().apply {
navigate(resId, args, navOptions, navigatorExtras)
}
}
/**
* safeNavigate
*/
fun Fragment.safeNavigate(
directions: NavDirections,
navOptions: NavOptions? = null
) {
safeNavigate(directions.actionId, directions.arguments, navOptions)
}
/**
* safePopBackStack
*/
fun Fragment.safePopBackStack() {
if (!isCurrentDestination()) {
return
}
forcePopBackStack()
}
/**
* forcePopBackStack
* note:ヒストリと現在表示されているFragmentの一致チェックなしで戻る処理を行う
* DialogFragmentを表示元のFragmentから非表示にする場合などの利用を想定
*/
fun Fragment.forcePopBackStack() {
findNavController().apply {
val navBackStackEntry : NavBackStackEntry = previousBackStackEntry ?: return
// ヒストリの1つ前の画面に戻る
// popBackStack()だと、連打により2つ前の画面まで戻ってしまいヒストリがおかしくなることがある
popBackStack(navBackStackEntry.destination.id, false)
}
}
/**
* safePopBackStack
*/
fun Fragment.safePopBackStack(
destinationId: Int,
inclusive: Boolean
) {
if (!isCurrentDestination()) {
return
}
findNavController().apply {
popBackStack(destinationId, inclusive)
}
}
safeNavigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
safeNavigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
safePopBackStack()
// または
safePopBackStack(R.id.MainFragment, false)
さいごに
NavigationComponentでのIllegalArgumentException
を検索してみると、
ボタン連打防止処理や、navigate
前のヒストリとの整合性確認がよく出てきますが、
端末の戻るボタンでNavControllerを操作(popBackStack)している場合は、
popBackStackにも対策を行う必要がありました