最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面。
Android Architecture组件是Android Jetpack的一部分,它们是一组库,旨在帮助开发者设计健壮、可测试和可维护的应用程序,包含一下组件:
本系列文章是各处copy过来的,个人感觉所有的开发者都应该尽早的熟悉Jetpack组件,相信一定会被它的魅力所吸引,最近也在完成一个使用以上所有组件实现的项目,作为对Jetpack组件的项目实践,下面来分析一下每个组件对项目开发的帮助。
导航架构组件简化了Android应用程序中导航的实现,通过在xml中添加元素并指定导航的起始和目的地,从而在Fragment之间建立连接在Activity中调用xml中设置的导航action从而跳转界面到目的地,简单来说它和之前在活动中调用startActivity的区别就类似于代码布局和xml中layout布局一样,既简单又可视化,如下图就是一个navigaton的xml图:
Navigation多数作用于Fragment中,不过导航组件还支持:Fragment、Activity、导航图和子图、自定义目标。本文内容实现如下功能:
在实战之前,我们先来了解一下Navigation
中最关键的三要素,他们是:
名词 | 解释 |
---|---|
Navigation Graph (New XML resource) |
如我们的第一张图所示,这是一个新的资源文件,用户在可视化界面可以看出他能够到达的Destination (用户能够到达的屏幕界面),以及流程关系。 |
NavHostFragment (Layout XML view) |
当前Fragment 的容器 |
NavController (Kotlin/Java object) |
导航的控制者 |
可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph
看作一个地图,NavHostFragment
看作一个车,以及把NavController
看作车中的方向盘,Navigation Graph
中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment
可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController
,虽然它取决于开车人(用户)。
第一步 添加依赖
模块层的build.gradle
文件需要添加:
ext.navigationVersion = "2.0.0"
dependencies {
//...
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}
如果你要使用SafeArgs
插件,还要在项目目录下的build.gradle
文件添加:
buildscript {
ext.navigationVersion = "2.0.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
}
以及模块下面的build.gradle
文件添加:
apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'
第二步 创建navigation导航
res
目录下创建navigation
目录 -> 右击navigation
目录New一个Navigation resource file
Destination
,如果说navigation
是我们的导航工具,Destination
是我们的目的地,在此之前,我已经写好了一个WelcomeFragment
、LoginFragment
和RegisterFragment
,添加Destination
的操作完成后如下所示:除了可视化界面之外,我们仍然有必要看一下里面的内容组成,login_navigation.xml
:
我在这里省略了一些不必要的代码。让我们看一下navigation标签
的属性:
属性 | 解释 |
---|---|
app:startDestination |
默认的起始位置 |
第三步 建立NavHostFragment
我们创建一个新的LoginActivity
,在activity_login.xml
文件中:
有几个属性需要解释一下:
属性 | 解释 |
---|---|
android:name |
值必须是androidx.navigation.fragment.NavHostFragment ,声明这是一个NavHostFragment |
app:navGraph |
存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph |
app:defaultNavHost="true" |
与系统的返回按钮相关联 |
第四步 界面跳转、参数传递和动画
在WelcomeFragment
中,点击登录和注册按钮可以分别跳转到LoginFragment
和RegisterFragment
中。
这里我使用了两种方式实现:
目标:WelcomeFragment
携带key
为name
的数据跳转到LoginFragment
,LoginFragment
接收后显示。
Have a account ? Login
按钮的点击事件如下:
btnLogin.setOnClickListener {
// 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf")
findNavController().navigate(R.id.login, bundle,navOption)
}
后续LoginFragment
的接收代码比较简单,直接获取Fragment中的Bundle
即可,这里不再出示代码。最后的效果:
Safe Args
目标:WelcomeFragment
通过Safe Args
将数据传到RegisterFragment
,RegisterFragment
接收后显示。
再看一下已经展示过的login_navigation.xml
:
细心的同学可能已经观察到navigation
目录下的login_navigation.xml
资源文件中的action
标签和argument
标签,这里需要解释一下:
心的同学可能已经观察到navigation
目录下的login_navigation.xml
资源文件中的action
标签和argument
标签,这里需要解释一下:
action标签
属性 | 作用 |
---|---|
app:destination |
跳转完成到达的fragment 的Id |
app:popUpTo |
将fragment 从栈 中弹出,直到某个Id的fragment |
argument标签
属性 | 作用 |
---|---|
android:name |
标签名字 |
app:argType |
标签的类型 |
android:defaultValue |
默认值 |
点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类:
WelcomeFragment
中的JOIN US
按钮点击事件:
btnRegister.setOnClickListener {
val action = WelcomeFragmentDirections
.actionWelcomeToRegister()
.setEMAIL("[email protected]")
findNavController().navigate(action)
}
RegisterFragment
中的接收:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ...
val safeArgs:RegisterFragmentArgs by navArgs()
val email = safeArgs.email
mEmailEt.setText(email)
}
以及效果:
需要提及的是,如果不用Safe Args
,action
可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)
方式生成,感兴趣的同学可以自行编写。
Navigation
可以绑定menus
、drawers
和bottom navigation
,这里我们以bottom navigation
为例,我先在navigation
目录下新创建了main_navigation.xml
,接着新建了MainActivity
,下面则是activity_main.xml
:
MainActivity
中的处理也十分简单:
class MainActivity : AppCompatActivity() {
lateinit var bottomNavigationView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
//...
val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
val navController = host.navController
initWidget()
initBottomNavigationView(bottomNavigationView,navController)
}
private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView, navController: NavController) {
bottomNavigationView.setupWithNavController(navController)
}
private fun initWidget() {
bottomNavigationView = findViewById(R.id.navigation_view)
}
}
效果:
5.1 NavHostFragment
官网上是这样介绍它的:NavHostFragment provides an area within your layout for self-contained navigation to occur. 大致意思就是NavHostFragment在布局中提供了一个区域,用于进行包含导航
接下来我们看一下它的源码:
public class NavHostFragment extends Fragment implements NavHost {
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (mDefaultNavHost) {
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
}
}
可以看到它就是一个Fragment
,在onAttach
生命周期开启事务将它自己设置成了PrimaryFragment了,当然通过defaultNavHost
条件判断的,这个布尔值看着眼熟吗?没错,就是我们在xml布局中设置的那一个。app:defaultNavHost="true"
接着看它的onCreate
生命周期
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavController(context);
mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
.......
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
我们看到在onCreate生命周期中创建了一个NavController,并且为这个NavController创建了一个_Navigator__添加了进去,_我们跟踪createFragmentNavigator,发现它创建了一个FragmentNavigator,这个类是做什么的呢?它继承了Navigator,查看注释我们知道它是为每个Navigation设置策略的,也就是说Fragment之间通过导航切换都是由它来操作的,下面会详细介绍的,这里先简单看下。
接下来我们看到为NavController设置了setGraph(),也就是我们xml里面定义的navGraph,导航布局里面的Fragment及action跳转等信息。
还有就是onCreateView、onViewCreated等生命周期方法,基本就是加载布局设置ID的方法了。
下面我们跟到NavController.setGraph()中看下是怎样将我们设计的fragment添加进去的?
5.2 NavController
/**
* Sets the {@link NavGraph navigation graph} to the specified graph.
* Any current navigation graph data (including back stack) will be replaced.
*
* The graph can be retrieved later via {@link #getGraph()}.
*
* @param graph graph to set
* @see #setGraph(int, Bundle)
* @see #getGraph
*/
@CallSuper
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
我们看如果设置的graph不为null,它执行了popBackStackInternal,看注释的意思为从之前的就的graph栈弹出所有的graph:
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
.....
.....
boolean popped = false;
for (Navigator navigator : popOperations) {
if (navigator.popBackStack()) {
mBackStack.removeLast();
popped = true;
} else {
// The pop did not complete successfully, so stop immediately
break;
}
}
return popped;
}
果真remove掉了之前所有的naviagtor。而这个mBackStack是什么时候添加的navigator的呢?查看源码我们发现:
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
// 如果NavGraph不在栈内,先拿到父类Navgarph
ArrayDeque hierarchy = new ArrayDeque<>();
NavGraph parent = newDest.getParent();
while (parent != null) {
hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
parent = parent.getParent();
}
// 现在遍历后堆栈并查看哪些导航图已经在栈内
Iterator iterator = mBackStack.iterator();
while (iterator.hasNext() && !hierarchy.isEmpty()) {
NavDestination destination = iterator.next().getDestination();
if (destination.equals(hierarchy.getFirst().getDestination())) {
//destination 如果已经在栈顶,不需要再add了
hierarchy.removeFirst();
}
}
// Add all of the remaining parent NavGraphs that aren't
// already on the back stack
mBackStack.addAll(hierarchy);
//添加新的 destination
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest, finalArgs);
mBackStack.add(newBackStackEntry);
}
if (popped || newDest != null) {
dispatchOnDestinationChanged();
}
}
还记得这个方法吗?我们一般手动切换Fragment时可以调用这个方法,最后就是跟踪到这里。
findNavController().navigate(R.id.bottomNavSampleActivity)
同时,切换目标Fragment到栈顶。我们发现最后dispatchOnDestinationChanged()
这个方法,分发目标界面切换。有必要去跟一下,你可能会发现意想不到的东西:
/**
* Dispatch changes to all OnDestinationChangedListeners.
*
* If the back stack is empty, no events get dispatched.
*
* @return If changes were dispatched.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean dispatchOnDestinationChanged() {
// We never want to leave NavGraphs on the top of the stack
//noinspection StatementWithEmptyBody
while (!mBackStack.isEmpty()
&& mBackStack.peekLast().getDestination() instanceof NavGraph
&& popBackStackInternal(mBackStack.peekLast().getDestination().getId(), true)) {
// Keep popping
}
if (!mBackStack.isEmpty()) {
NavBackStackEntry backStackEntry = mBackStack.peekLast();
for (OnDestinationChangedListener listener :
mOnDestinationChangedListeners) {
listener.onDestinationChanged(this, backStackEntry.getDestination(),
backStackEntry.getArguments());
}
return true;
}
return false;
}
这里面分发了所有实现了OnDestinationChangedListener
接口的方法,继续跟踪,看看都哪些实现了这个接口呢?
只有一个类实现了AbstractAppBarOnDestinationChangedListener,看一下具体实现:
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
DrawerLayout drawerLayout = mDrawerLayoutWeakReference != null
? mDrawerLayoutWeakReference.get()
: null;
if (mDrawerLayoutWeakReference != null && drawerLayout == null) {
controller.removeOnDestinationChangedListener(this);
return;
}
CharSequence label = destination.getLabel();
if (!TextUtils.isEmpty(label)) {
......
......
matcher.appendTail(title);
//设置title
setTitle(title);
}
boolean isTopLevelDestination = NavigationUI.matchDestinations(destination,
mTopLevelDestinations);
if (drawerLayout == null && isTopLevelDestination) {
//设置icon
setNavigationIcon(null, 0);
} else {
//设置返回箭头状态
setActionBarUpIndicator(drawerLayout != null && isTopLevelDestination);
}
}
原来如此,到这里就应该清楚了,当我们切换Fragment时,大概流程如下:
到这里,基本的几个核心类以及相关实现我们基本了解了,下面我们看一下基本的流程,首先我们从入口进去,一点点跟进
5.3 Navigation.findNavController(this, R.id.fragment_home)
我们在最开始会初始化一个NavController:
@NonNull
public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
View view = ActivityCompat.requireViewById(activity, viewId);
NavController navController = findViewNavController(view);
.......
return navController;
}
@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
.........
}
return null;
}
@SuppressWarnings("unchecked")
@Nullable
private static NavController getViewNavController(@NonNull View view) {
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}
查看代码可以看到是通过一个tag值来找到的,那么什么时候设置的呢?还记得5.1里面介绍的NavHostFragment
的生命周期onViewCreated
么?
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
.......
View rootView = view.getParent() != null ? (View) view.getParent() : view;
Navigation.setViewNavController(rootView, mNavController);
}
在视图创建的时候调用了Naviagtion.setViewNavController()。NavController初始化好了之后,接下来将它和NavigationView、ToolBar、BottomNavigationView、DrawerLayout进行绑定:
5.4 setupActionBarWithNavController
不管是NavigationView还是Bottom``NavigationView,都会调用这个方法,他是AppCompatActivity的一个扩展方法,调用的是NavigationUI这个类:
public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
@NonNull NavController navController,
@NonNull AppBarConfiguration configuration) {
navController.addOnDestinationChangedListener(
new ActionBarOnDestinationChangedListener(activity, configuration));
}
可以看到它就是调用了目标切换的那个接口,用来实现标题按钮等状态的改变。查看它的方法实现:
我们看到它重载了很多方法,包括我们上面提到的NavigationView、ToolBar、BottomNavigationView、DrawerLayout。这样就将组件的状态切换绑定起来了,当fragment切换时,上面提到的接口分发,去切换布局按钮等状态。
5.5 navView.setupWithNavController(navController)
public static void setupWithNavController(@NonNull final NavigationView navigationView,
@NonNull final NavController navController) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
//目标页面是否被选中
boolean handled = onNavDestinationSelected(item, navController);
if (handled) {
//切换菜单状态、关闭抽屉
ViewParent parent = navigationView.getParent();
if (parent instanceof DrawerLayout) {
((DrawerLayout) parent).closeDrawer(navigationView);
} else {
BottomSheetBehavior bottomSheetBehavior =
findBottomSheetBehavior(navigationView);
if (bottomSheetBehavior != null) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
}
}
}
return handled;
}
});
final WeakReference weakReference = new WeakReference<>(navigationView);
navController.addOnDestinationChangedListener(
new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
NavigationView view = weakReference.get();
if (view == null) {
navController.removeOnDestinationChangedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
item.setChecked(matchDestination(destination, item.getItemId()));
}
}
});
}
最后就是状态切换了,当点击menu菜单或者目标Fragment切换的时候,改变状态。
考虑到我们开始如果直接从setupWithNavController 入口进行分析的话,可能不太容易找到怎么创建的graph布局中的fragment,以及NavHostFragment到底是什么,所以我们先分析了布局中的**NavHostFragment,我们发现为什么要在布局中声明了一个NavHostFragment,**它是用来做什么的,最后发现在它的生命周期中创建了一个NavController,并且添加了FragmentNavigator,同时setGraph了。
紧接着我们通过setGraph进入到了NavController类中,通过graph里面设置的初始fragment看到了切换栈内切换Fragment的代码。
在里面我们看到了熟悉的navigate()方法,在里面dispatchOnDestinationChanged()吸引了我的注意力,通过查找,发现切换Fragment之后,通过该方法去改变布局的状态,也就是OnDestinationChangedListener接口。
到这里基本的代码实现已经了解的差不多了,然后我回到了入口,通过初始化NavController,调用NavigationUI中的方法绑定NavigationView、ToolBar、BottomNavigationView、DrawerLayout等布局,在调用navigate()方法后,改变状态,整个流程就走通了。
可能有一些不合理的地方,望大家见谅,但是这是我此次的一个基本流程。
即学即用Android Jetpack - Navigation
Jetpack源码解析---看完你就知道Navigation是什么了?