Android Jetpack Navigation组件(七):扩展知识

本文详细介绍了Android Jetpack Navigation组件的返回按键处理,包括物理返回按键和Toolbar导航按钮的事件处理。讲解了如何通过NavBackStackEntry、共享ViewModel和FragmentResultAPI来返回数据给目的地。此外,还讨论了如何在导航图外的Fragment实现导航,处理通知的显式DeepLink,以及DialogFragment的使用原则和常见操作。最后,阐述了如何在action中复写或创建目的地参数,确保参数正确传递。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、返回按键

1.物理返回按键

如果通过app:defaultNavHost="true"或者FragmentManager.setPrimaryNavigationFragment()Navigation组件监听物理返回按键事件,那么物理返回按键事件的传递如下:

ComponentActivity.onBackPressed() -> OnBackPressedDispatcher.onBackPressed() -> OnBackPressedCallback.handleOnBackPressed() -> NavController.popBackStack()

关键源码:

ComponentActivity

public void onBackPressed() {
    mOnBackPressedDispatcher.onBackPressed();
}

OnBackPressedDispatcher

public void onBackPressed() {
    Iterator<OnBackPressedCallback> iterator = mOnBackPressedCallbacks.descendingIterator();
    // 通过降序寻找 最新的且enable的OnBackPressedCallback 去处理物理返回按键事件
    while (iterator.hasNext()) {
        OnBackPressedCallback callback = iterator.next();
        if (callback.isEnabled()) {
            callback.handleOnBackPressed();
            return;
        }
    }
    // 如果没有OnBackPressedCallback处理本次物理返回按键事件,则默认调用Activity的onBackPressed()方法(Activity.finish())
    if (mFallbackOnBackPressed != null) {
        mFallbackOnBackPressed.run();
    }
}

NavController

// 实现onBackPressedCallback
private val onBackPressedCallback: OnBackPressedCallback =
    object : OnBackPressedCallback(false) {
        override fun handleOnBackPressed() {
        // 将栈顶的目的地弹出
            popBackStack()
        }
    }

private fun updateOnBackPressedCallbackEnabled() {
	// 1.enableOnBackPressedCallback由NavHostFragment是否为PrimaryNavigationFragment决定
	// 2.destinationCountOnBackStack > 1 表示NavController返回栈中的目的地大于1
	// 满足上述两个条件,NavController才会处理物理返回按键事件
    onBackPressedCallback.isEnabled = (enableOnBackPressedCallback && destinationCountOnBackStack > 1)
}

由上面的内容可以推出

  • 嵌套NavHost内只有一个目的地时,物理返回按键事件会由其父NavHost处理,弹出NavHost所在Fragment
  • 通过ComponentActivity.getOnBackPressedDispatcher().addCallback()可以拦截Navigation组件对物理返回按键事件的处理
  • 从其他应用通过隐式DeepLink导航到本应用的某个目的地,按物理返回按键会finish activity并回到其他应用的任务栈

2.Toolbar导航按钮

通过NavigationUI绑定Toolbar后,Toolbar的导航按钮事件会由Navigation组件处理。

关键源码:

NavigationUI

public fun setupWithNavController(toolbar: Toolbar,navController: NavController,
    configuration: AppBarConfiguration =
        AppBarConfiguration.Builder(navController.graph).build()
) {
	// 变换目的地时变换Toolbar内容
    navController.addOnDestinationChangedListener(
        ToolbarOnDestinationChangedListener(toolbar, configuration)
    )
    // 给Toolbar设置导航按钮事件
    toolbar.setNavigationOnClickListener { navigateUp(navController, configuration) }
}


