Navigation深入淺出,到出神入化,再到實戰改造(三)

改造Navigation

目標:

  1. 摒棄xml文件,用注解的方式管理路由節點。利用映射關系,動態生成路由節點配置文件
  2. 改造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進階--編譯時注解處理器(APT)詳解

Java AbstractProcessor實現自定義ButterKnife

JavaPoet的使用指南

Android AutoService 組件化

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();
    }
}
  1. Navigator要求子類,類頭必須添加@Navigator.Name注解標識,參考其他子類可知
  2. 每次都會利用反射去實例化對象 這里改成用Tag標記,隨后恢復
  3. 避免反復創建 添加。使用hide()/show()方式 不需要commit()
  4. 方法的最后會 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()

  1. 將Json文件看成原來的mobile_navigation.xml文件,由于是我們自定義的Json,Navigation無法解析,所以我們要解析成節點,封裝成NavGraph(存儲導航文件所有節點信息),然后按照解析流程,封裝成不同的Destination。然后與controller形成聯系。==注意== 值得注意的是,生成FragmentNavigator.Destination時,要用我們自定義的HiFragmentNavigator
  2. 提供頁面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源碼有足夠的了解,和自定義注解器相關知識??创a如果難懂,下面對面幾篇文章并附送源碼:

Navigation深入淺出,到出神入化,再到實戰改造(一)

Navigation深入淺出,到出神入化,再到實戰改造(二)

Java AbstractProcessor實現自定義ButterKnife

Java進階--編譯時注解處理器(APT)詳解

Java AbstractProcessor實現自定義ButterKnife

JavaPoet的使用指南

Android AutoService 組件化

Github地址 AS4.1以上

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容