目录
一、返回按键
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.两个原则
- DialogFragment只能存在于返回栈的栈顶
- 栈顶只能存在一个DialogFragment
基于以上两个原则,会分别导致以下两种情况:
- 从DialogFragment导航到Fragment会导致DialogFragment被弹出返回栈
- 从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传递参数