Router:一款單品、元件化、外掛化全支援的路由框架

Haoge發表於2018-02-06

簡介

由於現在已經有很多各種各樣的路由框架了,所以在這裡。我也不再贅述什麼是路由?路由框架的意義是什麼之類的了。

特性

  • 安全: 路由啟動過程中。全程catch住異常並通知使用者。完全不用擔心crash問題。
  • 強大的攔截器功能:與大部分的路由不同。提供三種路由攔截器機制,對應不同業務下使用。
  • 方便: 使用apt註解生成路由表,配置方便,易維護。
  • 靈活: 配置路由表方式多樣,滿足你在任意條件下進行使用。
  • 支援兩種路由:頁面路由與動作路由。
  • 支援重啟路由:路由被攔截後。可通過一行程式碼無縫恢復重啟路由。在登入檢查中會很有用。
  • 高度可定製:單品、元件化完美支援,對於外掛化環境。也可以針對性的定製使用。

用法

點選前往Github頁

本篇文章主要介紹Router在單品、元件化環境下的使用方式,針對外掛化環境下的使用適配。請參考以下文章:

Router: 教你如何進行任意外掛化環境下的路由適配

依賴

  • 新增jitpack倉庫
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}
複製程式碼
  • 新增依賴
compile "com.github.yjfnypeu.Router:router-api:2.6.0"
annotationProcessor "com.github.yjfnypeu.Router:router-compiler:2.6.0"
複製程式碼

路由表

路由表的定義

要理解什麼是路由表。需要先明確以下幾點定義:

路由對映: 一組特定url特定頁面的對映。比如www.baidu.com對映的是百度頁面。

路由表: 也叫路由對映表,一個用於儲存所有的路由對映的容器。

路由: 是指通過一個url在路由表中匹配到對應的頁面。並完成啟動的過程。稱為一次路由。

以下就是一個簡單的路由流程:

Router:一款單品、元件化、外掛化全支援的路由框架

所以對於Android端來說。路由表可以理解為一系列特定url與特定Activity之間的對映集合

建立路由表

與很多其他的路由框架不同。此路由框架並未使用自動註冊路由表的方式來做。因為自動註冊路由表在靈活性上有所欠缺。

建立路由表分為兩種方式:手動建立使用註解在編譯時動態生成路由表。

這裡主要介紹通過使用註解動態生成的建立方式。手動建立路由表的使用場景只在部分不支援執行時註解的編譯環境下推薦使用。

上面提到了,一個路由對映是一組特定url與特定頁面之間的對映關係。所以通過對指定頁面。新增註解配置上指定url。即可得到一組對應的路由對映:

@RouterRule("haoge://page/user")
public class UserActivity extends Activity {
	...
}
複製程式碼

新增好此註解後。即可對專案觸發一次編譯。使其自動生成對應的路由表類:

自動生成的路由表,類名為RouterRuleCreator:

// 此類為編譯時註解自動生成的類。
public class RouterRuleCreator implements RouteCreator {
  @Override
  public Map<String, ActivityRouteRule> createActivityRouteRules() {
    Map<String,ActivityRouteRule> routes = new HashMap<>();
    routes.put("haoge://page/user", new ActivityRouteRule(UserActivity.class));
    return routes;
  }

  @Override
  public Map<String, ActionRouteRule> createActionRouteRules() {
    Map<String,ActionRouteRule> routes = new HashMap<>();
    ...
    return routes;
  }
}
複製程式碼

PS:如果當前環境不支援編譯時註解。可以選擇手動建立此RouteCreator路由表例項類進行使用。

註冊路由表

生成具體的路由表類後。即可通過以下程式碼進行路由表註冊了:

RouterConfiguration.get().addRouteCreator(new RouterRuleCreator());
複製程式碼

註冊成功之後。則通過以下方式進行啟動:

Router.create(url).open(context);
複製程式碼

以上是最簡單的路由配置及用法。下面將一步步的更深入的介紹更多用法

一對多

對於同一個頁面。可以配置多個不重複的路由連結:

@RouterRule({url1, url2, url3})
public class ExampleActivity extends Activity {
	...
}
複製程式碼

頁面內獲取啟動的uri

所有的路由啟動事件,都會將啟動的url連結,存入bundle中進行傳遞。可通過以下key值進去讀取:

Uri uri = getIntent().getParcelableExtra(Router.RAW_URI);
複製程式碼

配置baseUrl

一般來說:一個app所定義使用的路由url都會有個特定的字首。而如果是在外掛化環境下。也推薦對各個外掛分別定義一份獨有的路由字首。原因將在下一篇介紹外掛化環境路由配置的文章中進行具體說明。

框架提供RouteConfig註解。一個module只能配置一次且必須配置於Application子類之上:

