2021/02/27

NavHostFragmentをFragmentの入れ子にする時はsetPrimaryNavigationFragmentを指定する

NavHostFragmentapp:defaultNavHost=trueを指定すればバックキー制御をNavHostFragmentに任せることができます.

    <androidx.fragment.app.FragmentContainerView
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        ...

NavHostFragmentをアクティビティのレイアウトに指定した時の構造は次の通りです.

Activity
  |- NavHostFragment

一方で, アクティビティ直下にNavHostFragmentを配置せず, 下記のように間にフラグメントがいる場合は注意が必要です.

Activity
  |- Fragment
     |- NavHostFragment

この場合, フラグメントのレイアウトでapp:defaultNavHost=trueを指定しても, バックキー制御などナビゲーション周りで意図しない動作となります.

解決策

NavHostFragmentを持つフラグメントを PrimaryNavigationFragment に設定します.

class HostFragment : Fragment {
    override fun onAttach(context: Context) {
        super.onAttach(context)
        parentFragmentManager.commit {
            setPrimaryNavigationFragment(this@HostFragment)
        }

FragmentTransaction.setPrimaryNavigationFragment

app:defaultNavHost

NavHostFragmentapp:defaultNavHost=trueを指定されると, 自身のonAttachで同様に setPrimaryNavigationFragment(this) を設定します.

NavHostFragment.ktの該当行 - GitHub

setPrimaryNavigationFragment

プライマリナビゲーションフラグメントに指定されると, バックナビゲーションなどをハンドリングできるようになります.
app:defaultNavHost=trueを指定するだけで, NavHostFragmentがバックナビゲーションをうまく制御できるのはこのためです.

プライマリナビゲーションフラグメントはフラグメントマネージャのインスタンス毎に1つしか設定できません.

FragmentManager.setPrimaryNavigationFragment - GitHub

NavHostFragmentを複数管理する場合, app:defaultNavHost=trueNavHostFragmentは1つにしなければならない理由でもあります.

フラグメントがプライマリナビゲーションフラグメントと判定されるには, 親フラグメントがいる場合, 関連するフラグメントマネージャのプライマリナビゲーションフラグメントに指定されている必要があります.

つまり, 次の構造ではNavHostFragmentの親フラグメント/親フラグメントマネージャがいないので, NavHostfragmentがプライマリナビゲーションフラグメントになります.

Activity
  |- NavHostFragment

しかし, 次の構造ではNavHostFragmentに親フラグメントがおり, その親がsetPrimaryNavigationFragmentとして指定されていない場合, 子であるNavHostFragmentもプライマリナビゲーションフラグメントの条件を満たしません.

Activity
  |- Fragment
     |- NavHostFragment

そのため, 親フラグメントは次のようなコードで自身をプライマリナビゲーションフラグメントとして指定する必要があります.

class HostFragment : Fragment {
    override fun onAttach(context: Context) {
        super.onAttach(context)
        parentFragmentManager.commit {
            setPrimaryNavigationFragment(this@HostFragment)
        }

蛇足: NavHostFragmentのバックナビゲーション周りの実装

// デフォルトでフラグメントマネージャのOnBackPressedCallbackはenable=falseになっている

FragmentManager
    private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
                @Override
                public void handleOnBackPressed() {
                    FragmentManager.this.handleOnBackPressed();
                }
            };

---

// OnBackPressedCallbackをenable=trueにするにはisPrimaryNavigationでtrueを返す必要がある

    private void updateOnBackPressedCallbackEnabled() {
        ...
        // This FragmentManager needs to have a back stack for this to be enabled
        // And the parent fragment, if it exists, needs to be the primary navigation
        // fragment.
        mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0
                && isPrimaryNavigation(mParent));
    }

---

// PrimaryNavigationFragmentに変更があると...

Fragment
    void performPrimaryNavigationFragmentChanged() {
        boolean isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(this); ⭐️
        // Only send out the callback / dispatch if the state has changed
        if (mIsPrimaryNavigationFragment == null
                || mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) {
            mIsPrimaryNavigationFragment = isPrimaryNavigationFragment;
            onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment); 🍣

---

// 一方, NavHostFragmentでは...

NavHostFragment
🍣
    public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) {
        if (mNavController != null) {
            mNavController.enableOnBackPressed(isPrimaryNavigationFragment); 🌴

---

// BackPressedCallbackを有効にするにはisPrimaryNavigationFragmentがtrueである必要がある.

NavController
🌴
    void enableOnBackPressed(boolean enabled) {
        mEnableOnBackPressedCallback = enabled;  
        updateOnBackPressedCallbackEnabled();

    private void updateOnBackPressedCallbackEnabled() {
        mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
                && getDestinationCountOnBackStack() > 1);

---

// NavControllerはOnBackPressedCallbackを持っている. NavHostFragmentのバックキー制御はNavControllerの責務

    private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            popBackStack();
        }
    };

以上.