［Android］Navigation Component で StartDestination を popBackStack したときの動作

# はじめに

Navigation Component で StartDestination を popBackStack すると currentDestination が null になると以下に記載がある。currentDestination が null になったときの挙動について詳しくは書かれてはいなかったので調べてまとめようと思います。(本記事は Navigation Component v2.3.1 で動作確認しています。)

```
https://developer.android.com/guide/navigation/navigation-navigate?hl=ja#back-stack

このメソッドが false を返すと、NavController.getCurrentDestination() は null を返します。
新しいデスティネーションに移動するか、アクティビティに対して finish() を呼び出してポップを処理する必要があります。
```

# 検証環境を構築する

次のような FirstFragment と SecondFragment 間で遷移できるアプリを構築して検証できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/fab7beab-4497-fe98-fde4-b4b8dab95270.png)

## build.gradle

Navigation Component を dependencies に追加して使えるようにします。

```
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
```

## MainActivity

次のような Navigation Graph を作成して、MainActivity の <fragment> にセットしてやります。

  
**nav_graph.xml**

```xml
<?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/nav_graph"
    app:startDestination="@id/FirstFragment">

    <fragment
        android:id="@+id/FirstFragment"
        android:name="jp.kaleidot725.sample.FirstFragment"
        android:label="@string/first_fragment_label"
        tools:layout="@layout/fragment_first">

        <action
            android:id="@+id/action_FirstFragment_to_SecondFragment"
            app:destination="@id/SecondFragment" />
    </fragment>
    <fragment
        android:id="@+id/SecondFragment"
        android:name="jp.kaleidot725.sample.SecondFragment"
        android:label="@string/second_fragment_label"
        tools:layout="@layout/fragment_second" />
</navigation>
```
  
**MainActivity**
  
```kotlin
class MainActivity : AppCompatActivity(R.layout.activity_main)
```

**activity_main.xml**

```xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment_content_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/nav_graph" />
</FrameLayout>
```


## FirstFragment

あとは表示する FirstFragment には次の View を配置して画面遷移を操作できるようにしておく。

- 遷移状態を表す TextView
- SecondFragmnt に navigate するための Button
- FirstFragment を popBackStack するための Button

**FirstFragment**
  
```kotlin
class FirstFragment : Fragment(R.layout.fragment_first) {
    private val navController: NavController get() = findNavController()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val textView = view.findViewById<TextView>(R.id.textview_first)
        textView.text = navController.currentDestination?.label ?: "NULL"

        val nextButton = view.findViewById<Button>(R.id.button_next)
        nextButton.setOnClickListener {
            navController.navigate(R.id.action_FirstFragment_to_SecondFragment)
            textView.text = navController.currentDestination?.label ?: "NULL"
        }

        val backButton = view.findViewById<Button>(R.id.button_pop_back_stack)
        backButton.setOnClickListener {
            navController.popBackStack()
            textView.text = navController.currentDestination?.label ?: "NULL"
        }
    }
}
```

**fragment_first.xml**
  
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

    <TextView
        android:id="@+id/textview_first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/button_next"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_next"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/next"
        app:layout_constraintBottom_toTopOf="@id/button_pop_back_stack"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textview_first" />

    <Button
        android:id="@+id/button_pop_back_stack"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/popbackstack"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/button_next" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

## SecondFragment

また SecondFragment にも次の View を配置して画面遷移を操作できるようにしておく。

- 遷移状態を表す TextView
- SecondFragment を popBackStack するための Button

**SecondFragment**
  
```kotlin
class SecondFragment : Fragment(R.layout.fragment_second) {
    private val navController: NavController get() = findNavController()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val textView = view.findViewById<TextView>(R.id.textview_second)
        textView.text = navController.currentDestination?.label ?: "NULL"

        val button = view.findViewById<Button>(R.id.button_pop_back_stack)
        button.setOnClickListener {
            navController.popBackStack()
            textView.text = navController.currentDestination?.label ?: "NULL"
        }
    }
}
```

**fragment_second.xml**
  
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondFragment">

    <TextView
        android:id="@+id/textview_second"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@id/button_pop_back_stack"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_pop_back_stack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/popbackstack"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textview_second"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
```

# SecondFragment で popBackStack したときの動作

はじめに SecondFragment で popBackStack したときの動作を確認します。FirstFragment から SecondFragment に navigate し、SecondFragment で popBackStack します。そうすると期待どおりの動作で currentDestination が FirstFragment となります。画面も問題なく FirstFragment が表示されており currentDestination と一致しています。

![succeess.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/db69ca5e-8cc6-68d5-556c-3fd95d437a67.gif)
  
# FirstFragment で popBackStack したときの動作

次に FirstFragment で popBackStack したときの動作を確認します。FirstFragment を表示した状態で popBackStack します。そうすると期待どおりの動作で currentDestination が NULL となります。ですが画面は FirstFragment が表示されており currentDestination と不一致しています。currentDestination が NULL となった場合には Activity を finish するか、新たな画面に遷移する必要があります。
  
新たな画面に遷移する際には NULL になっていることに注意が必要です。例えば次のように FirstFragment から SecondFragment への遷移を実行すると遷移元が FirstFragment ではなく NULL なので java.lang.IllegalArgumentException が発生して落ちます。なので currentDestination が NULL になっているときは遷移元が NULL である遷移方法を選んで遷移しなければなりません。

![failed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/0e5cb405-a0c1-f71e-924d-b7865161babd.gif)

# おわりに

今回調べたことを以下にまとめます。
  
- StartDestination で popBackStack すると currentDestination が null になるが、表示状態は startDestinaton のままになる。
- currentDestination が null になったら Activity を finish するか、 新たな画面に遷移するしかない。
- 新たな画面に遷移するときには遷移元が NULL でも大丈夫な遷移方法を選ばなければならない（遷移方法を間違えるとアプリが落ちるため）
  
currentDestination が NULL であるのに画面は startDestination が表示されたままになるのはわかりづらいですね。。。そもそも startDestination をプログラムから popBackStack しないというのが正常な動作だと思うので仕方がない感はありますね。
  
# 参考文献
- [ナビゲーションとバックスタック](https://developer.android.com/guide/navigation/navigation-navigate?hl=ja#back-stack)