前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Navigation深入浅出,到出神入化,再到实战改造(三)

Navigation深入浅出,到出神入化,再到实战改造(三)

作者头像
g小志
发布2022-03-29 15:10:06
4320
发布2022-03-29 15:10:06
举报
文章被收录于专栏:Android常用基础Android常用基础

改造Navigation

目标:
  1. 摒弃xml文件,用注解的方式管理路由节点。利用映射关系,动态生成路由节点配置文件
  2. 改造FragmentNavigator,,替换replace(),使用show(),hint()方式,路由Fragement

自定义注解处理器

1. 配置

gradle配置

代码语言:javascript
复制
//生成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

注解处理器基本用法

代码语言:javascript
复制
//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
}

注解处理器的引用

代码语言:javascript
复制
//Kotkin项目用 kapt Java项目用 annotationProcessor 
kapt project(path:'nav-compiler')
api project(path:'nav-annotations')

下面会将用的方法做介绍, ==关于更多注解处理器和相关知识,可参考这几篇文章:==

Java进阶--编译时注解处理器(APT)详解

Java AbstractProcessor实现自定义ButterKnife

JavaPoet的使用指南

Android AutoService 组件化

2. 创建项目

创建项目

这个工程会默认生成Navigation+BottomNavigationView项目结构。项目内容比较简单。这里不过多介绍。我们就改造这个项目。

创建两个Java lib :

在这里插入图片描述

为什么需要创建Java库? 创建Java库是因为在使用自定义AbstractProcessor需要使用到javax包中的相关类和接口,这个在android库中并不存在,所以需要使用到Java库。

nav_compiler module下的build.gradle:

代码语言:javascript
复制
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下创建注解文件:

代码语言:javascript
复制
/**
 * 自定义注解,将这个注解
 * 注释到我们的要路由的类上面
 * 这样我们就可以获取配置的节点(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 常量、方法或构造方法参数、局部变量或异常参数。

注解解释器具体代码如下:

代码语言:javascript
复制
/**
 * @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;
    }


}

主项目引用:

代码语言:javascript
复制
    api project(':nav_annotation')
    kapt project(':nav_compiler')
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    //添加这句
    id 'kotlin-kapt'
}

在路由节点页面添加:

代码语言:javascript
复制
@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文件:

代码语言:javascript
复制
{
  "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()方法的逻辑如下:

代码语言:javascript
复制
@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();
    }
}
  1. Navigator要求子类,类头必须添加@Navigator.Name注解标识,参考其他子类可知
  2. 每次都会利用反射去实例化对象 这里改成用Tag标记,随后恢复
  3. 避免反复创建 添加。使用hide()/show()方式 不需要commit()
  4. 方法的最后会 ft.commit();

2. 创建Destination实体类

与NavProcessor中创建的Json文件中的实体,字段一一对应

代码语言:javascript
复制
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解析类

代码语言:javascript
复制
/**
     * 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()

  1. 将Json文件看成原来的mobile_navigation.xml文件,由于是我们自定义的Json,Navigation无法解析,所以我们要解析成节点,封装成NavGraph(存储导航文件所有节点信息),然后按照解析流程,封装成不同的Destination。然后与controller形成联系。==注意== 值得注意的是,生成FragmentNavigator.Destination时,要用我们自定义的HiFragmentNavigator
  2. 提供页面MenuItem动态设置能力。文件服务端下发,这样。我们在显示时,就可以指定有个页面,显示与否。比如某个页面未实名不显示。后台直接下发的文件,不包含这个节点,或是我们可以用代码进行拦截。 数据与路由配置Json文件内容映射对应,如下:
代码语言:javascript
复制
{
  "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"

代码语言:javascript
复制
<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代码:

代码语言:javascript
复制
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进阶--编译时注解处理器(APT)详解

Java AbstractProcessor实现自定义ButterKnife

JavaPoet的使用指南

Android AutoService 组件化

Github地址 AS4.1以上

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022.03.09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 改造Navigation
    • 目标:
      • 自定义注解处理器
        • 1. 配置
        • 2. 创建项目
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档