美文网首页Android开发经验谈Android开发Android进阶之路
基于ARouter做的一些扩展(ARouter-Extend)

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

作者: pphdsny | 来源:发表于2019-03-04 15:14 被阅读23次

    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_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还是有待加强的。

    欢迎交流~ ~

    相关文章

      网友评论

        本文标题:基于ARouter做的一些扩展(ARouter-Extend)

        本文链接:https://www.haomeiwen.com/subject/vqdiuqtx.html