Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager
Android Jetpack组件(九)DataStore
Android开发中流行单个Activity
嵌套多个Fragment
的UI架构模式,但是对Fragment
的管理比较麻烦。Fragment
的切换包括对AppBar的管理、Fragment
间的切换动画以及Fragment
间的参数传递。在此过程中实现代码比较复杂混乱。为此,Jetpack提供了Navigation组件,方便我们管理页面和AppBar。
destination
和action
完成页面间的导航。DeepLink
。NavigationUI
类,对菜单、底部导航、抽屉菜单导航进行统一的管理。Navigation Graph
。这是一种新型的 XML 资源文件,其中包含应用程序所有的页面,以及页面间的关系。NavHostFragment
。这是一个特殊的 Fragment
,Navigation Graph
中的 Fragment 正是通过 NavHostFragment
进行展示的。NavController
。这是一个 Java/Kotlin 对象,用于在代码中完成 Navigation Graph
中具体的页面切换工作。使用Navigation时,我们需要添加如下依赖:
def nav_version = "2.3.3"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Jetpack Compose Integration
implementation "androidx.navigation:navigation-compose:1.0.0-alpha07"
新建一个Android项目后,选择res文件夹—>New Android Resource File,Resource type选择navigation,填写文件名后回车,Navigation Grpah文件创建完成。我们切换到Design面板,可以看到面板左上角提示No NavHostFragments found,接下来添加NavHostFragment
。
NavHostFragment
是一种特殊的Fragment
,我们需要将它添加到Activity
的布局文件中,作为其它Fragment
的容器。
<!--app:defaultNavHost="true"表示Fragment会自动处理系统返回事件-->
<!--app:navGraph="@navigation/mobile_navigation"设置Fragment对应的导航图-->
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />
我们再次回到Nav Graph文件的Design面板,可以看到我们刚才设置的NavHostFragment
。
点击Navigation Graph文件的Design面板上的click to add a destination,可以选择现有的Fragment
,也可以点击 Create new destination创建Fragment
。添加完成以后,可以看到Navigation Graph文件的代码。
<?xml version="1.0" encoding="utf-8"?>
<!--app:startDestination="@id/homeFragment"设置起始目的地-->
<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/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.yhj.jetpackstudy.ui.home.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/dashboardFragment"
android:name="com.yhj.jetpackstudy.ui.dashboard.DashboardFragment"
android:label="fragment_dashboard"
tools:layout="@layout/fragment_dashboard" />
</navigation>
在Navigation Graph文件的Design面板中,将鼠标悬停在destination的右侧,会出现一个圆圈,点击圆圈并将光标拖动到导航destination的上面,松开鼠标,会生成一个指示线。切换到Code面板可以看到代码发生了变化。
<fragment
android:id="@+id/homeFragment"
android:name="com.yhj.jetpackstudy.ui.home.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home" >
<action
android:id="@+id/action_homeFragment_to_dashboardFragment4"
app:destination="@id/dashboardFragment" />
</fragment>
页面的切换通常有两种方式:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Navigation.findNavController(view).navigate(R.id.action_navigation_home_to_navigation_dashboard);
}
});
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_navigation_home_to_navigation_dashboard));
在Navigation Graph文件的action下添加要执行的动画
<action
android:id="@+id/confirmationAction"
app:destination="@id/confirmationFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
使用Safe Args Gradle插件,该插件可以生成简单的对象和构造器类,支持在destination之间进行类型安全的导航和参数传递。
在Project的build.gradle的dependencies下添加classpath。
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
def nav_version = "2.3.3"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
如需生成适合Java模块或Java和Kotlin混合模块的代码,可以在app的build.gradle下添加apply plugin。
apply plugin: "androidx.navigation.safeargs"
如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,添加如下代码。
apply plugin: "androidx.navigation.safeargs.kotlin"
在Navigation Graph的添加如下<argument />
标签内代码。
<fragment
android:id="@+id/navigation_home"
android:name="com.yhj.life.LifeFragment"
android:label="fragment_life"
tools:layout="@layout/fragment_life">
<action
android:id="@+id/action_navigation_home_to_navigation_dashboard"
app:destination="@id/navigation_dashboard" />
<argument
android:name="user_name"
android:defaultValue="@null"
app:argType="string" />
<argument
android:name="age"
android:defaultValue="0"
app:argType="integer" />
</fragment>
argType除了支持常见的integer
、float
和long
等基本数据类型,还支持资源引用、自定义 Parcelable
、自定义 Serializable
和自定义 Enum
。
添加完成之后,在app java(generated)下面可以看到Safe Args插件为我们生成的代码,代码文件里包含参数生成的getter()
和setter()
。
Bundle bundle=new MainFragmentArgs.builder()
.setUserName("yhj")
.setAge(30)
.build().toBundle();
Navigation.findNavController(view).navigate(R.id.action_navigation_home_to_navigation_dashboard);
Bundle bundle=getArguments();
if(bundle!=null){
String userName=MainFragmentArgs.fromBundle(getArguments()).getUserName();
int age=MainFragmentArgs.fromBundle(getArguments()).getAge();
}
页面切换的过程中,通常会伴随着AppBar的变化,AppBar中的按钮也可能承担页面切换的工作,既然Navigation和AppBar都需要处理页面切换事件,为了方便管理,Jetpack引入了NavigationUI组件。
在Navigation Graph文件中可以通过android:label
来设置AppBar的标题。
<navigation>
<fragment
android:label="Page title">
...
</fragment>
</navigation>
NavigationUI支持以下顶部应用栏类型:
Toolbar
AppBarConfiguration
用于AppBar的配置,NavController用于页面的导航和切换。通过 NavigationUI.setupWithNavController
绑定起来。Toolbar
时,Navigation组件会自动处理导航按钮的点击事件,因此无需覆盖onSupportNavigateUp()
。 NavHostFragment fragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
navController = fragment.getNavController();
appBarConfiguration = new AppBarConfiguration.Builder(this.navController.getGraph()).build();
NavigationUI.setupWithNavController(
binding.toolbar, this.navController, appBarConfiguration);
CollapsingToolbarLayout
CollapsingToolbarLayout layout = findViewById(R.id.collapsing_toolbar_layout);
Toolbar toolbar = findViewById(R.id.toolbar);
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
AppBarConfiguration appBarConfiguration =new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupWithNavController(layout, toolbar, navController, appBarConfiguration);
ActionBar
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
需要覆盖onSupportNavigateUp()
处理向上导航。
@Override
public boolean onSupportNavigateUp() {
return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp();
}
DrawLayout
+Navigation View
)AppBarConfiguration appBarConfiguration =
new AppBarConfiguration.Builder(navController.getGraph())
.setDrawerLayout(drawerLayout)
.build();
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
NavigationView navView = findViewById(R.id.nav_view);
NavigationUI.setupWithNavController(navView, navController);
onCreateOptionsMenu()
添加菜单,则可以通过覆盖Activity
的onOptionsItemSelected()
以调用onNavDestinationSelected()
,从而将菜单项与目标页相关联。同时需要覆盖onSupportNavigateUp()
处理向上导航。@Override
public boolean onOptionsItemSelected(MenuItem item) {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
return NavigationUI.onNavDestinationSelected(item, navController)
|| super.onOptionsItemSelected(item);
}
当我们在Fragment
中添加菜单跳转目标页时,需要覆盖目标页面onCreateOptionsMenu()
,并在该方法中清除上个页面对应的menu。
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
menu.clear();
super.onCreateOptionsMenu(menu, inflater);
}
BottomNavigationView
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
//对于BottomNavigationView支持
NavigationUI.setupWithNavController(navView, navController);
NavController
提供了一个名为OnDestinationChangedListener
的接口,对页面切换事件进行监听,该接口在页面发生切换或参数改变时调用。
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
//处理事件
}
});
在项目开发中,我们可能需要跳转到应用内指定的页面,Navigation组件提供了DeepLink(深层链接),通过它实现跳转到应用指定页面。它支持两种不同类型的深层链接:显式深层链接和隐式深层链接。
显式深层链接使用PendingIntent
跳转到指定页面,例如应用程序收到某个通知推送,用户点击此通知时,条抓到展示该通知的内容页面。
当用户通过显式深层链接打开您的应用时,任务返回堆栈会被清除,并被替换为相应的深层链接页面。当用户从深层链接页面按下返回按钮时,他们会返回到相应的导航堆栈。
我们使用NavDeepLinkBuilder
类构造PendingIntent
。
PendingIntent pendingIntent = new NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.android)
.setArguments(args)
.createPendingIntent();
也可以通过navController.createDeepLink()
创建。
Navigation.findNavController(this, R.id.nav_host_fragment)
.createDeepLink()
.setGraph(R.navigation.mobile_navigation)
.setDestination(R.id.action_navigation_home_to_navigation_dashboard2)
.setArguments(bundle)
.createPendingIntent();
需要注意的是,如果提供的上下文不是Activity
,构造函数会使用PackageManager.getLaunchIntentForPackage()
作为默认Activity
启动(如有)。
//NavDeepLinkBuilder的构造函数
public NavDeepLinkBuilder(@NonNull Context context) {
mContext = context;
if (mContext instanceof Activity) {
mIntent = new Intent(mContext, mContext.getClass());
} else {
Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(
mContext.getPackageName());
mIntent = launchIntent != null ? launchIntent : new Intent();
}
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
隐式深层链接指的是应用中特定页面的URI,如用户点击某个链接。
在触发隐式深层链接时,返回堆栈的状态取决于是否使用 Intent.FLAG_ACTIVITY_NEW_TASK
标志启动隐式Intent
。
<deepLink />
标签。 <deepLink app:uri="www.yanghujun.com/{params}?arg={arg}"/>
注意:
{params}
的路径参数占位符与一个或多个字符相匹配。nav-graph
标签。<nav-graph android:value="@navigation/mobile_navigation"/>
构建项目时,Navigation 组件会将 <nav-graph />
标签替换为生成的 <intent-filter />
标签,以匹配导航图中的所有深层链接。
阅读FragmentNavigator
的源码时,可以看到页面切换的时候使用的是replace()
,这会造成Fragment
生命周期的重启,界面数据重新加载,不能复用。源码如下。
@SuppressWarnings("deprecation") /* Using instantiateFragment for forward compatibility */
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
通过show()/hide()
方式可以实现Fragment
的复用,同时也不会重启生命周期。可以自定义FragmentNavigator
重写navigate()
来达到以上目的。
首先自定义的FragmentNavigator
要和FragmentNavigator
一样,添加@Navigator.Name("新的Navigator名字")
注解。
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
//实例化过的要重用
//final Fragment frag = instantiateFragment(mContext, mManager,
// className, args);
//frag.setArguments(args);
final FragmentTransaction ft = mManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
//当前正在显示的fragment
Fragment fragment = mManager.getPrimaryNavigationFragment();
if (fragment != null) {
//隐藏展示下个页面
ft.hide(fragment);
}
Fragment frag = null;
String tag = String.valueOf(destination.getId());
//查找下个fragment,没有则创建
frag = mManager.findFragmentByTag(tag);
if (frag != null) {
ft.show(frag);
} else {
frag = instantiateFragment(mContext, mManager, className, args);
frag.setArguments(args);
ft.add(mContainerId, frag, tag);
}
//ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
// mBackStack管理的是fragment回退的堆栈,源码中是private的无法获取,通过反射的方式获取
ArrayDeque<Integer> mBackStack = null;
try {
Field field = FragmentNavigator.class.getDeclaredField("mBackStack");
field.setAccessible(true);
mBackStack = (ArrayDeque<Integer>) field.get(this);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
private String generateBackStackName(int backStackindex, int destid) {
return backStackindex + "-" + destid;
}
然后在Activity中创建自定义的FragmentNavigator
并设置给NavController
,需要注意的是我们要通过自定义的FragmentNavigator
手动来创建Destination(目的地),布局中app:navGraph=" "
设置无效。
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
navController = NavHostFragment.findNavController(fragment);
FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(this, getSupportFragmentManager(), fragment.getId());
navController.getNavigatorProvider().addNavigator(fragmentNavigator);
NavGraph navDestinations = initNavGraph(navController.getNavigatorProvider(), fragmentNavigator);
navController.setGraph(navDestinations);
//结合BottomNavigationView切换页面
navView.setOnNavigationItemSelectedListener(item -> {
navController.navigate(item.getItemId());
return true;
});
private NavGraph initNavGraph(NavigatorProvider provider, FixFragmentNavigator fragmentNavigator) {
NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));
//用自定义的导航器来创建目的地
FragmentNavigator.Destination destination1 = fragmentNavigator.createDestination();
destination1.setId(R.id.navigation_home);
destination1.setClassName(HomeFragment.class.getCanonicalName());
navGraph.addDestination(destination1);
FragmentNavigator.Destination destination2 = fragmentNavigator.createDestination();
destination2.setId(R.id.navigation_dashboard);
destination2.setClassName(DashboardFragment.class.getCanonicalName());
navGraph.addDestination(destination2);
FragmentNavigator.Destination destination3 = fragmentNavigator.createDestination();
destination3.setId(R.id.navigation_notifications);
destination3.setClassName(NotificationsFragment.class.getCanonicalName());
navGraph.addDestination(destination3);
navGraph.setStartDestination(destination1.getId());
return navGraph;
}
最后,我们还需要处理onBackPressed()
,因为返回Navigation会操作回退栈,切换到之前显示的页面,我们需要销毁当前页面则要进行拦截。
@Override
public void onBackPressed() {
finish();
}
这样,我们就完成了定制的Fragment页面切换。