改造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源碼有足夠的了解,和自定義注解器相關知識??创a如果難懂,下面對面幾篇文章并附送源碼:
Navigation深入淺出,到出神入化,再到實戰改造(一)
Navigation深入淺出,到出神入化,再到實戰改造(二)
Java AbstractProcessor實現自定義ButterKnife
Java AbstractProcessor實現自定義ButterKnife
Github地址 AS4.1以上