基于ARouter做的一些扩展(ARouter-Extend)

ARouter是一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

ARouter-Extend:基于ARouter实现的跳转扩展,解决组件内页面跳转传参不确定的问题

前言

最近在做全面的页面URL化,之前引入了ARouter,但是并没有真正的用起来,页面跳转还是通过以下方式

@Route(path = "/app/test")
public class TestActivity extends BaseActivity {
    public static final String EXTRA_HOUSE_ID = "extra_house_id";      //小区id
    public static final String EXTRA_CITY_ID = "extra_city_id";        //城市id

    private int mHouseId;
    private int mCityId;
    
    //...
    @Override
    protected void initIntentParams(Intent intent) {
        super.initIntentParams(intent);
        mHouseId = intent.getIntExtra(EXTRA_HOUSE_ID, 0);
        mCityId = intent.getIntExtra(EXTRA_CITY_ID, 0);
    }
    //...

    public static void launchActivity(Context context, int cityId, int houseId) {
        Bundle bundle = new Bundle();
        if (cityId <= 0) {
            //如传递的cityId无效,则使用全局的cityId
            cityId = CityManager.getInstance().getCityId();
        }
        bundle.putInt(EXTRA_CITY_ID, cityId);
        bundle.putInt(EXTRA_HOUSE_ID, houseId);
        ARouter.getInstance().build("/app/test").with(bundle).navigation();
    }
}

目前这种方法的最大问题就是==>重复的模板代码

  • launchActivity中的参数赋值
  • initIntentParams中的参数解析
  • cityId的有效性检测

ARouter的官方用法:

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
//-------------------
// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/test/activity").navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build("/test/1")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();

其实ARouter对于我使用过程遇到的这些问题都能解决,为什么我一直都没有真正的用起来呢?

最主要的原因就是ARouter的path路径页面参数定义,也就是我不喜欢ARouter.getInstance().build("/test/activity").navigation()这种跳转方式,原因有以下几点:

  • path路径页面参数定义最清楚的应该是模块开发者(A),对于调用方(B)并不是很清楚path和params是什么?
  • B调用A的页面时候,并不确定哪些参数是可选的,哪些是必选?
  • 当A开发的模块需要新增个必选参数时候,B并不知道,而且编译不会报错。

当然,上面说的这些,你都可以说B不知道去看开发文档么、A会告诉B的…,这些都是建立在理想情况下,再说每次开发都看文档那得多累,不是么….

如果能把目前项目中用到的launchActivity方法自动生成的话,那该多爽啊….

说干就干!!!

目标

  • 自动生成launchActivity方法
  • launchActivity方法中区分必选参数和可选参数
  • 参数的有效性检测
  • URL跳转参数类型转换

自动生成launchActivity方法

实现方案:APT,整体参考ARouter-compiler中的实现,关于APT有很多文章介绍,就不多说了,直接上代码:

依赖库

apply plugin: 'java-library'

sourceCompatibility = "8"
targetCompatibility = "8"

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.alibaba:arouter-annotation:1.0.6'
    implementation 'com.google.auto.service:auto-service:1.0-rc3'
    implementation 'com.squareup:javapoet:1.8.0'
}

com.alibaba:arouter-annotation:1.0.6 —> 基于ARouter的注解扩展

com.google.auto.service:auto-service:1.0-rc3 —>方便生成编译依赖

com.squareup:javapoet:1.8.0 —>方便生成java文件

Processor API

public interface Processor {
    //支持的可选参数,可通过getOptions()获取==>AROUTER_MODULE_NAME
    Set<String> getSupportedOptions();

    //支持的注解类型==>com.alibaba.android.arouter.facade.annotation.Route
    Set<String> getSupportedAnnotationTypes();

    //支持的源码版本==>SourceVersion.RELEASE_8
    SourceVersion getSupportedSourceVersion();

    //初始化,可以获取运行的一些环境参数
    void init(ProcessingEnvironment var1);

    //对注解进行处理,返回true的话,后续拦截器无法处理(阿里自己的RouteProcessor就返回了true,蛋疼!!!)
    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}

实现Processor

那些Processor的方法,可以使用下列注解实现

@AutoService(Processor.class)
@SupportedOptions({KEY_MODULE_NAME, KEY_GENERATE_DOC_NAME})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE})
public class RouterProcessor extends AbstractProcessor {
 //...   
        @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set != null && !set.isEmpty()) {
            Set<? extends Element> routeElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
            try {
                logInfo("Found routes, start...");
                this.parseRoutes(routeElements);
            } catch (Exception e) {
                logger.error(e);
            }
        }
        //一定要返回false,不然就会想ARouter自己的RouterProcessor一样,别人就没办法继续处理此Annotation了
        return false;
    }
    //...
}

parseRoutes