public fun navigateUp(navController: NavController, configuration: AppBarConfiguration): Boolean {
    val openableLayout = configuration.openableLayout
    val currentDestination = navController.currentDestination
    val topLevelDestinations = configuration.topLevelDestinations
    return if (openableLayout != null && currentDestination != null &&
        currentDestination.matchDestinations(topLevelDestinations)
    ) {
        openableLayout.open()
        true
    } else {
    	// 首先调用NavController.navigateUp()方法
        return if (navController.navigateUp()) {
            true
        // 如果NavController.navigateUp()返回false,那么回调我们之前通过AppBarConfiguration设置的fallbackOnNavigateUpListener接口
        } else configuration.fallbackOnNavigateUpListener?.onNavigateUp() ?: false
    }
}

NavController


    /**
     * Attempts to navigate up in the navigation hierarchy. Suitable for when the
     * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
     * (or starting) corner of the app UI.
     *
     * The intended behavior of Up differs from [Back][popBackStack] when the user
     * did not reach the current destination from the application's own task. e.g. if the user
     * is viewing a document or link in the current app in an activity hosted on another app's
     * task where the user clicked the link. In this case the current activity (determined by the
     * context used to create this NavController) will be [finished][Activity.finish] and
     * the user will be taken to an appropriate destination in this app on its own task.
     *
     * @return true if navigation was successful, false otherwise
     */
    @MainThread
    public open fun navigateUp(): Boolean {
        // If there's only one entry, then we may have deep linked into a specific destination on another task.
        if (destinationCountOnBackStack == 1) {
            val extras = activity?.intent?.extras
            // 如果是从其他应用触发DeepLink导航到该目的地。   注意与物理返回按键处理方式的不同
            if (extras?.getIntArray(KEY_DEEP_LINK_IDS) != null) {
// 重建(如果有)/创建 并回到本应用自己的任务栈,逐级导航到该目的地所在导航图的startDestination(显式DeepLink的导航逻辑)
                return tryRelaunchUpToExplicitStack()
            } else {// 如果是在本应用导航到该目的地
// 1.如果该目的地是startDestination则直接返回false,
// 2.如果该目的地不是startDestination,则重建任务栈,逐级导航到该目的地所在导航图的startDestination(显式DeepLink的导航逻辑)
                return tryRelaunchUpToGeneratedStack()
            }
        } else {
        	// 当返回栈包含多个目的地时,走NavController.popBackStack()方法
            return popBackStack()
        }
    }

二、返回数据给目的地

1.通过NavBackStackEntry

一般情况下,用本方法即可满足需求。

参考:Android Jetpack Navigation组件(六):编程交互——返回结果给前目的地

2.通过共享ViewModel

如果数据量较大,应该使用本方法。

参考:Android Jetpack Navigation组件(六):编程交互——获取导航图范围的ViewModel

当然,也可以共享Activity范围的ViewModel。或者共享Parent Fragment范围的ViewModel。

3.通过Fragment Result API

如果Fragment不在导航图里,可以使用本方法传递一次性数据。

核心要点:

  • 通过FragmentManager.setFragmentResultListener()接收数据
  • 通过FragmentManager.setFragmentResult()发送数据
  • 保证接收方和发送方使用的是同一个FragmentManager

假设MainFragment内有AFragment和BFragment:

(1)同级Fragment之间通过ParentFragmentManager传递数据

假设AFragment和BFragment为同级Fragment。

AFragment:

// 通过ParentFragmentManager.setFragmentResultListener()接收数据(Bundle)
getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
    @Override
    public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
        // We use a String here, but any type that can be put in a Bundle is supported
        String result = bundle.getString("bundleKey");
        // Do something with the result
    }
});

BFragment:

Bundle result = new Bundle();
result.putString("bundleKey", "result");
// 通过ParentFragmentManager.setFragmentResult()发送数据(Bundle)
getParentFragmentManager().setFragmentResult("requestKey", result);

(2)父子Fragment之间通过父Fragment的FragmentManager传递数据

假设MainFragment为BFragment的ParentFragment。

MainFragment:

