从0到1实现一个Android路由——APT收集路由
Posted xingfeng_coder
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从0到1实现一个Android路由——APT收集路由相关的知识,希望对你有一定的参考价值。
从0到1实现一个android路由系列文章
- 从0到1实现一个Android路由(1)——初探路由
- 从0到1实现一个Android路由(2)——URL解析器
- 从0到1实现一个Android路由(3)——APT收集路由
- 从0到1实现一个Android路由(4)——多模块的APT收集路由
- 从0到1实现一个Android路由(5)——对Kotlin模块的支持
- 从0到1实现一个Android路由(6)——拦截请求再跳转
之前的例子中,关于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模块主要是调用方,使用方式主要有几步:
- init()设置scheme和host
class MyApplication : Application()
override fun onCreate()
super.onCreate()
EasyRouter.getInstance().init("easyrouter","demo")
- 设置监听器
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)
- 跳转页面
EasyRouter.getInstance().goToPages(this, secondActivityUrl)
以上就是EasyRouter的使用手册,目前支持的功能就这些。后面会继续完善。
总结
经历了一个五脏俱全的例子,到URL处理器,再到本章的APT收集路由,我们的路由库已经越来越完善,也可以渐渐应对一些问题了。当然,与大厂的开源路由库还是有很大的差距的,后面会继续添加功能。
目前的功能有:
- apt自动收集路由信息
- 支持初始化后再添加路由
- 支持相对url和绝对url的跳转、带参数跳转
- 外部支持设置全局监听器,用于实现路由拦截、兜底
关于代码,可以参考signle_module分支代码
参考
- https://docs.gradle.org/nightly/userguide/java_plugin.html#sec:incremental_annotation_processing
- Android APT(编译时代码生成)最佳实践
- google/auto
- JavaPoet
- 深入理解编译注解(五)RetentionPolicy.SOURCE 和 RetentionPolicy.CLASS区别讨论
- Java注解处理器
关注我的技术公众号,不定期会有技术文章推送,不敢说优质,但至少是我自己的学习心得。微信扫一扫下方二维码即可关注:
以上是关于从0到1实现一个Android路由——APT收集路由的主要内容,如果未能解决你的问题,请参考以下文章