@RouteConfig(baseUrl="haoge://page/")
public class App extends Application {
	...
}
複製程式碼

下表是路由字首與路由地址之前的匹配關係,橫排表示RouteRule配置的路由地址,豎排表示baseUrl:

Router:一款單品、元件化、外掛化全支援的路由框架

自動解析url引數

Router的自動引數解析。是結合的Parceler框架來進行使用的,關於Parceler框架的介紹可以參考下方的連結:

Parceler: 優雅的使用Bundle進行資料存取就靠它了!

請注意:Parceler框架並不是Router所必須依賴的框架。只是新增此框架使用後,能使用更強大的特性。

如果不使用的話。所有的url引數。都將預設解析為String並進行傳遞。

  1. 引數自動轉換:

首先。我們先在Activity基類中配置注入入口,配置此入口後。就會自動從intent中讀取對應的資料。注入到子類中的被Arg註釋過的成員變數中去了:

public class BaseActivity extends Activity {

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Parceler.toEntity(this,getIntent());
    }
} 
複製程式碼

假設當前我們有以下一個頁面:

@RouterRule("haoge://page/example")
public class ExampleActivity extends BaseActivity {
	@Arg
	String name;
	@Arg
	long id;
	@Arg
	boolean isLogin;
	...
}
複製程式碼

可以看到。這個頁面含有三個屬性。並都新增了Arg註解。那麼此時我們可以以下方的連結來進行跳轉傳參:

Router.create("haoge://page/example?name=haoge&id=10086&isLogin=false").open(context);
複製程式碼

連結中的引數將會自動根據Arg註解的型別進行自動轉換。並在轉換後轉載入Intent中進行傳遞. 即相當於以下的操作:

String url = "haoge://page/example?name=haoge&id=10086&isLogin=false";
Uri uri = Uri.parse(url);
Bundle bundle = new Bundle;
bundle.putString("name", uri.getQueryParameter("name"))
bundle.putLong("id", 
	Long.parseLong(uri.getQueryParameter("id")));
bundle.putBoolean("isLogin", 
	Boolean.parseBoolean(uri.getQueryParameter("isLogin")));

// 啟動路由並傳遞bundle
...
複製程式碼

此種自動轉換型別的引數。只支援基本資料型別。若Arg所註釋的屬性型別不為基本資料型別。則不觸發自動轉換,將讀取的String串直接存入Intent中。

  1. 傳遞複雜引數

但是很多時候我們的傳引數據又不止是基本資料型別。比如說普通實體bean。比如說一個列表。所以這就是Parceler展現光芒的時候了!

Parceler自帶JSON資料轉換功能。對於傳遞的是非基本資料型別的。則可以在引數中傳遞此型別的json串:這也是為什麼推薦引入Parceler框架進行使用的原因:小而強大!

複雜引數需要使用Parceler的轉換功能,關於資料轉換器。也在上面那篇文章中有描述。這裡我使用的是FastJson的轉換器:

Parceler.setDefaultConverter(FastJsonConverter.class);
複製程式碼

首先假設當前有個此頁面, 需要傳參為普通實體類User:

@RouterRule("usercenter")
public class UserCenterActivity extends BaseActivity{
	@Arg
	User user;
}

public class User {
	public String username;
	public String password;
	...
}
複製程式碼

那麼就可以通過以下連結進行跳轉:

// 這裡為了理解方便,我沒有直接拼裝連結。
User user = new User("router", "123456");
String json = JSON.toJSONString(user);
// 對json串需要先進行url編碼。
String encodeJson = URLEncoder.encode(json);
String url = String.format("haoge://page/usercenter?user=%s", encodeJson);
Router.create(url).open(context);
複製程式碼

可以看到,通過此種方式,可以直接傳遞具體的json資料進行傳遞。請注意對於連結中的json資料。一定要先進行encode編碼。避免內部uri解析異常。

由於目標頁對應的user型別不為基本資料型別。所以此處將直接將user所指代的值。自動解碼後直接放入Intent中傳遞入目標頁。目標頁中則會使用Parceler自動將此json轉換解析成User類。完成複雜引數的傳遞

動作路由

上面所介紹的。都是通過一個連結。開啟一個對應的頁面。此種路由跳轉稱為頁面路由。

Router也支援另一種路由:動作路由,此種路由沒有頁面跳轉。只是用於做一些特殊的操作。

比如說:加入購物車、清空購物車資料、退出登入等。

@RouterRule("shopcar.clear")
public class ClearShopcarAction extends ActionSupport {
	@Override
    public void onRouteTrigger(Context context, Bundle bundle) {
        // TODO 清空購物車操作
    }
}
複製程式碼

然後即可通過以下連結觸發清空購物車操作:

Router.create("haoge://page/shopcar.clear").open(context);
複製程式碼

額外請求引數

上面舉的例子。都是全部資料通過一個url直接傳遞。但是很多時候。我們是需要在此url的基礎上。再額外新增一些別的資料進行傳遞的。比如轉場動畫配置、requestCode配置、Intent.flags配置等.

Router.create(url)
	.addExtras(bundle) // 新增額外bundle資料引數
	.requestCode(code) // 用於startActivityForResult
	.setAnim(enterAnim, exitAnim)// 轉場動畫
	.addFlags(flag)// intent.addFlags(flag);
	.addInterceptor(interceptor)// 新增攔截器
	.setCallback(callback)// 設定路由回撥
	.open(context);
複製程式碼

新增路由回撥

在講路由表的時候有提到過,路由是一種特殊的啟動流程,且啟動不一定成功。所以很自然的,這個時候就需要有一個路由回撥介面。

路由回撥介面為RouteCallback類:

public interface RouteCallback {
	// 當路由定址失敗時。觸發此回撥
	void notFound(Uri uri, NotFoundException e);
	// 當路由啟動成功時。觸發此回撥
	void onOpenSuccess(Uri uri, RouteRule rule);
	// 當路由啟動失敗時。觸發此回撥。包括
	void onOpenFailed(Uri uri, Throwable e);
}
複製程式碼

路由回撥配置分為兩種:

  • 全域性路由回撥:所有的路由啟動事件。都會觸發此回撥
RouterConfiguration.get().setCallback(callback);
複製程式碼
  • 區域性路由回撥:只被當前路由啟動觸發。
Router.create(url)
	.setCallback(callback)
	.open(context);
複製程式碼

路由回撥在進行頁面跳轉埋點時,會是非常有用的一個特性。

日誌列印

當配置Router.DEBUG為true時(預設為false)。框架將會啟用內部日誌輸出。建議使用BuildConfig.DEBUG進行日誌開關控制, 達到在只在開發時進行日誌輸出的作用:

Router.DEBUG = BuildConfig.DEBUG
複製程式碼

日誌可通過RouterLog進行過濾檢視

Router:一款單品、元件化、外掛化全支援的路由框架

攔截器

顧名思義:攔截器就是用於在路由啟動過程中,進行一系列的提前檢查,當檢查不符合規則時,則使此次路由啟動失敗。

舉個最經典的案例:登入檢查

Router:一款單品、元件化、外掛化全支援的路由框架

當不使用路由進行跳轉時,這種情況就會導致你本地寫上了大量的登入判斷邏輯程式碼。這在維護起來是很費勁的。而且也非常不靈活,而使用攔截器的方式來做登入檢查,就會很方便了:

Router:一款單品、元件化、外掛化全支援的路由框架

下面是一個簡單的登入攔截實現:

// 實現RouteInterceptor介面
public class LoginInterceptor implements RouteInterceptor{
    @Override
    public boolean intercept(Uri uri, RouteBundleExtras extras, Context context){
    	// 未登入時進行攔截
        return !LoginChecker.isLogin();
    }

    @Override
    public void onIntercepted(Uri uri, RouteBundleExtras extras, Context context) {
    	// 攔截後跳轉登入頁並路由資訊傳遞過去,便於登入後進行恢復
        Intent loginIntent = new Intent(context,LoginActivity.class);
        // uri為路由連結
        loginIntent.putExtra("uri",uri);
        // extras中裝載了所有的額外配置資料
        loginIntent.putExtra("extras",extras);
        context.startActivity(loginIntent);
    }
}
複製程式碼
public class LoginActivity extends BaseActivity {

	@Arg
	Uri uri;
	@Arg
	RouteBundleExtras extras;
	
	void onLoginSuccess() {
		if(uri != null) {
			// 登入成功。使用此方法直接無縫恢復路由啟動
			Router.resume(uri, extras).open(context);
		}
		finish();
	}
}
複製程式碼

攔截器功能是Router框架的重點,且經過長時間的迭代。Router目前提供三種攔截器提供使用,你可以根據你自己的需要。靈活的選擇使用什麼型別的攔截器。

1. 全域性預設攔截器:

設定方式:RouterConfiguration.get().setInterceptor(interceptor);

作用域:此全域性預設攔截器。將會被所有啟動的路由事件所觸發。

推薦使用場景

一些需要進行全域性判斷的檢查:比如登入檢查等。且最好此處所配置的攔截器。新增上對應的開關控制。

比如說做登入檢查的,控制如果有requestlogin引數的才啟用登入檢查。將登入檢查控制交給提供url的地方。

2. 針對某次路由所特別設定的攔截器

設定方式:Router.create(url).addInterceptor(interceptor);

作用域:只被此處所建立的路由觸發。

