組件化方案:JIMU之UI路由(一)

背景介紹:

張明慶老哥之前在得到工作時,開源了DDAndroidComponent項目,演示組件化思路及實現(xiàn),本人當時作為協(xié)作者參與了一部分開發(fā),現(xiàn)項目遷移到JIMU ,接下來將在新項目倉庫進行維護。

張明慶老哥的幾篇文章:
1、Android徹底組件化方案實踐
2、Android徹底組件化demo發(fā)布
3、Android徹底組件化-代碼和資源隔離
4、Android徹底組件化—UI跳轉升級改造
5、Android徹底組件化—如何使用Arouter

為什么要有這一篇

首先,不在此處展開組件化核心思想“隔離與發(fā)現(xiàn)”,因為有了隔離的要求(其他模塊的Activity在編譯期不可見,無法創(chuàng)建顯示Intent),和JIMU本身自有一套路由(我們稱之為“路由”,“IOC容器”,“DI組件”都是可以的,本質就是用來注冊和尋找實例的)的實際基礎存在,這決定了在組件化中跨模塊跳轉頁面,需要使用以下的一種技術:

  • 將模塊內的頁面跳轉,整理出API,在ComponentService中暴露。
  • 抽象頁面跳轉的過程,使用映射關系,由路由短鏈發(fā)起跳轉請求。

其次,為什么Demo中沒有直接使用ARouter等成型方案?ARouter是個很優(yōu)秀的項目,但是對于JIMU而言,他有點over-weight而且功能重復,我前面簡單提到:“JIMU本身自有一套路由”,而且在JIMU中自動注冊的功能是注冊到該路由的,再加入ARouter純粹多余選擇多了往往是麻煩事),以及在項目中使用ARouter的,可以參考鏈接中的第五篇,使用ARouter進行路由跳轉,甚至是基于ARouter實現(xiàn)組件化,以達到項目的簡潔。

結合討論群中一些朋友們提出的問題,以及github上的典型issue,本文進行一些扼要的總結,便于大家排錯。

為了方便,我們下文將用UIRouter來代指JIMU中提供的UI路由。
以下是outline

  • UIRouter 1.0.0提供哪些東西
  • UIRouter 1.0.0包含哪些已知問題
  • 如何集成 UIRouter
  • UIRouter的特性概述
  • 常見問題Q/A

UIRouter 1.0.0提供哪些東西

  • 依賴庫:router-annotation 包含了可用的注解以及內部使用的實體類、幫助類
  • 注解處理器 router-anno-compiler

可供使用的注解:(代碼取自新版本,和v1.0.0存在一定小差異)

  • RouteNode 路由節(jié)點,對應Activity
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface RouteNode {
    /**
     * path of one route
     */
    String path();
    /**
     * The priority of route.
     *
     * we inspect the path and throw exception when duplicated
     * paths were find, thus, it's useless and impossible to use priority
     */
    @Deprecated
    int priority() default -1;

    /**
     * description of the activity, user for gen route table
     */
    String desc() default "";
}
  • Autowired 用于從intent中獲取參數(shù),并將值注入Field的注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface Autowired {

    /**
     * @return param's name or service name.
     */
    String name() default "";

    /**
     * <em>primitive java type check will be ignore</em>
     * check the result of DI, if inject failed, the value of
     * the field will be null, if required, output log
     *
     * @return true for required,false otherwise
     */
    boolean required() default false;

    /**
     * throw exception when the required field is null after inject.
     * <p>
     * It can help developer find most data delivering bugs when developing.
     * but not suggest to open this function after release.
     * <p>
     * I suggest to define a Constant maintained manually
     * <p>
     * only activated when required = true and throwOnNull = true.
     *
     * @return true if throwing exception when null is required, false otherwise
     */
    boolean throwOnNull() default false;

    /**
     * @return field description
     */
    String desc() default "none desc.";
}

其他非暴露供使用的內容不做介紹。

UIRouter 1.0.0包含哪些已知問題

  • 因單例模板出現(xiàn)問題導致:本設計成單例的JsonService和AutowireService不是單例
  • 在一個Module中進行java和kotlin的混編并同時需要使用RouteNode注解時存在問題

如何集成 UIRouter

首先:您應該以及集成了JIMU方案,使用了gradle plugin 并且集成了基礎庫:

以下演示代碼均建立在java項目、gradle plugin版本<3.0 基礎上

  1. 集成注解依賴庫:
compile 'com.luojilab.ddcomponent:router-annotation:1.0.0'