// 通过ChildFragmentManager.setFragmentResultListener()接收数据(Bundle)
getChildFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
    @Override
    public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
        String result = bundle.getString("bundleKey");
    }
});

BFragment代码与(1)一致。

(3)Activity与Fragment之间通过Activity的FragmentManager传递数据

假设BFragment直属于MainActivity。

MainActivity:

getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
    @Override
    public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
        String result = bundle.getString("bundleKey");
    }
});

BFragment代码与(1)一致。

注意:

  • 一个requestKey只能有一个listener和result。当listener处于非STARTED状态时,多次调用setFragmentResult()取最后一次的result
  • 如果调用setFragmentResult(),但是没有listener接收result,那这个result将留存于FragmentManager直到有listener接收该result
  • 一旦listener接收到result也就是走了onFragmentResult()回调,那么该result将从FragmentManager中清除
  • 当listener处于STARTED状态时,只能调用一次setFragmentResult(),并且listener会立即回调onFragmentResult()

三、导航图外的Fragment实现导航

导航图外的Fragment可以借助ParentFragment实现导航,前提是ParentFragment在导航图内。常见于ViewPager中的Fragment。

假设TabFragment的ParentFragment是AFragment,AFragment、BFragment在导航图内,TabFragment在导航图外。

TabFragment

// 通过id导航到BFragment  
// NavHostFragment.findNavController()会寻找TabFragment的ParentFragment(AFragment)所在导航图的NavHost的NavController
NavHostFragment.findNavController(TabFragment.this).navigate(R.id.bFragment);

可以看到写法与在导航图里的Fragment进行导航的写法是相同的,但是有以下三点注意事项:

  • 由于TabFragment不在导航图里,所以无法使用Action导航,无法使用Safe Args传递参数
  • 由于TabFragment没有NavBackStackEntry,所以无法通过NavBackStackEntry返回数据
  • 对Navigation组件来说,不是从TabFragment,而是从AFragment导航到BFragment

结论:

  • 应该在不需要获取目的地Fragment返回结果的情况下使用上述方法
  • TabFragment可以借助AFragment与BFragment通信 (参考本章第二节:返回数据给目的地)

四、通知不使用显式DeepLink

采用传统方法点击通知跳转到Activity,在Activity的onNewIntent()使用NavController导航

这样做不会使应用重建任务栈和Activity。

五、DialogFragment

1.两个原则

  1. DialogFragment只能存在于返回栈的栈顶
  2. 栈顶只能存在一个DialogFragment

基于以上两个原则,会分别导致以下两种情况:

  1. 从DialogFragment导航到Fragment会导致DialogFragment被弹出返回栈
  2. 从DialogFragment导航到DialogFragment会导致两个DialogFragment都被弹出返回栈

关键源码:

public class DialogFragmentNavigator(...) : Navigator<Destination>() {
......
    private val observer = LifecycleEventObserver { source, event ->
        if (event == Lifecycle.Event.ON_CREATE) {
		.....
        } else if (event == Lifecycle.Event.ON_STOP) {
            val dialogFragment = source as DialogFragment
            if (!dialogFragment.requireDialog().isShowing) {
                val beforePopList = state.backStack.value
                val poppedEntry = checkNotNull(beforePopList.lastOrNull {
                    it.id == dialogFragment.tag
                }) {
                    ...
                }
                if (beforePopList.lastOrNull() != poppedEntry) {
                    Log.i(
                        TAG, "Dialog $dialogFragment was dismissed while it was not the top " +
                            "of the back stack, popping all dialogs above this dismissed dialog"
                    )
                }
                popBackStack(poppedEntry, false)
            }
        }
    }
......
}

2.使用总结

2.1 dismiss

  • 主要有3种方法dismiss DialogFragment:
    1.NavController.navigateUp()
    2.NavController.popBackStack()
    3.DialogFragment.dismiss()
  • DialogFragment dimiss后会走onDismiss()回调,并弹出返回栈

