美文网首页kotlinkotlin频道Kotlin编程
用kotlin实现activity路由框架的Processor

用kotlin实现activity路由框架的Processor

作者: fengzhizi715 | 来源:发表于2017-01-14 16:59 被阅读753次

    页面路由框架,无论在android还是在iOS的开发中都是很常见的模块与模块之间的解耦工具,特别是对中大型App而言,基本上都会有自己的路由框架。

    Processor的原理

    在讲原理之前,先看看整个项目的结构。


    SAF-Kotlin-Router结构.png
    • saf-router:是整个路由框架的核心,可以单独使用。
    • saf-router-annotation:是路由框架的注解模块,可以基于注解来声明router跳转的页面。
    • saf-router-compiler:由于我们的注解是编译时注解,而非运行时注解。在程序编译时会生成一个RouterManager的类,此类会管理App的router mapping信息。
    RouterManager的生成.png

    然后,我们来看看神奇的RouterProcessor

    package com.safframework.router
    
    import com.squareup.javapoet.ClassName
    import com.squareup.javapoet.JavaFile
    import com.squareup.javapoet.MethodSpec
    import com.squareup.javapoet.TypeSpec
    import java.util.*
    import javax.annotation.processing.*
    import javax.lang.model.SourceVersion
    import javax.lang.model.element.Element
    import javax.lang.model.element.Modifier
    import javax.lang.model.element.TypeElement
    import javax.lang.model.util.Elements
    
    /**
     * Created by Tony Shen on 2017/1/10.
     */
    //@AutoService(Processor::class)
    class RouterProcessor: AbstractProcessor() {
    
        var mFiler: Filer?=null //文件相关的辅助类
        var mElementUtils: Elements?=null //元素相关的辅助类
        var mMessager: Messager?=null //日志相关的辅助类
    
        @Synchronized override fun init(processingEnv: ProcessingEnvironment) {
            super.init(processingEnv)
            mFiler = processingEnv.filer
            mElementUtils = processingEnv.elementUtils
            mMessager = processingEnv.messager
        }
    
        /**
         * @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
         */
        override fun getSupportedSourceVersion(): SourceVersion {
            return SourceVersion.latestSupported()
        }
    
        /**
         * @return 指定哪些注解应该被注解处理器注册
         */
        override fun getSupportedAnnotationTypes(): Set<String> {
            val types = LinkedHashSet<String>()
            types.add(RouterRule::class.java.canonicalName)
            return types
        }
    
        override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
            val elements = roundEnv.getElementsAnnotatedWith(RouterRule::class.java)
    
            try {
                val type = getRouterTableInitializer(elements)
                if (type != null) {
                    JavaFile.builder("com.safframework.router", type).build().writeTo(mFiler)
                }
            } catch (e: FilerException) {
                e.printStackTrace()
            } catch (e: Exception) {
                Utils.error(mMessager, e.message)
            }
    
            return true
        }
    
        @Throws(ClassNotFoundException::class)
        private fun getRouterTableInitializer(elements: Set<Element>?): TypeSpec? {
            if (elements == null || elements.size == 0) {
                return null
            }
    
            val routerInitBuilder = MethodSpec.methodBuilder("init")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .addParameter(TypeUtils.CONTEXT, "context")
    
            routerInitBuilder.addStatement("\$T.getInstance().setContext(context)", TypeUtils.ROUTER)
            routerInitBuilder.addStatement("\$T options = null", TypeUtils.ROUTER_OPTIONS)
    
            elements.map {
                it as TypeElement
            }.filter(fun(it: TypeElement): Boolean {
                return Utils.isValidClass(mMessager, it, "@RouterRule")
            }).forEach {
                val routerRule = it.getAnnotation(RouterRule::class.java)
                val routerUrls = routerRule.url
                val enterAnim = routerRule.enterAnim
                val exitAnim = routerRule.exitAnim
                if (routerUrls != null) {
                    for (routerUrl in routerUrls!!) {
                        if (enterAnim > 0 && exitAnim > 0) {
                            routerInitBuilder.addStatement("options = new \$T()", TypeUtils.ROUTER_OPTIONS)
                            routerInitBuilder.addStatement("options.enterAnim = " + enterAnim)
                            routerInitBuilder.addStatement("options.exitAnim = " + exitAnim)
                            routerInitBuilder.addStatement("\$T.getInstance().map(\$S, \$T.class,options)", TypeUtils.ROUTER, routerUrl, ClassName.get(it))
                        } else {
                            routerInitBuilder.addStatement("\$T.getInstance().map(\$S, \$T.class)", TypeUtils.ROUTER, routerUrl, ClassName.get(it))
                        }
                    }
                }
            }
    
            val routerInitMethod = routerInitBuilder.build()
    
            return TypeSpec.classBuilder("RouterManager")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(routerInitMethod)
                    .build()
        }
    }
    

    kotlin用起来是很爽,但是还是踩过很多的坑。

    • 坑1:
      原先用java来写时,用谷歌的Auto库很顺畅地生成RouterManager类。换了kotlin以后,好像不行了,于是我用了土方法。创建了META-INF/services/javax.annotation.processing.Processor,并加上
    com.safframework.router.RouterProcessor
    

    这样才能生成RouterManager。

    • 坑2:
      原先getRouterTableInitializer()是长这样的:
        private TypeSpec getRouterTableInitializer(Set<? extends Element> elements) throws ClassNotFoundException {
            if(elements == null || elements.size() == 0){
                return null;
            }
    
            MethodSpec.Builder routerInitBuilder = MethodSpec.methodBuilder("init")
                    .addModifiers(Modifier.PUBLIC,Modifier.STATIC)
                    .addParameter(TypeUtils.CONTEXT,"context");
    
            routerInitBuilder.addStatement("$T.getInstance().setContext(context)",TypeUtils.ROUTER);
            routerInitBuilder.addStatement("$T options = null",TypeUtils.ROUTER_OPTIONS);
    
            for(Element element : elements){
                TypeElement classElement = (TypeElement) element;
    
                // 检测是否是支持的注解类型,如果不是里面会报错
                if (!Utils.isValidClass(mMessager,classElement,"@RouterRule")) {
                    continue;
                }
    
                RouterRule routerRule = element.getAnnotation(RouterRule.class);
                String [] routerUrls = routerRule.url();
                int enterAnim = routerRule.enterAnim();
                int exitAnim = routerRule.exitAnim();
                if(routerUrls != null){
                    for(String routerUrl : routerUrls){
                        if (enterAnim>0 && exitAnim>0) {
                            routerInitBuilder.addStatement("options = new $T()",TypeUtils.ROUTER_OPTIONS);
                            routerInitBuilder.addStatement("options.enterAnim = "+enterAnim);
                            routerInitBuilder.addStatement("options.exitAnim = "+exitAnim);
                            routerInitBuilder.addStatement("$T.getInstance().map($S, $T.class,options)",TypeUtils.ROUTER, routerUrl, ClassName.get((TypeElement) element));
                        } else {
                            routerInitBuilder.addStatement("$T.getInstance().map($S, $T.class)",TypeUtils.ROUTER, routerUrl, ClassName.get((TypeElement) element));
                        }
                    }
                }
            }
    
            MethodSpec routerInitMethod = routerInitBuilder.build();
    
            return TypeSpec.classBuilder("RouterManager")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(routerInitMethod)
                    .build();
        }
    

    我用kotlin对for循环进行了优化,起初还可以用map、filter,但是遇到两层for循环好像找不到更好的办法。本想用高阶函数,但是不想折腾了。如果您有更好的办法,一定要告诉我。

    • 坑3:
      Kotlin的类没有静态变量。不过有同伴对象(Companion Object)的概念。如果在某个类中声明一个同伴对象, 那么只需要使用类名作为限定符就可以调用同伴对象的成员了, 语法与Java中调用类的静态方法、静态变量一样。

    举个栗子:

    class TypeUtils {
    
        companion object {
            val CONTEXT = ClassName.get("android.content", "Context");
            val ROUTER = ClassName.get("com.safframework.router", "Router")
            val ROUTER_OPTIONS = ClassName.get("com.safframework.router.RouterParameter", "RouterOptions")
        }
    }
    

    既然踩了很多坑,那还是放上github地址吧:
    https://github.com/fengzhizi715/SAF-Kotlin-Router

    下载安装

    在根目录下的build.gradle中添加

     buildscript {
         repositories {
             jcenter()
         }
         dependencies {
             classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
         }
     }
    

    在app 模块目录下的build.gradle中添加

    apply plugin: 'com.neenbedankt.android-apt'
    
    ...
    
    dependencies {
        compile 'com.safframework.router:saf-router:1.0.0'
        apt 'com.safframework.router:saf-router-compiler:1.0.0'
        ...
    }
    

    特性

    它提供了类似于rails的router功能,可以轻易地实现app的应用内跳转,包括Activity之间、Fragment之间实现相互跳转,并传递参数。

    这个框架的saf-router-compiler模块是用kotlin编写的。

    使用方法

    Activity跳转

    它支持Annotation方式和非Annotation的方式来进行Activity页面跳转。使用Activity跳转时,必须在App的Application中做好router的映射。

    我们会做这样的映射,表示从某个Activity跳转到另一个Activity需要传递user、password这2个参数

    Router.getInstance().setContext(getApplicationContext()); // 这一步是必须的,用于初始化Router
    Router.getInstance().map("user/:user/password/:password", DetailActivity.class);
    

    有时候,activity跳转还会有动画效果,那么我们可以这么做

    RouterOptions options = new RouterOptions();
    options.enterAnim = R.anim.slide_right_in;
    options.exitAnim = R.anim.slide_left_out;
    Router.getInstance().map("user/:user/password/:password", DetailActivity.class, options);
    

    Annotation方式

    在任意要跳转的目标Activity上,添加@RouterRule,它是编译时的注解。

    @RouterRule(url={"second/:second"})
    public class SecondActivity extends Activity {
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            Intent i = getIntent();
            if (i!=null) {
                String second = i.getStringExtra("second");
                Log.i("SecondActivity","second="+second);
            }
        }
    }
    
    

    而且,使用@RouterRule也支持跳转的动画效果。

    如果要跳转到SecondActivity,在App的任意地方只需:

    Router.getInstance().open("second/1234"); // 1234表示传递的参数,是String类型。
    

    用Annotation方式来进行页面跳转时,Application无需做router的映射。因为,saf-router-compiler模块已经在编译时生成了一个类RouterManager。它长得形如:

    package com.safframework.router;
    
    import android.content.Context;
    import com.safframework.activity.SecondActivity;
    import com.safframework.router.RouterParameter.RouterOptions;
    
    public class RouterManager {
      public static void init(Context context) {
        Router.getInstance().setContext(context);
        RouterOptions options = null;
        Router.getInstance().map("second/:second", SecondActivity.class);
      }
    }
    

    Application只需做如下调用,就可在任何地方使用Router了。

    RouterManager.init(this);// 这一步是必须的,用于初始化Router
    
    

    非Annotation方式

    在Application中定义好router映射之后,activity之间跳转只需在activity中写下如下的代码,即可跳转到相应的Activity,并传递参数

    Router.getInstance().open("user/fengzhizi715/password/715");
    

    如果在跳转前需要先做判断,看看是否满足跳转的条件,doCheck()返回false表示不跳转,true表示进行跳转到下一个activity

    Router.getInstance().open("user/fengzhizi715/password/715",new RouterChecker(){
    
         public boolean doCheck() {
               return true;
          }
     );
    

    Fragment跳转

    Fragment之间的跳转也无须在Application中定义跳转映射。直接在某个Fragment写下如下的代码

    Router.getInstance().openFragment(new FragmentOptions(getFragmentManager(),new Fragment2()), R.id.content_frame);
    

    当然在Fragment之间跳转可以传递参数

    Router.getInstance().openFragment("user/fengzhizi715/password/715",new FragmentOptions(getFragmentManager(),new Fragment2()), R.id.content_frame);
    

    其他跳转

    单独跳转到某个网页,调用系统电话,调用手机上的地图app打开地图等无须在Application中定义跳转映射。

    Router.getInstance().openURI("http://www.g.cn");
    
    Router.getInstance().openURI("tel://18662430000");
    
    Router.getInstance().openURI("geo:0,0?q=31,121");
    

    总结

    最后,使用这个框架是不需要先有的程序去配置Kotlin的环境的。
    未来,会考虑把这个项目的其余模块也都用Kotlin来编写,以及新功能的开发。

    相关文章

      网友评论

      • cntlb:Groovy写apt也挺爽的, 没用过AutoService,手动配置也不麻烦哈哈. 另外博主这个库应该不可以有多个@Modules, 否则就有多个RouterManager在同一个包下面生成. 即使修改了包名,到时候收集这些RouterManager也是很麻烦的.
        fengzhizi715:额 其实是有@Module和@Modules的
        详情见:https://github.com/fengzhizi715/SAF-Kotlin-Router#4-%E6%A8%A1%E5%9D%97%E5%8C%96

      本文标题:用kotlin实现activity路由框架的Processor

      本文链接:https://www.haomeiwen.com/subject/ccljbttx.html