private void parseRoutes(Set<? extends Element> routeElements) {
    if (routeElements != null && !routeElements.isEmpty()) {
        logInfo("Found routes, size is " + routeElements.size());
        //新建一个统一的跳转工具类
        TypeSpec.Builder activityLaunchBuilder = TypeSpec.classBuilder(generateClassName())
                .addJavadoc(WARNING_TIPS)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

        //添加方法
        List<MethodSpec> methodList = new ArrayList<>();
        for (Element element : routeElements) {
            //暂时只处理Activity
            TypeMirror activityTm = elements.getTypeElement(Consts.ACTIVITY).asType();
            boolean isActivity = false;
            if (types.isSubtype(element.asType(), activityTm)) {
                isActivity = true;
            }
            if (!isActivity) {
                continue;
            }

            Route route = element.getAnnotation(Route.class);
            //获取使用@Autowired标注的参数
            List<FieldModel> requiredNames = new ArrayList<>();
            List<FieldModel> noRequiredNames = new ArrayList<>();
            for (Element field : element.getEnclosedElements()) {
                if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null) {
                    Autowired paramConfig = field.getAnnotation(Autowired.class);
                    String injectName = TextUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name();
                    boolean required = paramConfig.required();
                    if (required) {
                        requiredNames.add(new FieldModel(injectName, field.asType(), paramConfig.desc()));
                    } else {
                        noRequiredNames.add(new FieldModel(injectName, field.asType(), paramConfig.desc()));
                    }
                }
            }
            //添加方法
            List<MethodSpec> methodSpecs =
                    generateLaunchMethodList("launch" + element.getSimpleName(),
                            route.path(),
                            requiredNames, noRequiredNames);
            methodList.addAll(methodSpecs);
        }

        for (MethodSpec method : methodList) {
            activityLaunchBuilder.addMethod(method);
        }
        //写入包名,生成文件
        JavaFile javaFile = JavaFile.builder(PACKAGE_OF_GENERATE_FILE, activityLaunchBuilder.build())
                .build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

生成launchActivity方法

generateLaunchMethodList

生成Activity支持带参数启动的方法列表,目前支持三种模式

  • Autowiredrequired = true的所有参数
  • Autowired修饰的所有参数(必选+非必选参数)
  • Autowireddesc = "ALONE"的独立参数
private List<MethodSpec> generateLaunchMethodList(String methodName, String routerPath, List<FieldModel> requiredNames, List<FieldModel> norequiredNames) {
    List<MethodSpec> methodSpecList = new ArrayList<>();
    //必选参数方法
    methodSpecList.add(generateLaunchMenthod(methodName, routerPath, requiredNames));
    //必选+非必选参数方法
    List<FieldModel> paramNames = new ArrayList<>();
    if (norequiredNames != null && norequiredNames.size() > 0) {
        paramNames.addAll(requiredNames);
        paramNames.addAll(norequiredNames);
        methodSpecList.add(generateLaunchMenthod(methodName, routerPath, paramNames));
    }
    //是否有独立参数ALONE
    for (int i = 0; i < paramNames.size(); i++) {
        FieldModel fieldModel = paramNames.get(i);
        if (TextUtils.equals(fieldModel.getDesc(), Consts.ALONE_DESC)) {
            methodSpecList.add(generateLaunchMenthod(methodName, routerPath, Collections.singletonList(fieldModel)));
        }
    }

    return methodSpecList;
}

generateLaunchMenthod

根据入参list,生成launchActivity方法

private MethodSpec generateLaunchMenthod(String methodName, String routerPath, List<FieldModel> paramNames) {
    TypeMirror type_Context = elements.getTypeElement(CONTEXT).asType();

    MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName)
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class);
    //添加context参数
    builder.addParameter(ClassName.get(type_Context), "context");
    //添加自定义参数
    for (int i = 0; i < paramNames.size(); i++) {
        builder.addParameter(TypeName.get(paramNames.get(i).getTypeMirror()), paramNames.get(i).getName());
    }
    //语法
    TypeMirror type_bundle = elements.getTypeElement(Consts.BUNDLE).asType();
    builder.addStatement("$T bundle = new $T()", ClassName.get(type_bundle), ClassName.get(type_bundle));
    for (int i = 0; i < paramNames.size(); i++) {
        int type = typeUtils.typeExchange(paramNames.get(i).getTypeMirror());
        String buildStatement = buildStatement(type, paramNames.get(i).getName());
        if (!TextUtils.isEmpty(buildStatement)) {
            builder.addStatement(buildStatement);
        }
    }
    TypeMirror routerManager = elements.getTypeElement(ROUTER_MANAGER).asType();
    //这部分可替换成ARouter的跳转方法
    builder.addStatement("$T.getInstance().navigation(context, \"" + routerPath + "\", bundle)", ClassName.get(routerManager));

    return builder.build();
}

