手把手帶你擼一個路由(2)--帶參跳轉

niknowzcd發表於2018-07-15

繼上一篇 手把手帶你擼一個路由(1)--介面跳轉之後,這篇文章來說一說如何實現攜帶引數的跳轉

寫在前頭,本系列文章為了更簡單的講述一些路由的知識,demo中的程式碼寫的十分簡單和直接.所以會存在不少判斷不合理的地方.對demo有合理建議的人可以再評論處指出。

帶參跳轉

正常情況下Activity之間的跳轉如下

Intent intent = new Intent(this, TestActivity1.class);
intent.putExtra("name", "張三");
startActivity(intent);

//intent內部維護著一個Bundle物件,而Bundle實現了Parcelable介面,相對而言算是比較高效率的引數傳遞方式。
複製程式碼

既然Android原生已經有了高效的引數傳遞方式,那自然是要利用起來。增加一個 IntentWrapper.class用來表示對intent引數的封裝

public class IntentWrapper {

    private Bundle mBundle;
    private String originalUrl;
    private RouteDemo routeDemo;

    private volatile static IntentWrapper instance = null;

    public static IntentWrapper getInstance() {
        if (instance == null) {
            synchronized (IntentWrapper.class) {
                if (instance == null) {
                    instance = new IntentWrapper();
                }
            }
        }
        return instance;
    }

    public IntentWrapper build(RouteDemo routeDemo, String url) {
        this.routeDemo = routeDemo;
        this.originalUrl = url;
        mBundle = new Bundle();
        return this;
    }

    public IntentWrapper withString(String key, String value) {
        mBundle.putString(key, value);
        return this;
    }

    public IntentWrapper withInt(String key, int value) {
        mBundle.putInt(key, value);
        return this;
    }

    public void open() {
        routeDemo.open(originalUrl, mBundle);
    }

}
複製程式碼

宣告一個Bundle物件,同時對外提供兩個簡單的方法withStringwithInt

接下來需要對RouteDemo進行改造

在上一篇文章中,由於路由跳轉是沒有攜帶引數的,所以是以

RouteDemo.open("test");
複製程式碼

這樣的簡單形式。

改造之後的呼叫方式

RouteDemo.getInstance().build("route://test")
    .withString("name", "張三")
    .withInt("age", 15)
    .open();
複製程式碼

通過build函式獲取前文所提到的IntentWrapper物件,藉由這個物件來傳遞引數。最後通過open()函式回撥RouteDemo中的跳轉邏輯.

完整的RouteDemo.class

public class RouteDemo {

    private static HashMap<String, Class> activityMap = new HashMap<>();
    private static Application mApplication;

    private volatile static RouteDemo instance = null;

    public static RouteDemo getInstance() {
        if (instance == null) {
            synchronized (RouteDemo.class) {
                if (instance == null) {
                    instance = new RouteDemo();
                }
            }
        }
        return instance;
    }

