基于ARouter做的少量扩展(ARouter-Extend)

作者 : 开心源码 本文共10543个字,预计阅读时间需要27分钟 发布时间: 2022-05-12 共172人阅读

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

ARouter-Extend源码地址

前言

最近在做全面的页面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_compilerkapt 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还是有待增强的。

欢迎交流~ ~

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 基于ARouter做的少量扩展(ARouter-Extend)

发表回复