最终效果

==>源文件

@Route(path = "/app/test")
public class TestActivity extends Activity {

    @Autowired(required = true)
    public int cityId;
    @Autowired
    public int houseId;
    @Autowired(desc = "ALONE")
    public Object test;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //一定要写
        ARouter.getInstance().inject(this);
        setContentView(R.layout.activity_test);
    }
}

==>生成的launchActivity文件

public final class AppActivityLaunch {
  public static void launchTestActivity(Context context, int cityId) {
    Bundle bundle = new Bundle();
    bundle.putString("cityId", cityId);
    RouterManager.getInstance().navigation(context, "/app/test", bundle);
  }

  public static void launchTestActivity(Context context, int cityId, int houseId, Object test) {
    Bundle bundle = new Bundle();
    bundle.putString("cityId", cityId);
    bundle.putInt("houseId", houseId);
    RouterManager.getInstance().navigation(context, "/app/test", bundle);
  }

  public static void launchTestActivity(Context context, Object test) {
    Bundle bundle = new Bundle();
    RouterManager.getInstance().navigation(context, "/app/test", bundle);
  }
}

后续跳转直接就通过AppActivityLaunch 中的launchXXXActivity方法,方便、直接、明了、不易出错!

遇到的一些坑

  1. 自己独立编译能生成文件,和ARouter集成后无法生成文件

    注意以下的顺序,是不是写在ARouter_compiler后面了

    //必须写在arouter_compiler前面,因为arouter_compiler对注解处理返回true,后续没办法处理
    kapt dependencies.router_compiler        //自己的route_compiler
    kapt dependencies.arouter_compiler       //阿里的ARouter_compiler
    
  2. 使用@Autowired标识的属性无法获取

    可以从以下几方面继续排查:

    • 是否在onCreate方法中写了:ARouter.getInstance().inject(this);
    • 注意属性的名称是否一致
    • 注意属性的类型是否一致,Float和double,int和String是无法接受的
    • Serializable类型的属性无法获取,注意router-api的版本是否是最新的1.4.1,之前使用的1.3.1的版本中Serializable被当做对象,需要自己写解析器才能解析。
    • 再无法获取,可以全局搜索ARoute_complier的生成类TestActivity$$ARouter$$Autowired,debug一下具体情况
  3. 全局参数有效性的补充逻辑

    例如cityId是一个全局参数,当入参的cityId不存在或者无效,需要填充全局的cityId作为入参。

    刚开始是考虑在route_compiler中填充这部分判断代码,后面考虑了一下,耦合性太大,于是借用了@Route中的extras属性,使用ARouter中提供的拦截器(IInterceptor)

    @Interceptor(priority = 1, name = "CityID有效性检测")
    public class CityIdInterceptor implements IInterceptor {
        @Override
        public void process(Postcard postcard, InterceptorCallback callback) {
            int extra = postcard.getExtra();
            if ((extra & RouterExtraCons.CITY_ID_CHECK) == RouterExtraCons.CITY_ID_CHECK) {
                //检测cityId是否有效
                int cityId = postcard.getExtras().getInt("cityId");
                if (cityId <= 0) {
                    //填充全局的cityId
                    postcard.getExtras().putInt("cityId", CityManager.getInstance().getCityId());
                }
            }
            callback.onContinue(postcard);
        }
    
        @Override
        public void init(Context context) {
    
        }
    }
    
  4. 外部Uri跳转进来的时候,由于通过Uri通过getQueryParameter获取的参数都是String,如果目标Activity需要的入参是Int的时候,就没办法匹配上了,咋办?

    目前没有比较好的方法,用了个兼容性差但是能满足业务要求的方法==>获取到String后强转,代码如下:

        /**
         * 默认强制转换格式Int,Float,Boolean
         *
         * @param param
         * @return
         */
        private static Object parseParamType(String param) {
            if (param == null) return "";
            //int
            try {
                return Integer.parseInt(param);
            } catch (NumberFormatException e) {
                LogUtils.d("parseParamType is not Integer");
            }
            //Float
            try {
                return Float.parseFloat(param);
            } catch (NumberFormatException e) {
                LogUtils.d("parseParamType is not Float");
            }
            //boolean
            try {
                if (TextUtils.equals(param, "true") || TextUtils.equals(param, "false"))
                    return Boolean.parseBoolean(param);
            } catch (NumberFormatException e) {
                LogUtils.d("parseParamType is not Boolean");
            }
            return param;
        }
    

后记

目前做的这些,还是很有限的,只是对ARouter的一点点扩展,基于自己的业务和第三方库的配套使用,底层技术点都是相通的,在注解的扩展性上ARouter还是有待加强的。
源码地址
欢迎交流~ ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容