前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从0到1实现一个Android路由(3)——APT收集路由

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

作者头像
用户1108631
发布2019-08-17 12:39:59
6190
发布2019-08-17 12:39:59
举报
文章被收录于专栏:每天学点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的提示,这个解决方法见参考的第一个链接:https://docs.gradle.org/nightly/userguide/javaplugin.html#sec:incrementalannotation_processing

项目结构

整个项目包含的模块有:

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

annotation模块

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

代码语言:javascript
复制
@Retention(RetentionPolicy.CLASS)@Target(ElementType.TYPE)public @interface Path {    String value();}

compiler模块

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

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

代码语言:javascript
复制
public interface UrlCollector {    Map<String, Class<? extends Activity>> getUrlRouterMap();}

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

process核心代码如下:

代码语言:javascript
复制
@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目录下,代码如下:

代码语言:javascript
复制
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,实现如下:

代码语言:javascript
复制
    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
代码语言:javascript
复制
 class MyApplication : Application() {    override fun onCreate() {        super.onCreate()        EasyRouter.getInstance().init("easyrouter","demo")    }}
  1. 设置监听器
代码语言:javascript
复制
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示意一下。

  1. 看情况是否使用addUrl()添加扫描不到的url,比如说那些Kotlin编写的界面对应的url
代码语言:javascript
复制
addUrl(secondActivityUrl, SecondActivity::class.java)            addUrl(paramsActivityUrl, WithParamsActivity::class.java)
  1. 跳转页面
代码语言:javascript
复制
EasyRouter.getInstance().goToPages(this, secondActivityUrl)

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

总结

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

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

关于代码,可以参考https://github.com/wangli135/EasyRouter/tree/single_module

参考

  • https://docs.gradle.org/nightly/userguide/javaplugin.html#sec:incrementalannotation_processing
  • https://joyrun.github.io/2016/07/19/AptHelloWorld/
  • https://github.com/google/auto/tree/master/service
  • https://github.com/square/javapoet
  • https://www.jianshu.com/p/0e48cdfedfca
  • https://www.race604.com/annotation-processing/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学点Android知识 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 坑点
  • 项目结构
    • annotation模块
      • compiler模块
        • api模块
          • app模块
          • 总结
          • 参考
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档