最近、FragmentTransactionからNavigationへ移行しました。
今回の記事では、基本的なNavigationの実装方法などは他にも記事がたくさんあるのでそこは省いています。
基本的な実装というよりは、ここでは設計時に考慮すべきだった事や実装時に気をつけないとクラッシュが起きる所の話などを中心にしています。
Navigationに移行作業中の方や、これからNavigationの導入を検討している方など、誰かの参考になれば嬉しいです。
1. Action と Global Action
デスティネーション間の経路を視覚的に表示する Action についてのTipsです。
まず基本的な話ですが、Navigationでは、xmlで書かれたナビゲーショングラフを使用してアプリのナビゲーション(画面遷移)を管理します。
例えば、Fragmentが3つだけあるアプリのナビゲーショングラフはこんな感じ。
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_navigation"
app:startDestination="@id/first_fragment">
<fragment
android:id="@+id/first_fragment"
android:name="com.example.sample.FirstFragment"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_first_fragment_to_second_fragment"
app:destination="@id/second_fragment" />
</fragment>
<fragment
android:id="@+id/second_fragment"
android:name="com.example.sample.SecondFragment"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_second_fragment_to_third_fragment"
app:destination="@id/third_fragment" />
</fragment>
<fragment
android:id="@+id/third_fragment"
android:name="com.example.sample.ThirdFragment"
tools:layout="@layout/fragment_third">
</fragment>
</navigation>
この例だと、First → Second → Third というシンプルな流れで、この各経路がActionで定義されています。経路が簡潔でわかりやすいですね。
次に、画面を2つ増やして、さらにどの画面からも最初の画面(FirstFragment)に遷移する経路を作るとなったとき、単純にactionを追加するとこんなグラフになります。
ちょっと線が絡み合って見にくくなりましたね。
まだFragmentが5個だけで経路も多くないですが、今後Fragmentの数が20, 40, 60,,,と増えると、どことどこが繋がっているのか追いにくくなってしまうのが想像して頂けるかと思います。
で!こんなときは、Global Actionを使ってやればいいです。
設定は、こんな風にナビゲーショングラフ上のGUIから簡単にできます。
FirestFragmentの左側に付いている、→ がGlobalActionです。
線が減ってすっきりしましたー!
このように複数の画面から遷移される画面(たとえばアプリのホーム画面とか)への経路は、Global Action
で定義するとグラフの線が絡み合わずにすっきり管理することができます。xmlの定義も少なく済むし。
ただちょっと注意点が、このGlobal Action
が便利すぎるところ。
便利で何が悪いねんって言われそうですが、
極端な例としてあげると、
全経路をGlobal Action
で定義したグラフがこれです
(もはやグラフになってない)
まあこんなことするケースは中々ないとは思いますが、全部Global Actonで定義しても動作的には問題はないって事を言いたかった。ただし、Fragment間に線が表示されないので視覚的に経路が追えなくなりますね。
私は、ラクしたくて気づいたらGlobalActionを結構使っていたので、実装途中で「これでいいのか?」って少し迷ったりしました。
アプリの規模やユースケースによるので正解はないと思いますが、Global Action
と通常のAction
をどう使い分けるかは、ナビゲーショングラフの設計時に考慮しましょう。
2. Navigationに add Fragmentはない!!
Navigationには、add Fragmentが存在しないので、FragmentTransaction
でadd
を使用している方は注意が必要ですよっていうお話です。
Navigationでは、replaceで遷移をすることが前提なんですって。
そのため、addを使っていた所はreplaceされても問題ないようにFragment側の実装を修正しました。
ただしどうしてもadd
の挙動にしたい場合は以下のような対応が選択肢としてあります。参考までに。
- データを
ViewModel
に保存してonViewCreated()
でチェックする -
<Dialog>
を使用して全画面DialogFragmentsを使用する
3. safeArgsで意外に渡せなかった引数の型
safeArgsに関するTipsです。
Navigationの便利機能の1つであり、安全に引数の引き渡しが可能になるsafeAegs。このsafeArgsで、意外に引き渡す事ができない型がいくつかありました。
そのうちの1つで個人的に意外だったのがList
。
ちなみに、double
, short
, byte
, char
もビルド時にエラーになって使えない。
List
のように、そのままだと渡せない場合は、@Parcelize
と組み合わせて data classを作成してそれを引き渡すか、bundle
で渡す方法がある。
サポートされている引数の型は、デスティネーション間でデータを渡すにまとまっているので、new Instanceで引き渡していた所をSafeArgsにしたいと考えている場合はご一読しとくのがおすすめ。
4.java.lang.RuntimeException: Unable to start activity
各FragmentでNavControllerを使用するときに気をつけたいところです。
結論から言うと、Fragment のonCreate()
でNavController
を取得する実装をしているのがよくなかった。
これがよくない例。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navController = findNavController()
}
このままだと通常起動時は問題ないですが、Activityが再生成されたときに以下のクラッシュが発生します。
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.sample/com.example.sample.MainActivity}: android.view.InflateException: Binary XML file line #9 in com.example.sample:layout/activity_main: Binary XML file line #9 in com.example.sample:layout/activity_main: Error inflating class fragment
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
ログは、まだ親のActivityが生成されていないのにFragment側でNavControllerを取得しようとするからクラッシュが発生してしまってるよ
と言ってます。
対処としては、Kotlinの場合は、lazyで遅延初期化するよう実装すれば解決します。
private val navController by lazy { findNavController() }
Javaの場合、便利なlazyはないので、DaggerやDIする方法がある。
うちではKotlin化を進めているのでこの機会にまだJavaで書かれていたFragmentは全てKotlin化しました。
5. java.lang.IllegalArgumentException: navigation destination XXX is unknown to this NavController
各Fragmentでナビゲーションの処理を書くときに気をつけたいところです。
私の場合は、リリース後にクラッシュが発生して少し困ったので、今後Navigationに移行する人には同じ轍を踏んでほしくない。
そのクラッシュの例がこれ
(FirstFragmentでnextボタンを連打してSecondFragmentに遷移しようとしたときのクラッシュ)
java.lang.IllegalArgumentException: navigation destination com.example.sample:id/action_first_fragment_to_second_fragment is unknown to this NavController
at androidx.navigation.NavController.navigate(NavController.java:789)
at androidx.navigation.NavController.navigate(NavController.java:730)
at androidx.navigation.NavController.navigate(NavController.java:716)
at androidx.navigation.NavController.navigate(NavController.java:704)
at com.example.sample.FirstFragment$onViewCreated$1.onClick(FirstFragment.kt:27)
ログは、現在のNavControllerは、action_first_fragment_to_second_fragmentなんて知らないですよ。
と言っていますね。
発生原因として考えられるもの
- 連打
- 複数のナビゲーションの同時押し
- etc.
つまり、FirstFragmentからSecondFragmentへの遷移で発生したクラッシュの一連の流れはこんな感じ。
1. FirstFragmentからSecondFragmentへ遷移する経路を高速連打
2. SecondFragmentに遷移しているのに、action_first_fragment_to_second_fragmentを実行しようとする
3. SecondFragmentのNavControllerはそんなDestinationは知らないのでエラーを出す
この対処として役に立つのがcurrentDestinationチェックです。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
next_button?.setOnClickListener {
if (navController.currentDestination?.id == R.id.first_fragment) {
navController.navigate(R.id.action_first_fragment_to_second_fragment)
}
}
}
今のところ手元で再現できているのが、アニメーション付きで遷移する経路での連打や同時押しですが、アニメーションなしの経路でも同様のクラッシュは発生するようです。
そのため、このcurrentDestinationをチェックする処理はナビゲーションの前に必ず入れた方がよさそうな所感が今のところありますが。もっと良い方法などがあればぜひ教えてください。
おわり
最後まで読んでいただきありがとうございました。