从0到1实现一个Android路由(3)——APT收集路由

从0到1实现一个Android路由系列文章

之前的例子中,关于url和Activity之间的关系,是写死在一个Map中的,可以看做是一个静态路由。随着项目规模的扩大,这样一个个的手写那张表是个工作量比较大的工作,那么有什么简单的方式可以实现自动化呢?

答案是APT(Annotation Processing Tool)。原理是在编译时收集注解信息,然后生成源代码或进行某些操作。对于路由,做法可以是给要跳转的Activity声明注解,指定其跳转的url,APT在编译时收集这些信息,然后存入到某张表里,这样当app运行时,可以首先把表加载到内存中,之后就可以就行跳转了。

坑点

由于之前的例子是Kotlin写的,因此也想写个Kotlin的注解处理器,但因为总总问题,就搁浅了,最终得将这一部分使用Java进行编写。这个问题会继续寻求解决方法的。阿里的ARouter是支持Kotlin的,等我学习完ARouter之后有机会会再介绍的。

由于gradle版本高于4.7,app module属于Kotlin和Java混的,编译会出现incremental的提示,这个解决方法见参考的第一个链接解决方案

项目结构

整个项目包含的模块有:

  • annotation:注解模块,定义了注解
  • compiler:定义了注解处理器APT
  • api:路由API
  • app:Android Application,使用上面的三个库

annotation模块

目前annotation只有一个注解,Path,用来定义url,其实现如下:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Path {
    String value();
}

compiler模块

APT处理器,处理Path注解,然后将收集到的信息,写入到一个类中,使用的JavaPoet用来生成Java源文件。具体实现可以参考github里的代码。

api模块中定义了一个接口,其实现是:

public interface UrlCollector {
    Map<String, Class<? extends Activity>> getUrlRouterMap();
}

该接口返回一个Map,这个Map就包含了url和Activity的对应关系,而APT就是生成了该类的一个实现类UrlCollectorImpl,并将其放到了com.xingfeng.android.api包下面。

process核心代码如下:

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.size() == 0) {
            return false;
        }

        MethodSpec.Builder builder = MethodSpec.methodBuilder("getUrlRouterMap")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class),
                        ParameterizedTypeName.get(ClassName.get(Class.class),
                                WildcardTypeName.subtypeOf(ClassName.get("android.app", "Activity")))))
                .addStatement("Map<String,Class<? extends Activity>> urlMaps=new $T<>()", HashMap.class);
        //遍历每个@Path注解,将内容添加到Map中
        for (Element element : roundEnvironment.getElementsAnnotatedWith(Path.class)) {
            //收集信息
            if (element.getKind() != ElementKind.CLASS) {
                continue;
            }
            TypeElement typeElement = (TypeElement) element;
            Path path = typeElement.getAnnotation(Path.class);
            String url = path.value();
            builder.addStatement("urlMaps.put($S,$T.class)", url, typeElement.asType());

        }

        builder.addStatement("return urlMaps");
        TypeSpec typeSpec = TypeSpec.classBuilder("UrlCollectorImpl")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(ClassName.get("com.xingfeng.android.api", "UrlCollector"))
                .addMethod(builder.build())
                .build();

        JavaFile javaFile = JavaFile.builder("com.xingfeng.android.api", typeSpec).build();
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return false;
    }

生成的类位于app/build/generated/source/apt/debug目录下,代码如下:

public final class UrlCollectorImpl implements UrlCollector {
  @Override
  public Map<String, Class<? extends Activity>> getUrlRouterMap() {
    Map<String,Class<? extends Activity>> urlMaps=new HashMap<>();
    urlMaps.put("easyrouter://demo/absoluteUrlActivity",AbsoluteUrlActivity.class);
    urlMaps.put("/dynamicActivity",DynamicActivity.class);
    return urlMaps;
  }
}

生成了这样一个类以后,如何使用呢?

api模块

api模块提供了对外的接口,主要是EasyRouter这样一个单例类,其入口为init()方法,需要传入scheme和host,实现如下:

    private static final String URL_COLLECTOR_IMPL_CLASS_NAME = "com.xingfeng.android.api.UrlCollectorImpl";

/**
     * 初始化
     *
     * @return true表示成功, false表示失败
     */
    public boolean init(String scheme, String host) {
        try {
            UrlCollector urlCollector = (UrlCollector) Class.forName(URL_COLLECTOR_IMPL_CLASS_NAME).newInstance();
            urlRouterMap = urlCollector.getUrlRouterMap();
            this.scheme = scheme;
            this.host = host;
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

        return false;
    }

可以看到,主要就是通过反射,提供我们刚才生成的类,生成了之后,将路由表保存到实例当中就ok了。

目前,对外主要提供了两个api:

  • addUrl(String,Class):手动添加路由表;
  • goToPages(Context,String):路由跳转
  • setRouterListener(RouterListener):设置全局的监听器,可以用于拦截、兜底

app模块

app模块主要是调用方,使用方式主要有几步:

  1. init()设置scheme和host
 class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        EasyRouter.getInstance().init("easyrouter","demo")
    }
}
  1. 设置监听器
setRouterListener(object : EasyRouter.RouterListener {
                override fun onIntercept(url: String?): Boolean {
                    if (url != null && url.startsWith("http")) {
                        startActivity(Intent(this@MainActivity, WebViewActivity::class.java).apply {
                            putExtra("external_url", url!!)
                        })
                        return true
                    }
                    return false
                }

                override fun onLost(url: String?) {
                    startActivity(Intent(this@MainActivity, DegradeActivity::class.java).apply {
                        putExtra("error_msg", "没找到该${url!!}对应的页面")
                    })
                }

                override fun onFound(url: String?) {
                    Toast.makeText(this@MainActivity, "找到了", Toast.LENGTH_SHORT).show()
                }
            })

这里,主要对http开头的协议进行内部WebView转发,没有找到页面的url跳转到一个兜底的页面,找到的情况就弹一个Toast示意一下。
3. 看情况是否使用addUrl()添加扫描不到的url,比如说那些Kotlin编写的界面对应的url

addUrl(secondActivityUrl, SecondActivity::class.java)
            addUrl(paramsActivityUrl, WithParamsActivity::class.java)
  1. 跳转页面
EasyRouter.getInstance().goToPages(this, secondActivityUrl)

以上就是EasyRouter的使用手册,目前支持的功能就这些。后面会继续完善。

总结

经历了一个五脏俱全的例子,到URL处理器,再到本章的APT收集路由,我们的路由库已经越来越完善,也可以渐渐应对一些问题了。当然,与大厂的开源路由库还是有很大的差距的,后面会继续添加功能。
目前的功能有:

  • apt自动收集路由信息
  • 支持初始化后再添加路由
  • 支持相对url和绝对url的跳转、带参数跳转
  • 外部支持设置全局监听器,用于实现路由拦截、兜底

关于代码,可以参考signle_module分支代码

参考

关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注:
二维码

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页