Android
Kotlin

Fragmentによる画面遷移でハマった

やりたいこと

Fragmentで作られた画面A,B,Cがあり、A->B->Cと遷移し、
Cでバックキーをタップした際は、Bを飛ばしてAに戻りたい

レイアウト

ベースのレイアウト

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

画面Aのレイアウト

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="#00000000"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button"
        android:text="Fragment A Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

device-2017-11-16-141547.png

画面Bのレイアウト

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="#00000000"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button"
        android:text="Fragment B Button"
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

device-2017-11-16-141554.png

画面Cのレイアウト

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00000000">

    <Button
        android:id="@+id/button"
        android:text="Fragment C Button"
        android:layout_gravity="bottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</FrameLayout>

device-2017-11-16-141600.png

画面遷移処理

画面Aの追加処理

    val transaction = supportFragmentManager.beginTransaction()
    transaction.add(R.id.container, FragmentA.newInstance())
    transaction.commit()

画面Bへの遷移処理

    val transaction = fragmentManager.beginTransaction()
    transaction.replace(R.id.container, FragmentB.newInstance())
    transaction.addToBackStack(null)
    transaction.commit()

画面Cへの遷移処理

    val transaction = fragmentManager.beginTransaction()
    transaction.replace(R.id.container, FragmentC.newInstance())
    // Bを飛ばしてAに戻りたいのでbackStack追加しない
    //transaction.addToBackStack(null) 
    transaction.commit()

上記実装で画面A->B->Cと遷移し、バックキーを押すと、なぜか画面Aと画面Cが両方表示されてしまう。
device-2017-11-16-141606.png

原因

http://extra-vision.blogspot.jp/2016/02/android-fragment-transaction-back-stack.html
上記リンクが参考になった。
addToBackStackはfragmentをスタックに積む命令ではなく、fragment操作のトランザクションを記録する命令であり、バックキータップ時の処理はスタックに積んだfragmentをpopしているわけではなく、記録されたトランザクションの逆の操作を行っている、ということである。
今回のケースでは、

add(fragment A) トランザクション記録しない
replace(fragment B) トランザクション記録する
replace(fragment C) トランザクション記録しない

となっており、replaceは内部的にはremoveとaddを連続で行うのと同じことなので、展開すると、

add(fragment A) トランザクション記録しない
remove(fragment A), add(fragment B) トランザクション記録する
remove(fragment B), add(fragment C) トランザクション記録しない

となる。この状態でバックキーをタップすると、記録されたトランザクションの逆の操作を行うため、

remove(fragment A), add(fragment B) トランザクション記録する

の逆の操作である、

remove(fragment B), add(fragment A)

の操作が実行され、fragmentAが最前面に表示され、fragmentCは残るということになる。

解決策

画面Cでバックキーをタップした際の処理をフックし、自身を殺した上でpopBackStackする

    fragmentManager.beginTransaction().remove(this).commit()
    fragmentManager.popBackStack()

もしくは、addToBackStack時にタグを指定しておき、タグ指定でpopBackStackする。

    transaction.replace(R.id.container, FragmentB.newInstance())
    transaction.addToBackStack("fragmentA")
....
....
    fragmentManager.popBackStack("fragmentA", FragmentManager.POP_BACK_STACK_INCLUSIVE)