Navigation深入浅出,到出神入化,再到实战改造(三)
改造Navigation
目标:
- 摒弃xml文件,用注解的方式管理路由节点。利用映射关系,动态生成路由节点配置文件
- 改造FragmentNavigator,,替换replace(),使用show(),hint()方式,路由Fragement
自定义注解处理器
1. 配置
gradle配置
//生成Json文件工具类
api 'com.alibaba:fastjson:1.2.59'
//注解处理器配置工具
api 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
如果想要注解处理器能够在编译器生成代码,需要做一个配置说明,这里有两种配置方法:
具体参考这篇文章:Java AbstractProcessor实现自定义ButterKnife
注解处理器基本用法
//auto.service:auto-service使用时要添加这个注解
@AutoService(Processor.class)
// 项目配置 当前正在使用的Java版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//要处理的注解类型的名称(这里必须是完整的包名+类名
@SupportedAnnotationTypes({"org.devio.hi.nav_annotation.Destination"})
public class NavProcessor extends AbstractProcessor {
@Override
void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//处理器被初始化的时候被调用
}
boolean process(Set annotations, RoundEnvironment roundEnv)
//处理器处理自定义注解的地方
return false
}
注解处理器的引用
//Kotkin项目用 kapt Java项目用 annotationProcessor
kapt project(path:'nav-compiler')
api project(path:'nav-annotations')
下面会将用的方法做介绍, ==关于更多注解处理器和相关知识,可参考这几篇文章:==
Java AbstractProcessor实现自定义ButterKnife
2. 创建项目
创建项目这个工程会默认生成Navigation+BottomNavigationView项目结构。项目内容比较简单。这里不过多介绍。我们就改造这个项目。
创建两个Java lib :
为什么需要创建Java库? 创建Java库是因为在使用自定义AbstractProcessor需要使用到javax包中的相关类和接口,这个在android库中并不存在,所以需要使用到Java库。
在nav_compiler
module下的build.gradle:
dependencies {
implementation fileTree(dir: 'libs', includes: ['*,jar'])
//自定义注解处理器相关依赖
//Json工具类
api 'com.alibaba:fastjson:1.2.59'
//让自定义处理器在编译时 能够被唤醒 能够执行
api 'com.google.auto.service:auto-service:1.0-rc6'
//添加我们定义的注解lib依赖
implementation project(path: ':nav_annotation')
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}
在nav_annotation
module下创建注解文件:
/**
* 自定义注解,将这个注解
* 注释到我们的要路由的类上面
* 这样我们就可以获取配置的节点(e.g Activity/Fragment/Dialog)
* 然后利用代码生成节点配置,替换掉nav_graph.xml;
*/
@Target(ElementType.TYPE)//类作用域
@Retention(RetentionPolicy.CLASS)//编译期生效
public @interface Destination {
/**
* 页面在路由中的名称
*/
String pareUrl();
/**
* 节点是不是默认首次启动页
*/
boolean asStarter() default false;
}
在这里我们有必要认识一下什么是Element。 在Java语言中,Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:
- PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
- ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
- TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
- VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
注解解释器具体代码如下:
/**
* @Author :ggxz
* @Date: 2022/3/5
* @Desc:
*/
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"org.devio.hi.nav_annotation.Destination"})
public class NavProcessor extends AbstractProcessor {
private static final String PAGE_TYPE_ACTIVITY = "Activity";
private static final String PAGE_TYPE_FRAGMENT = "Fragment";
private static final String PAGE_TYPE_DIALOG = "Dialog";
private static final String OUTPUT_FILE_NAME = "destination.json";
private Messager messager;
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//日志打印工具类
messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "enter init...");
//创建打印文件
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//获取代码中所有使用@Destination 注解的类或字段
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Destination.class);
if (!elementsAnnotatedWith.isEmpty()) {
Map<String, JSONObject> destMap = new HashMap<>();
handleDestination(elementsAnnotatedWith, destMap, Destination.class);
try {
//创建资源文件
FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME);
// 获取创建资源文件默认路径: .../app/build/intermediates/javac/debug/classes/目录下
// 希望存放的目录为: /app/main/assets/
String resourcePath = resource.toUri().getPath();
// 获取 .../app 之前的路径
String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
String assetsPath = appPath + "src/main/assets";
File file = new File(assetsPath);
if (!file.exists()) {
file.mkdirs();
}
String content = JSON.toJSONString(destMap);
File outputFile = new File(assetsPath, OUTPUT_FILE_NAME);
if (outputFile.exists()) {
outputFile.delete();
}
outputFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(outputFile);
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
writer.write(content);
writer.flush();
outputStream.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private void handleDestination(Set<? extends Element> elements, Map<String, JSONObject> destMap, Class<Destination> aClass) {
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element;
//全类名
String clzName = typeElement.getQualifiedName().toString();
Destination annotation = typeElement.getAnnotation(aClass);
String pageUrl = annotation.pageUrl();
boolean asStart = annotation.asStart();
//获取目标页的id 用全类名的hasCode
int id = Math.abs(clzName.hashCode());
//获取 注解标记的类型(Fragment Activity Dialog)
String destType = getDestinationType(typeElement);
if (destMap.containsKey(pageUrl)) {
messager.printMessage(Diagnostic.Kind.ERROR, "不同页面不允许使用相同的pageUrl:" + pageUrl);
} else {
JSONObject jsonObject = new JSONObject();
jsonObject.put("pageUrl", pageUrl);
jsonObject.put("asStarter", asStart);
jsonObject.put("id", id);
jsonObject.put("destType", destType);
jsonObject.put("clzName", clzName);
destMap.put(pageUrl, jsonObject);
}
}
}
private String getDestinationType(TypeElement typeElement) {
//父类型
TypeMirror typeMirror = typeElement.getSuperclass();
//androidx.fragment.app.Fragment
String superClzName = typeMirror.toString();
if (superClzName.contains(PAGE_TYPE_ACTIVITY.toLowerCase())) {
return PAGE_TYPE_ACTIVITY.toLowerCase();
} else if (superClzName.contains(PAGE_TYPE_FRAGMENT.toLowerCase())) {
return PAGE_TYPE_FRAGMENT.toLowerCase();
} else if (superClzName.contains(PAGE_TYPE_DIALOG.toLowerCase())) {
return PAGE_TYPE_DIALOG.toLowerCase();
}
//1. 这个父类型是类的类型,或是接口的类型
if (typeMirror instanceof DeclaredType) {
Element element = ((DeclaredType) typeMirror).asElement();
//如果这个父类的类型 是类的类型
if (element instanceof TypeElement) {
//递归调用自己
return getDestinationType((TypeElement) element);
}
}
return null;
}
}
主项目引用:
api project(':nav_annotation')
kapt project(':nav_compiler')
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//添加这句
id 'kotlin-kapt'
}
在路由节点页面添加:
@Destination(pageUrl = "main/tabs/home", asStarter = true)
class HomeFragment : Fragment() {}
@Destination(pageUrl = "main/tabs/notifications", asStarter = false)
class NotificationsFragment : Fragment() {}
@Destination(pageUrl = "main/tabs/dashboard", asStarter = false)
class DashboardFragment : Fragment() {
点击build
->Rebuild Projiect
,就可可以看到assets目录下生成的destination.json
文件:
{
"main/tabs/dashboard": {
"asStarter": false,
"pageUrl": "main/tabs/dashboard",
"id": 1537160370,
"clzName": "org.devio.proj.navigatorrouter.ui.dashboard.DashboardFragment",
"destType": "fragment"
},
"main/tabs/home": {
"asStarter": true,
"pageUrl": "main/tabs/home",
"id": 524823610,
"clzName": "org.devio.proj.navigatorrouter.ui.home.HomeFragment",
"destType": "fragment"
},
"main/tabs/notifications": {
"asStarter": false,
"pageUrl": "main/tabs/notifications",
"id": 1214358362,
"clzName": "org.devio.proj.navigatorrouter.ui.notifications.NotificationsFragment",
"destType": "fragment"
}
}
接下来就开始加载这个文件,把他替换成mobile_navigation.xml。在解析加载之前,再次强调下,为什么要这么做。最终我们的目的是,通过此Json来配置我们的路由。进行统一管理,解耦。解决不够灵活,摆脱繁琐的xml文件编写。使得开发阶段可以使用注解。编译时自动扫描配置,运行时自行管理页面映射。
接下来我们开始解析这个destination.json
文件
1. 重写FragmentNavigator replace()替换成show()/hide()
创建HiFragmentNavigator 类,并将FragmentNavigator 全部粘贴过去,同时修改public NavDestination navigate()方法的逻辑如下:
@Navigator.Name("hifragment")//1
public class HiFragmentNavigator extends Navigator<HiFragmentNavigator.Destination> {
@Nullable
@Override
public NavDestination navigate(@NonNull HiFragmentNavigator.Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
... 省略
//2
// Fragment frag = instantiateFragment(mContext, mFragmentManager,
// className, args);
//这里每次都会利用反射去实例化对象 这里我改成用Tag标记
//className=android.fragment.app.homeFragment tag=HomeFragment
String tag = className.substring(className.lastIndexOf(".") + 1);
//不要每次都实例化对象
Fragment frag = mFragmentManager.findFragmentByTag(tag);
if (frag == null) {
frag = instantiateFragment(mContext, mFragmentManager,
className, args);
}
//替換成 show() hide()
// ft.replace(mContainerId, frag);
//3
if (!frag.isAdded()) {
ft.add(mContainerId, frag, tag);
}
List<Fragment> fragments = mFragmentManager.getFragments();
for (Fragment fragment : fragments) {
//把其他的全部隐藏
ft.hide(fragment);
}
//展示的页面
ft.show(frag);
... 省略
//4
ft.setReorderingAllowed(true);
ft.commit();
}
}
- Navigator要求子类,类头必须添加@Navigator.Name注解标识,参考其他子类可知
- 每次都会利用反射去实例化对象 这里改成用Tag标记,随后恢复
- 避免反复创建 添加。使用hide()/show()方式
注
不需要commit() - 方法的最后会 ft.commit();
2. 创建Destination实体类
与NavProcessor中创建的Json文件中的实体,字段一一对应
public class Destination {
public String pageUrl; //页面url
public int id; //路由节点(页面)的id
public boolean asStarter;//是否作为路由的第一个启动页
public String destType;//路由节点(页面)的类型,activity,dialog,fragment
public String clzName;//全类名
}
3. 创建NavUtil解析类
/**
* key:pageUrl value:Destination
*/
private static HashMap<String, Destination> destinationHashMap;
/**
* 由于我们删除掉mobile_navigation.xml文件,那我们就需要自己处理解析流程,然后把节点和各个类进行关联
* 赋值给NavGraph
*
* @param activity 上下文
* @param controller 控制器
* @param childFragmentManager 必须是childFragmentManager 源码中创建FragmentNavigator和DialogNavigator都是用的它
* @param containerId activity.xml中装载NavHostFragment的id
*/
public static void buildNavGraph(FragmentActivity activity,
@NonNull NavController controller,
FragmentManager childFragmentManager,
int containerId) {
//获取json文件内容
String content = parseFile(activity, "destination.json");
//json文件映射成实体HashMap
destinationHashMap = JSON.parseObject(content, new TypeReference<HashMap<String, Destination>>() {
}.getType());
/**
* 创建NavGraph 它是解析mobile_navigation.xml文件后,存储所有节点的Destination
* 我们解析的Destination节点,最终都要存入NavGraph中
*/
// 获取Navigator管理器中的Map 添加Destination
NavigatorProvider navigatorProvider = controller.getNavigatorProvider();
//创建NavGraphNavigator 跳转类
NavGraphNavigator navigator = new NavGraphNavigator(navigatorProvider);
// 最终目的是创建navGraph
NavGraph navGraph = new NavGraph(navigator);
//创建我们自定义的FragmentNavigator
HiFragmentNavigator hiFragmentNavigator = new HiFragmentNavigator(activity, childFragmentManager, containerId);
//添加到Navigator管理器中
navigatorProvider.addNavigator(hiFragmentNavigator);
//获取所有value数据
Iterator<Destination> iterator = destinationHashMap.values().iterator();
while (iterator.hasNext()) {
Destination destination = iterator.next();
if (destination.destType.equals("activity")) {
//如果是activity类型,上节源码中分析,它的必要参数是ComponentName
ActivityNavigator activityNavigator = navigatorProvider.getNavigator(ActivityNavigator.class);
//通过activityNavigator得到ActivityNavigator.Destination
ActivityNavigator.Destination node = activityNavigator.createDestination();
node.setId(destination.id);
node.setComponentName(new ComponentName(activity.getPackageName(), destination.clzName));
//添加到我们的navGraph对象中 它存储了所有的节点
navGraph.addDestination(node);
} else if ((destination.destType.equals("fragment"))) {
HiFragmentNavigator.Destination node = hiFragmentNavigator.createDestination();
node.setId(destination.id);
node.setClassName(destination.clzName);
navGraph.addDestination(node);
} else if (destination.destType.equals("dialog")) {
DialogFragmentNavigator dialogFragmentNavigator = navigatorProvider.getNavigator(DialogFragmentNavigator.class);
DialogFragmentNavigator.Destination node = dialogFragmentNavigator.createDestination();
node.setId(destination.id);
node.setClassName(destination.clzName);
navGraph.addDestination(node);
}
//如果当前节点
if (destination.asStarter) {
navGraph.setStartDestination(destination.id);
}
}
// 视图navGraph和controller 相关联
controller.setGraph(navGraph);
}
private static String parseFile(Context context, String fileName) {
AssetManager assetManager = context.getAssets();
StringBuilder builder = null;
try {
InputStream inputStream = assetManager.open(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
inputStream.close();
reader.close();
return builder.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* main_tabs_config.json 通常由服务器下发,告知我们那些menu需要展示
* 自定义BottomBar的目的是 让Tab和Destination建立映射关系
* 根据pageUrl断定那个menu对应那个Destination
*
* 也就是bottom_nav_menu.xml文件 中的配置 按照对应要求 改成json文件后端下发
*/
public static void builderBottomBar(BottomNavigationView navView) {
String content = parseFile(navView.getContext(), "main_tabs_config.json");
BottomBar bottomBar = JSON.parseObject(content, BottomBar.class);
List<BottomBar.Tab> tabs = null;
tabs = Objects.requireNonNull(bottomBar).tabs;
Menu menu = navView.getMenu();
for (BottomBar.Tab tab : tabs) {
if (!tab.enable)
continue;
Destination destination = destinationHashMap.get(tab.pageUrl);
if (destinationHashMap.containsKey(tab.pageUrl)) {//pageUrl对应不上 则表示无此页面
//对应页面节点的destination.id要和menuItem id对应
if (destination!=null){
MenuItem menuItem = menu.add(0, destination.id, tab.index, tab.title);
menuItem.setIcon(R.drawable.ic_home_black_24dp);
}
}
}
}
}
此方法提供两种能力buildNavGraph()
- 将Json文件看成原来的mobile_navigation.xml文件,由于是我们自定义的Json,Navigation无法解析,所以我们要解析成节点,封装成NavGraph(存储导航文件所有节点信息),然后按照解析流程,封装成不同的Destination。然后与controller形成联系。==注意== 值得注意的是,生成FragmentNavigator.Destination时,要用我们自定义的HiFragmentNavigator
- 提供页面MenuItem动态设置能力。文件服务端下发,这样。我们在显示时,就可以指定有个页面,显示与否。比如某个页面未实名不显示。后台直接下发的文件,不包含这个节点,或是我们可以用代码进行拦截。 数据与路由配置Json文件内容映射对应,如下:
{
"selectTab": 0,
"tabs": [
{
"size": 24,
"enable": true,
"index": 0,
"pageUrl": "main/tabs/home",
"title": "Home"
},
{
"size": 24,
"enable": true,
"index": 1,
"pageUrl": "main/tabs/dashboard",
"title": "Dashboard"
},
{
"size": 40,
"enable": false,
"index": 2,
"pageUrl": "main/tabs/notification",
"title": "Notification"
}
]
}
对应实体:
public class BottomBar {
public int selectTab;//默认选中下标
public List<Tab> tabs;
public static class Tab {
/**
* size : 24 按钮的大小
* enable : true 是否可点击 不可点击则隐藏
* index : 0 在第几个Item上
* pageUrl : main/tabs/home 和路由节点配置相同,不存在则表示无此页面
* title : Home 按钮文本
*/
public int size;
public boolean enable;
public int index;
public String pageUrl;
public String title;
}
}
activity.xml删除一下两项:
app:menu="@menu/bottom_nav_menu""
app:navGraph="@navigation/mobile_navigation"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
解绑mobile_navigation.xml
文件 解绑app:menu="@menu/bottom_nav_menu
文件 MainAcivity.class代码:
val navController = findNavController(R.id.nav_host_fragment_activity_main)
//NavHostFragment 容器
val fragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main)
NavUtil.buildNavGraph(
this,
navController,
fragment!!.childFragmentManager,
//容器 id
R.id.nav_host_fragment_activity_main
)
//创建底部按钮 删除app:menu="@menu/bottom_nav_menu" 配置
NavUtil.builderBottomBar(navView)
//跳转itemId就是我们在builderBottomBar中 MenuItem的 destination.id --> menuItem = menu.add(0, destination.id, tab.index, tab.title);的
navView.setOnItemSelectedListener { item ->
navController.navigate(item.itemId)
true
}
现在Navigation无须xml配置,路由注解即可实现,切换不会重建Fragment和重建View,支持tab高定制联动功能。
主要说明都是方法中。实现此功能要求对Navgiation源码有足够的了解,和自定义注解器相关知识。看代码如果难懂,下面对面几篇文章并附送源码:
Navigation深入浅出,到出神入化,再到实战改造(一)
Navigation深入浅出,到出神入化,再到实战改造(二)
Java AbstractProcessor实现自定义ButterKnife
Java AbstractProcessor实现自定义ButterKnife
Github地址 AS4.1以上