2.2 cancel

  • 通过DialogFragment的setCancelable()方法设置DialogFragment是否可以cancel(cancelable属性),默认为true
  • 3种方法cancel DialogFragment:
    1.点击空白屏幕
    2.系统返回键
    3.getDialog().cancel() (不受cancelable属性限制)
  • DialogFragment cancel后会依次走onCancel()onDismiss()回调,并弹出返回栈

2.3 onCreateDialog()与onCreateView()

  • onCreateDialog()onCreateView()只能二选一
  • onCreateDialog()onCreate()onCreateView()两个回调之间

2.4 LifecycleOwner

  • 如果使用onCreateDialog()创建Dialog,应该使用DialogFragment本身或者对应的NavBackStackEntry作为LifecycleOwner
    而不能使用DialogFragment的ViewLifecycleOwner(为空)

六、在action里复写/创建目的地参数

通过在<action>声明<argument>复写/创建目的地参数

例如有以下导航图

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph"
    app:startDestination="@id/aFragment">

    <fragment
        android:id="@+id/aFragment"
        android:name="com.scx.navigation.deepLink.AFragment">
        <action
            android:id="@+id/action_aFragment_to_bFragment"
            app:destination="@id/bFragment" >
<!--复写BFragment目的地的参数arg1,设置默认值为abc。注意不能修改arg1的参数类型,这是规定-->
            <argument
                android:name="arg1"
                app:argType="string"
                android:defaultValue="abc"/>
<!--创建参数arg2,在使用本action时,会将arg2传递给BFragment-->
<!--注意BFragment无法通过SafeArgs获取该参数,只能通过requireArguments()的Bundle获取-->
            <argument
                android:name="arg2"
                app:argType="integer" />
        </action>
    </fragment>

    <fragment
        android:id="@+id/bFragment"
        android:name="com.scx.navigation.deepLink.BFragment">
        <argument
            android:name="arg1"
            app:argType="string"/>
    </fragment>

    <navigation
        android:id="@+id/nav_graph_nested"
        app:startDestination="@id/cFragment">
<!--CFragment是嵌套导航图的startDestination,需要id和name两个参数-->
        <fragment
            android:id="@+id/cFragment"
            android:name="com.scx.navigation.deepLink.CFragment">

            <argument
                android:name="id"
                app:argType="integer" />
            <argument
                android:name="name"
                app:argType="string" />
        </fragment>

        <fragment
            android:id="@+id/dFragment"
            android:name="com.scx.navigation.deepLink.DFragment" />
    </navigation>
    
<!--  导航到嵌套导航图的action。 一定要复写参数,否则无法给嵌套导航图和它的startDestination传递参数  -->
    <action
        android:id="@+id/action_global_nav_graph_nested"
        app:destination="@id/nav_graph_nested" >
<!--    复写嵌套导航图的startDestination(CFragment)需要的参数,这样就可以通过action给嵌套导航图和CFragment传递参数了    -->
<!--    同样,按照规定,这里的参数名称和类型必须与CFragment声明的参数名称和类型相同    -->
        <argument
            android:name="id"
            app:argType="integer" />
        <argument
            android:name="name"
            app:argType="string" />
    </action>

</navigation>

AFragment导航到BFragment

int arg2 = 0;
navController.navigate(AFragmentDirections.actionAFragmentToBFragment(arg2));

BFragment获取参数

// 通过SafeArgs获取arg1参数
String arg1 = BFragmentArgs.fromBundle(requireArguments()).getArg1();
// 通过Bundle获取arg2参数。因为arg2没在BFragment目的地中声明,所以无法通过SafeArgs获取该参数
int arg2 = requireArguments().getString("arg2");

导航到嵌套导航图

int id = 1;
String name = "Jack";
navController.navigate(NavGraphDirections.actionGlobalNavGraphNested(id, name));

核心思想:SafeArgs通过action传递参数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值