推薦使用場景:一些只在此處啟動路由時才需要觸發的檢查:比如deeplink外部連結入口處,檢查外部連結是否合法等。

3. 針對某個目標路由所特別設定的攔截器

設定方式:在配置了RouterRule的目標類上。新增@RouteInterceptor()註解。將需要配置的註解Class加入:

@RouteInterceptors(CustomInterceptors.class)
@RouterRule(rule)
public class ExampleActivity extends Activity {}
複製程式碼

作用域:當路由url所匹配的目標路由為此路由時被觸發

推薦使用場景:針對此頁面跳轉的的檢查,比如對傳遞引數進行過濾,避免傳入無效資料導致不可期異常等。

此三種攔截器,觸發的優先順序為:全域性預設 > 某次路由 > 某個目標路由,且若當此路由已被某個攔截器攔截了。則將不會繼續觸發後續攔截器

Router在元件化環境下進行使用

對於元件化中使用Router框架。可以參考此處放於github上的元件化demo

可結合上方demo與下方描述一同檢視。達到更加方便理解的作用!

在元件化環境下使用路由框架。主要需要考慮以下幾點:

註冊多個路由表

由於Router本身沒有使用自動註冊的方式來進行路由表註冊。而是提供了相應的介面、相應的方法來手動註冊,這種配置方式本身即可做到動態註冊多個路由表的效果:

RouterConfiguration.get().addRouteCreator(routeCreator);
複製程式碼

所以在元件化中進行使用。只要想辦法註冊多個元件自身生成的路由表即可。

啟用業務元件的註解處理器

路由表類的生成,是在編譯時自動生成的。而編譯時生成框架的作用域只在當前module中。所以需要將Router的註解處理器,分別配置新增到每個業務元件之中,使每個業務元件都能夠使用註解生成自身的路由表類:

annotationProcessor "com.github.yjfnypeu.Router:router-compiler:2.6.0"
複製程式碼

而元件化中,對於使用的註解處理器。推薦的做法是將所有需要使用的註解處理器都抽離到一個統一的gradle指令碼中。然後由各個元件直接apply應用即可:

建立processor.gradle:

dependencies {
    annotationProcessor "com.github.yjfnypeu.Router:router-compiler:2.6.0"
    ...// 所有註解處理器均放置於此配置
}
複製程式碼

然後在元件中的build.gradle中直接通過apply from方法應用此指令碼即可。

這樣的做法有以下幾點好處:

  1. 由於編譯時註解的註解處理器。是直接提供給IDE進行使用的。並不會再打包時將對應的程式碼打包進apk中。所以不用擔心引入額外的不需要的程式碼進入apk中。
  2. 便於版本升級統一控制

避免多個元件生成的路由表衝突

在單品環境下使用時。有介紹會在編譯時生成一個具體的路由表類RouterRuleCreator, 而這個類生成的包名是預設寫死的:com.lzh.router.

所以在多元件環境下進行使用時,需要對每個module指定不同的生成路由表類的包名。避免出現重複類衝突問題:

@RouteConfig(pack="com.router.usercenter")
public class UCApp extends Application {
	...
}
複製程式碼

RouteConfig註解不止提供單品中使用的baseUrl方法進行路由字首配置。也提供pack方法。用於指定此module所生成的路由表的具體包名。所以對於元件化環境。只要對不同元件指定不同的包名即可!

推薦的註冊方式

由於元件化其實所有元件都是被app殼所載入的。並不像外掛化那樣會出現按需載入的情況。所以這種環境下,多路由表的註冊方式,推薦使用反射,一次性將所有有效元件全部載入的方式進行使用:

private void loadRouteRulesIfExist() {
	// 此packs為所有元件中定義的路由表類生成包名集。
	String[] packs = ComponentPackages.Packages;
	String clzNameRouteRules = ".RouterRuleCreator";
	for (String pack : packs) {
	    try {
	        Class<?> creator = Class.forName(pack + clzNameRouteRules);
	        RouteCreator instance = (RouteCreator) creator.newInstance();
	        RouterConfiguration.get().addRouteCreator(instance);
	    } catch (Exception ignore) {
	    	// ignore
	    }
	}
}
複製程式碼

因為使用的是反射註冊。所以請不要忘了加上混淆配置:

-keep class * implements com.lzh.nonview.router.module.RouteCreator
複製程式碼

具體程式碼可以參考github上的元件化demo

外掛化

對於介紹外掛化環境下的使用方式的文章, 可以參考下方連結

Router: 教你如何進行任意外掛化環境下的路由適配

如果你當前的外掛化方案是使用的Small或者RePlugin。那麼也可以參考以下兩個demo

Small環境下使用Router

RePlugin環境下使用Router

相關文章