用kotlin实现activity路由框架的Processor

页面路由框架,无论在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来编写,以及新功能的开发。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容