用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閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容