    public void init(Application application) {
        mApplication = application;
        try {
            //通過反射呼叫AutoCreateModuleActivityMap_app類的方法,並給activityMap賦值
            Class clazz = Class.forName("com.dly.routeDemo.AutoCreateModuleActivityMap_app");
            Method method = clazz.getMethod("initActivityMap", HashMap.class);
            method.invoke(null, activityMap);
            for (String key : activityMap.keySet()) {
                System.out.println("activityMap = " + activityMap.get(key));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public IntentWrapper build(String url) {
        return IntentWrapper.getInstance().build(this,url);
    }
	
    public void open(String url, Bundle bundle) {
        for (String key : activityMap.keySet()) {
            if(url.equals(key)){
                Intent intent = new Intent(mApplication, activityMap.get(key));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putExtras(bundle);
                mApplication.startActivity(intent);
            }
        }
    }

    public void open(String url) {
        for (String key : activityMap.keySet()) {
            if (url.equals(key)) {
                Intent intent = new Intent(mApplication, activityMap.get(key));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                mApplication.startActivity(intent);
            }
        }
    }
  
}
複製程式碼

如果只是像上述那樣顯示跳轉,那路由毫無意義。通常我們更多的需求會是伺服器返回一個url,客戶端根據這個url進行匹配跳轉。

比如首頁的一個banner頁,昨天要求跳轉到testActivity1這個介面,今天又突然改需求,要求跳轉到testActivity2這個介面。這個時候路由的動態配置就派上用場了。

比較常見的呼叫方法如下

RouteDemo.getInstance().open("route://test?name=555");
複製程式碼

再講述URL跳轉之前,必須得先說明一下Android下的URL和URI,以及他們之間的關係和格式,為之後URI解析做下鋪墊。

URL,URN,URI的關係 (對URI比較熟悉的可以跳過這一段)###

URI在於I(Identifier)是統一資源識別符號,可以唯一標識一個資源。
URL在於L(Location)是統一資源定位符,可以提供找到該資源的路徑。
URN在於N( name)統一資源名稱,通過名字標識資源。

一般來說URL和URN都是URI的子集,它們兩者共同組成了URI。

比如www.zhihu.com/question/21…,這一串地址可以唯一標識一個資源,所以是一個URI,同時也可以通過這個地址找到資源,所以也是一個URL。

還有一種情況,urn:isbn:0-486-27557-4這是一本書的isbn,可以唯一標識一本書,但我們無法通過這一串isbn找到書籍,所以這裡是URI,不是URL,準確說這一串isbn是一個URN。

URI的結構

URI的幾種劃分形式

基本劃分

[scheme:]scheme-specific-part[#fragment] 
複製程式碼

進一步劃分

[scheme:][//authority][path][?query][#fragment]
複製程式碼

再進一步劃分

[scheme:][//host:port][path][?query][#fragment] 
複製程式碼

其中有一些簡單的規則

  • path可以有多個,每個用/連線,比如 scheme://authority/path1/path2/path3?query#fragment
  • query引數可以帶有對應的值,也可以不帶,如果帶對應的值用=表示,如: scheme://authority/path1/path2/path3?id = 1#fragment,這裡有一個引數id,它的值是1
  • query引數可以有多個,每個用&連線 scheme://authority/path1/path2/path3?id = 1&name = mingming&old#fragment 這裡有三個引數:
    引數1:id,其值是:1
    引數2:name,其值是:mingming
    引數3:old,沒有對它賦值,所以它的值是null
  • 在android中,除了scheme、authority是必須要有的,其它的幾個path、query、fragment,它們每一個可以選擇性的要或不要,但順序不能變,比如:
    其中"path"可不要:scheme://authority?query#fragment 其中"path"和"query"可都不要:scheme://authority#fragment 其中"query"和"fragment"可都不要:scheme://authority/path "path","query","fragment"都不要:scheme://authority

其中有一些簡單的規則

  • path可以有多個,每個用/連線,比如 scheme://authority/path1/path2/path3?query#fragment
  • query引數可以帶有對應的值,也可以不帶,如果帶對應的值用=表示,如: scheme://authority/path1/path2/path3?id = 1#fragment,這裡有一個引數id,它的值是1
  • query引數可以有多個,每個用&連線 scheme://authority/path1/path2/path3?id=1&name =張三&old#fragment 這裡有三個引數:
    引數1:id,其值是:1
    引數2:name,其值是:張三 引數3:old,沒有對它賦值,所以它的值是null
  • 在android中,除了scheme、authority是必須要有的,其它的幾個path、query、fragment,它們每一個可以選擇性的要或不要,但順序不能變,比如:
    其中"path"可不要:scheme://authority?query#fragment 其中"path"和"query"可都不要:scheme://authority#fragment 其中"query"和"fragment"可都不要:scheme://authority/path "path","query","fragment"都不要:scheme://authority

做一個簡單的例子匹配

http://www.java2s.com:8080/yourpath/fileName.htm?name=張三&id=4#niknowzcd  
複製程式碼
  • scheme:http
  • host:www.java2s.com
  • port:8080
  • path:/yourpath/fileName.htm
  • query:name=張三&id=4
  • fragment:niknowzcd

常用的api

同樣的例子

http://www.java2s.com:8080/yourpath/fileName.htm?name=張三&id=4#niknowzcd  
複製程式碼
  • getScheme() :獲取Uri中的scheme字串部分,在這裡即http
  • getSchemeSpecificPart():獲取Uri中的scheme-specific-part:部分,這裡是://www.java2s.com:8080/yourpath/fileName.htm?
  • getFragment(): 獲取Uri中的Fragment部分,niknowzcd
  • getAuthority():獲取Uri中Authority部分,即www.java2s.com:8080
  • getPath():獲取Uri中path部分,即/yourpath/fileName.htm
  • getQuery():獲取Uri中的query部分,即name=張三&id=4
  • getHost():獲取Authority中的Host字串,即www.java2s.com
  • getPost():獲取Authority中的Port字串,即8080

另外還有兩個常用的屬性:getPathSegments()getQueryParameter(String key)

List getPathSegments() 會將整個path路徑儲存下來,並以/符號作為分隔符

String mUriStr = "http://www.java2s.com:8080/yourpath/fileName.htm?stove=10&path=32&id=4#harvic";  
Uri mUri = Uri.parse(mUriStr);  
List<String> pathSegList = mUri.getPathSegments();  
for (String pathItem:pathSegList){  
    Log.d("debug",pathItem);  
}  

//輸出
yourpath	
fileName.htm
複製程式碼

getQueryParameter(String key): 則是根據key來獲取對應的Query值

String mUriStr = "http://www.java2s.com:8080/yourpath/fileName.htm?name=張三&id=4#niknowzcd";  
mUri = Uri.parse(mUriStr);  
Log.d(debug,"getQueryParameter(\"name\"):"+mUri.getQueryParameter("name"));  
Log.d(debug,"getQueryParameter(\"id\"):"+mUri.getQueryParameter("id"));  

//輸出
getQueryParameter("name"):張三
getQueryParameter("id"):
複製程式碼

在path中,即使針對某一個KEY不對它賦值是允許的,但在利用getQueryParameter()獲取該KEY對應的值時,獲取到的是null;

匹配URI跳轉

我們回過頭看看之前寫的路徑的匹配方式

public void open(String url) {
        for (String key : activityMap.keySet()) {
            if(url.equals(key)){
                Intent intent = new Intent(mApplication, activityMap.get(key));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                mApplication.startActivity(intent);
            }
        }
    }
複製程式碼

採用的是字串Url全匹配的形式, 這種方式侷限性很大,就拿

route://test?name=555
複製程式碼

上述這個字串來說,我希望的是跳轉的test對照的Activity中,並且攜帶引數name=555.

如果用全URL匹配的話,我們可能需要做的是在目標Activity上加上類似的標註

@Route("route://test?name=555")
複製程式碼

這樣跳轉是能跳轉了,不過如何攜帶引數又成了問題。

這時再回去看看URI的結構劃分,取其中一種適用性比較廣泛的如下

[scheme:][//host:port][path][?query][#fragment] 
複製程式碼

其中

[scheme:][//host:port][path]
複製程式碼

這部分就足夠表示一個唯一的介面資源了,後面的引數只是區分當前介面所需要顯示的資料是那些而已。

就比如一個UserInfoActivity不管你傳入的userId是什麼,你所在的view都是UserInfoActivity

所以在匹配一個外部的URL的時候,需要匹配的是URL路徑部分,即

[scheme:][//host:port][path]
複製程式碼

這一部分

順著個思路,來改寫RouteDemo中的程式碼。

增加一個checkUrlPath函式

 //Uri的標準格式 scheme、authority 二者是必須的
private static boolean checkUrlPath(String targetUrl, String matchUrl) {
    Uri targetUri = Uri.parse(targetUrl);
    Uri matchUri = Uri.parse(matchUrl);

    Assert.assertNotNull(targetUri.getScheme());
    Assert.assertNotNull(targetUri.getHost());

    if (targetUri.getScheme().equals(matchUri.getScheme()) && targetUri.getHost().equals(matchUri.getHost())) {
        return TextUtils.equals(targetUri.getPath(), matchUri.getPath());
    } else {
        return false;
    }
}
複製程式碼

這裡還是採用了一個簡單粗暴的方式,當scheme,host,path三者都相等的時候,我們認為匹配上了,這裡沒提到port是因為路由中通常使用不到port這個屬性,如果需要的話,可以getAuthority()去獲取。

匹配到了path之後,還需要一個步驟,就是把我們需要的引數傳遞過去。使用 是getQueryParameterNames()getQueryParameter(queryParameterName)

具體實現如下

private Intent parseParams(Intent intent, String targetUrl) {
    Uri uri = Uri.parse(targetUrl);
    Set<String> queryParameterNames = uri.getQueryParameterNames();
    for (String queryParameterName : queryParameterNames) {
        intent.putExtra(queryParameterName, uri.getQueryParameter(queryParameterName));
    }
    return intent;
}
複製程式碼

最後再貼一下改造後的RouteDemo的函式

public class RouteDemo {

    private static HashMap<String, Class> activityMap = new HashMap<>();
    private static Application mApplication;

    private volatile static RouteDemo instance = null;

    public static RouteDemo getInstance() {
        if (instance == null) {
            synchronized (RouteDemo.class) {
                if (instance == null) {
                    instance = new RouteDemo();
                }
            }
        }
        return instance;
    }

    public void init(Application application) {
        mApplication = application;
        try {
            //通過反射呼叫AutoCreateModuleActivityMap_app類的方法,並給activityMap賦值
            Class clazz = Class.forName("com.dly.routeDemo.AutoCreateModuleActivityMap_app");
            Method method = clazz.getMethod("initActivityMap", HashMap.class);
            method.invoke(null, activityMap);
            for (String key : activityMap.keySet()) {
                System.out.println("activityMap = " + activityMap.get(key));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public IntentWrapper build(String url) {
        return IntentWrapper.getInstance().build(this,url);
    }

    public void open(String url, Bundle bundle) {
        for (String key : activityMap.keySet()) {
            if (checkUrlPath(url, key)) {
                Intent intent = new Intent(mApplication, activityMap.get(key));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putExtras(bundle);
                mApplication.startActivity(intent);
            }
        }
    }


    public void open(String url) {
        for (String key : activityMap.keySet()) {
            if (checkUrlPath(url, key)) {
                Intent intent = new Intent(mApplication, activityMap.get(key));
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent = parseParams(intent, url);
                mApplication.startActivity(intent);
            }
        }
    }

    private Intent parseParams(Intent intent, String targetUrl) {
        Uri uri = Uri.parse(targetUrl);
        Set<String> queryParameterNames = uri.getQueryParameterNames();
        for (String queryParameterName : queryParameterNames) {
            intent.putExtra(queryParameterName, uri.getQueryParameter(queryParameterName));
        }
        return intent;
    }


    //Uri的標準格式 scheme、authority 二者是必須的
    private static boolean checkUrlPath(String targetUrl, String matchUrl) {
        Uri targetUri = Uri.parse(targetUrl);
        Uri matchUri = Uri.parse(matchUrl);

        Assert.assertNotNull(targetUri.getScheme());
        Assert.assertNotNull(targetUri.getHost());

        if (targetUri.getScheme().equals(matchUri.getScheme()) && targetUri.getHost().equals(matchUri.getHost())) {
            return TextUtils.equals(targetUri.getPath(), matchUri.getPath());
        } else {
            return false;
        }
    }

}
複製程式碼

github 地址

RouteDemo

相關文章