其实本来想做的很简单,写一个简单路由可以在项目中进行使用,在github上使用的一些路由都感觉有这样那样的问题,使用起来并不是非常顺心,于是想自己写一个路由,可是开始以后才发现,把这个工作想的太简单了,这个工作比想象得复杂得多,于是把需求缩减再缩减,两天时间才写了个最最基础得功能,目前实现得就是打开一个新页面,也就是 startactivity() 并且还不能传参数,不过有了这个基础,相信后面的写起来会顺利很多,因为这两天遇到很多问题,所以就想找个地方记录一下,也理一理目前思路
首先,实现的功能是
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test1)
var router = Router()
RxView.clicks(textView1)
.compose(RxJavaUtils.preventDuplicateClicksTransformer())
.subscribe {
router.start(baseContext,"testactivity")
}
}
}
@Route("testactivity")
class TestActivity: Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
}
}
这里就是
router.start(baseContext,"testactivity")
这样在MainActivity 打开TestActivity,相当于 startActivity(Intent(baseContext,TestActivity::class.java))
如果是有一个这样的需求的话,我们理一理:
1.是需要实现一个 @Route 这样的注解
2.通过@Route 注解获取到所有注解过的activity
3.在调用router的start的时候,通过传入的 字符串匹配到@Route注解的Activity
4.把这个找到的activity打开
ok,那么一步一步做吧
1.是需要实现一个 @Route 这样的注解
对于这个注解第一步是毫无疑问的,它是写在类上面的一个注解,所以 @Target 是 ElementType.TYPE 倒是没什么问题
接下来就是这个注解的生命周期,我们肯定是在编译后甚至运行的时候依然需要使用到注解,那么保险起见就选择
@Retention(RetentionPolicy.RUNTIME)
那么简单贴一下代码
@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
annotation class Route(vararg val value: String)
这里对于@Route("testactivity")这样的注解,参数需要一个字符串就可以了,没什么问题
2.通过@Route 注解获取到所有注解过的activity
这里是比较头大的一块,参考了一些其他路由之后,决定在编译期间完成注解路由的映射表,然后在运行时,对Route进行初始化的时候完成Map的初始化
首先在编译的时候,需要生成一个map,map生成的话,我们需要一个class,class中维护一个map,将所有的@Route注解的Activity添加到map中好了,我们希望的效果是这样的两个class就ok了,当然这里使用的是java,为啥用java呢,主要后面我们需要生成代码的时候用的是一个java代码生成的lib javapoet ,没有找到kotlin生成的lib比较遗憾
public interface RouteTable {
void handle(Map> map);
}
import com.bruce.brucerouter.template.RouteTable;
import com.bruce.neo.filemanager.MainActivity;
import com.bruce.neo.filemanager.TestActivity;
import java.util.Map;
public class AppRouteTable implements RouteTable {
public AppRouteTable() {
}
public void handle(Map> map) {
map.put("test", MainActivity.class);
map.put("testactivity", TestActivity.class);
}
}
那么我们可以在我们的路由lib中定义 RouteTable 这个接口,然后在app module中生成 AppRouteTable这个类来实现这个接口。
这里有两个知识点
第一个是 AbstractProcessor 这个类,通过它我们可以在编译时得到所有注解过的Activity
第二个是 javapoet 这个库,可以使用它来进行java代码的生成
这里就不对这两块进行详细展开了,推荐几个博客
过程中遇到了各种各样的问题,当然问题来自于这些东西的不太熟悉
这里记录个卡住比较麻烦的问题
AbstractProcessor 需要在java lib中才能导入,当然,这属于项目的配置问题,参考上面的AbstractProcessor 注解处理器可以慢慢解决,其他小问题就不进行记录了,生成代码部分还未做优化,一些地方都做了注释,代码比较丑,先贴上来,优化后再进行更新
@SupportedAnnotationTypes("com.bruce.neo.annotation.Route")// 注册给哪个注解使用的
@SupportedSourceVersion(SourceVersion.RELEASE_7)//支持的jdk
class RouteProcessor : AbstractProcessor(){
lateinit var messager :Messager
private var mModuleName: String? = ""
override fun init(processorEnvironment: ProcessingEnvironment?) {
super.init(processorEnvironment)
messager = processingEnv.messager
mModuleName = processorEnvironment?.getOptions()!!.get("moduleName")!!
}
override fun process(set: MutableSet?, roundEnvironment: RoundEnvironment?): Boolean {
messager.printMessage(Diagnostic.Kind.NOTE, "start: --------------")
val elements = roundEnvironment?.getElementsAnnotatedWith(Route::class.java)
if (elements == null || elements!!.isEmpty()) return true
val typeElements = HashSet()
for (element in elements!!) {
if (element.getKind().isClass() && validateClass(element as TypeElement)) { // 注解在Class上的Route
typeElements.add(element)
}
}
if (mModuleName != null) {
val validModuleName = mModuleName.toString().replace(".", "_").replace("-", "_")
generateRouteTable(validModuleName, typeElements)
} else {
throw RuntimeException(String.format("No option `%s` passed to Route annotation processor.", "moduleName"))
}
return true
}
/**
* RouteTable.
*/
private fun generateRouteTable(moduleName: String, elements: Set) {
val mapTypeName = ParameterizedTypeName.get(ClassName.get(Map::class.java),
ClassName.get(String::class.java), ParameterizedTypeName.get(ClassName.get(Class::class.java),
WildcardTypeName.subtypeOf(Any::class.java)))// 生成 Map> map
val mapParameterSpec = ParameterSpec.builder(mapTypeName, "map").build()
val methodHandle = MethodSpec.methodBuilder("handle") // 需要生成的方法
.addAnnotation(Override::class.java)
.addModifiers(Modifier.PUBLIC)
.addParameter(mapParameterSpec)
// 记录path,防止重复的route path
val pathRecorder = HashMap()
for (element in elements) {
val paths = element.getAnnotation(Route::class.java).value
for (path in paths) {
if (pathRecorder.containsKey(path)) {
throw RuntimeException(String.format("Duplicate route path: %s[%s, %s]",path, element.qualifiedName, pathRecorder[path]))
}
methodHandle.addStatement("map.put(\$S, \$T.class)", path, ClassName.get(element))// map 中添加数据
pathRecorder[path] = element.qualifiedName.toString()
}
}
val interfaceType = processingEnv.elementUtils.getTypeElement("com.bruce.brucerouter.template.RouteTable") // 实现的接口
val type = TypeSpec.classBuilder(capitalize(moduleName) + "RouteTable")
.addSuperinterface(ClassName.get(interfaceType)) // 继承接口
.addModifiers(Modifier.PUBLIC)//class 为public
.addMethod(methodHandle.build()) //实现的方法
.addJavadoc("Generated by Router. Do not edit it!\n")//生成注释
.build()
JavaFile.builder("com.bruce.neo", type).build().writeTo(processingEnv.filer)
messager.printMessage(Diagnostic.Kind.NOTE, "end: --------------")
}
/**
* Verify the annotated class. Must be a subtype of Activity
*/
private fun validateClass(typeElement: TypeElement): Boolean {
if (!isSubtype(typeElement, "android.app.Activity")) {
return false
}
val modifiers = typeElement.modifiers
// abstract class.
if (modifiers.contains(Modifier.ABSTRACT)) {
return false
}
return true
}
private fun isSubtype(typeElement: Element, type: String): Boolean {
return processingEnv.typeUtils.isSubtype(typeElement.asType(),
processingEnv.elementUtils.getTypeElement(type).asType())
}
}
ok,这步结束了,然后next
3.在调用router的start的时候,通过传入的 字符串匹配到@Route注解的Activity
这里需要理清楚一些,我们生成的 AppRouteTable 是在app这个module下的,我们init router 的时候是在router这个lib下,app引入了router,但是 router并没有引入 app,所以在router中是不能直接访问AppRouteTable这个类的,所以这里需要使用反射进行访问,下面简单贴下代码,顺便把start的代码也一并贴上来了,未做优化,例如Router应该是个单例,还有其他的一些容错都没有做,只是最简单的代码查看
class Router {
// 获取到注册的activity
val routeTable: Map> = HashMap()
init {
var routeTableName = "com.bruce.neo.AppRouteTable"
val clz = Class.forName(routeTableName)
val instance = clz.newInstance() as RouteTable
instance.handle(routeTable)
}
fun start(context:Context,target:String){
//打印
routeTable.forEach({
Log.e("test",it.key)
Log.e("test",it.value.simpleName)
})
var intent = Intent(context,routeTable.get(target))
context.startActivity(intent)
}
ok,到这里就大功告成了,比较抽象,项目目前太过demo,就不上传了,后面进一步完善优化,能达到可以使用的时候再进行上传
后续要做的事情还有很多,至少需要支持参数的传递 startactivityForResult 实现,Fragment页面切换实现,甚至添加拦截器等等等等,一步一步做