之前的例子中,关于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只有一个注解,Path,用来定义url,其实现如下:
@Retention(RetentionPolicy.CLASS)@Target(ElementType.TYPE)public @interface Path { String value();}
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模块提供了对外的接口,主要是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:
app模块主要是调用方,使用方式主要有几步:
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示意一下。
addUrl(secondActivityUrl, SecondActivity::class.java) addUrl(paramsActivityUrl, WithParamsActivity::class.java)
EasyRouter.getInstance().goToPages(this, secondActivityUrl)
以上就是EasyRouter的使用手册,目前支持的功能就这些。后面会继续完善。
经历了一个五脏俱全的例子,到URL处理器,再到本章的APT收集路由,我们的路由库已经越来越完善,也可以渐渐应对一些问题了。当然,与大厂的开源路由库还是有很大的差距的,后面会继续添加功能。 目前的功能有:
关于代码,可以参考https://github.com/wangli135/EasyRouter/tree/single_module
本文分享自 每天学点Android知识 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!