注意:componentLib包中已經(jīng)包含了注解依賴庫,所以不需要再聲明依賴庫。

  1. 集成注解處理器:
annotationProcessor 'com.luojilab.ddcomponent:router-anno-compiler:1.0.0'

注意:在Module的build.gradle中聲明,不要在底層庫中聲明;不聲明無法使用UIRouter功能。

并指定Module的環(huán)境參數(shù):

defaultConfig {
    javaCompileOptions {
        annotationProcessorOptions {
             arguments = [host: "share"]
        }
    }
}

注意:這里的環(huán)境參數(shù)會影響使用時編寫的url(or URI),以此處代碼為例,我們的url形式是這樣的:

[schema]//[host][path]?[queryString]

舉個例子,該Module中存在一個RouteNode:path為“/index”
那么對應的url為:

【任意協(xié)議】//share/index

如果未指定,那么將使用默認值“default”,即:

【任意協(xié)議】//default/index

3.將生成的映射注冊到UIRouter
抱歉在之前的內容中,遺漏了這一塊,給使用者帶來了困擾
這一部分內容,在閱讀上而言放在后面會更好一點,但是容易被忽視。
在使用中,我們會得到自動生成的路由映射,一定要注冊到UIRouter;例如:

public class ShareApplike implements IApplicationLike {

    UIRouter uiRouter = UIRouter.getInstance();

    @Override
    public void onCreate() {
        uiRouter.registerUI("share");
    }

    @Override
    public void onStop() {
        uiRouter.unregisterUI("share");
    }
}

我們選擇的是在組件的生命期入口進行注冊和反注冊。不多做贅述。

實際上這樣我們就完成了集成,接下來就是使用了。

為路由節(jié)點(Activity)添加注解

注意,priority沒有實質性意義,已廢棄,Module中不允許出現(xiàn)同樣的path

@RouteNode(path = "/main", desc = "首頁")
public class MainActivity extends BaseActivity implements View.OnClickListener {
//...代碼略去
}

以appModule中的MainActivity為例添加了一個節(jié)點。這樣我們可以得到一個生成類:AppUiRouter:

public class AppUiRouter extends BaseCompRouter {
  @Override
  public String getHost() {
    return "app";
  }

  @Override
  public void initMap() {
    super.initMap();
    routeMapper.put("/main",MainActivity.class);
  }
}

一個用于輔助的Module路由表清單txt:AppRouterTable.txt

auto generated, do not change !!!! 

HOST : app

首頁
/main

進行跳轉

 UIRouter.getInstance().openUri({context},
                    "JIMU://app/main", {bundle});

關于參數(shù)

將在(二)中詳細展開,這里簡單介紹一下取參數(shù),取參數(shù)使用了Autowired注解,進行DI。

以一個新版中的演示Demo為例:

@RouteNode(path = "/uirouter/demo/2", desc = "使用bundle傳遞參數(shù)")
public class Demo2Activity extends TestActivity {
    private static Bundle bundle = new Bundle();

    static {
        bundle.putString("foo", "foo string");
        bundle.putString("EXTRA_STR_BAR", "bar string");
    }

    @Autowired() //不指定名稱時將使用變量名,若被混淆可能出現(xiàn)問題,
                // 建議使用name指定key,參考bar的使用
    String foo;

    @Autowired(name = "EXTRA_STR_BAR")
    String bar;

    public static final UiRouterDemoActivity.Case aCase
            = new UiRouterDemoActivity.Case(false,
            "使用bundle傳遞參數(shù)",
            "JIMU://app/uirouter/demo/2",
            bundle);

    @Override
    protected void displayInfo(TextView textView) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("使用bundle傳遞參數(shù)成功\r\n");
        stringBuilder.append("foo:").append(foo).append("\r\n");
        stringBuilder.append("bar:").append(bar).append("\r\n");

        textView.setText(stringBuilder.toString());
    }
}

父類的代碼:

abstract class TestActivity extends AppCompatActivity {

    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        textView = findViewById(R.id.demo_tv_info);
        AutowiredService.Factory.getSingletonImpl()
                .autowire(this);
        displayInfo(textView);
    }

    protected abstract void displayInfo(TextView textView);
}

稍微細說一下:
需要被注入的Field為:

@Autowired() //不指定名稱時將使用變量名,若被混淆可能出現(xiàn)問題,
                // 建議使用name指定key,參考bar的使用
String foo;

@Autowired(name = "EXTRA_STR_BAR")
String bar;

建議:指定name!

我們看看生成的內容中,多了哪些:

  • 路由 AppUiRouter.java 中:

public class AppUiRouter extends BaseCompRouter {
  @Override
  public String getHost() {
    return "app";
  }

  @Override
  public void initMap() {
    super.initMap();
    routeMapper.put("/main",MainActivity.class);
    routeMapper.put("/uirouter/demo/2",Demo2Activity.class);
    paramsMapper.put(Demo2Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
  }
}

相比于MainActivity,多了一些東西:paramMapper中添加了一些配置,這里不詳細展開,在不hook系統(tǒng)API的情況下,都會回歸到使用Intent啟動Activity。而傳參依舊需要使用Bundle,我們知道bundle的讀寫是需要知道類型的。UIRouter支持的類型下一篇展開

  • Demo2Activity$$Router$$Autowired.java文件
    以下代碼僅演示下,不做展開,讀者不用深究,將在(二)中詳細展開
    類似:
/**
 * Auto generated by AutowiredProcessor */
public class Demo2Activity$$Router$$Autowired implements ISyringe {
  private JsonService jsonService;

  @Override
  public void inject(Object target) {
    jsonService = JsonService.Factory.getSingletonImpl();
    Demo2Activity substitute = (Demo2Activity)target;
    substitute.foo = substitute.getIntent().getStringExtra("foo");
    substitute.bar = substitute.getIntent().getStringExtra("EXTRA_STR_BAR");
  }

  @Override
  public void preCondition(Bundle bundle) throws ParamException {
  }
}
  • 路由表中多了以下內容:
使用bundle傳遞參數(shù)
/uirouter/demo/2
foo:String
EXTRA_STR_BAR:String

注意:多了參數(shù)信息,是 name的值 和 參數(shù)的類型

發(fā)起注入的核心代碼:和v1.0.0的API有一定區(qū)別

  AutowiredService.Factory.getSingletonImpl().autowire(this);

UIRouter的特性概述

時間和篇幅原因,移到(二)中展開。

常見問題Q/A

  1. Q:為什么無法跳轉?
    A:根本原因都是沒有正確集成:排查次序
  • 是否集成了注解庫和注解處理庫?
  • 同Module中path是否有重復?
  • gradle任務message中是否有異常輸出?
  • 路由映射是否注冊到UIRouter
  • 是否url有誤?
  • 是否有參與檢測的參數(shù),但是沒有包含或者有誤?(對于這一點還不清楚的,請等待第二篇文章)
  • 生成的UIRouter是否在APPLike的onCreate生命周期節(jié)點中注冊到UIRouter
  • 組件會維護兩份manifest文件,是否遺漏添加(注:異常已被Router捕獲并處理為:將目標加入黑名單,故沒有直觀的crash
  1. Q:出現(xiàn)了ClassNotFoundException怎么辦?
    A:應該是啟用了混淆,添加免混淆配置:可能因為項目變動的原因,后期會修改生成類path,一切以項目主頁為準
-keep class com.luojilab.router.** {*;}
-keep class com.luojilab.gen.** {*;}
-keep class * implements com.luojilab.component.componentlib.router.ISyringe {*;}

3.Q:出現(xiàn)了錯誤很難排查怎么辦?
A:根本原因是我當時和張老哥沒有協(xié)調好,導致1.0.0的代碼過早發(fā)出,而迭代版本因為其他原因遲遲未發(fā),四月份一定發(fā)版本,新版本中l(wèi)og的輸入以及防御性代碼比較完善,應該可以提供充足的糾錯信息。

4.Q:gradle plugin >=3.0 集成問題?
A:首先注意,我最開始就將依賴庫和注解處理庫分開了,這樣已經(jīng)避免了重復使用api和annotationProcessor聲明同一個庫的各種問題,如果在底層庫中集中添加注解依賴庫,使用api(或者還未移除的compile),不要使用Implementation;若是在組件Module中,隨意使用api或Implementation。但是必須使用annotationProcessor聲明注解處理庫使用compile已經(jīng)不會自動添加到注解處理包路徑下

  1. Q:kotlin是否可以用?
    A:可以使用,如何集成參考demo,注意,對java和kt的Activity都使用注解,僅需要使用kapt聲明注解處理器即可,按照Demo集成kapt3即可,不需要聲明annotationProcessor,禁止使用早已廢棄的apt插件

(Q/A持續(xù)更新)


下一篇將詳細展開UIRouter的基礎功能特性、新版本特性,并會安排發(fā)一個迭代版本。

JIMU的討論群,群號693097923,歡迎大家加入:

image

qq群中有很多熱心的朋友,一些重要的討論,往往從群里面展開,最后轉移到項目的issue中展開討論以及總結。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容