ENavigation | 一个路由组件

328 阅读5分钟

ENavigation

一个使用kotlin封装的路由框架,适用于组件化开发场景,目前支持路由自动注册,拦截器,子线程跳转,跳转动画等功能。
ENavigaiton可以用来跳转Activity,内外部Scheme,系统界面等等。GitHubDemo下载

跳转流程

image.png

解决场景

组件化开发时,需要跳转到指定的Activity,但是很多时候,目标Activity类又是不可见的,不能直接通过startActivity跳转,ENavigation便是解决这一场景的方案。另外,很多时候我们访问一个页面,需要先判断用户是否有访问该页面的权限,此时通常的做法就是在跳转之前做一个权限判断,但是如此这般,项目中就会多出很多重复的权限判断代码,使用ENavigation的拦截器便能很好的解决这一问题,只需要定义一个拦截器类即可。

依赖配置

首先需要在使用到ENavigation的模块的build.gradle文件中配置host:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'com.ricky.enavigation.plugin'
}
android {
    compileSdk 30
    // ...
}
kapt {
    arguments {
        arg("host", "app")
    }
}

dependencies {
    ...
    // 核心库
    implementation project(path: ':enavigation_impl')
    kapt project(path: ':enavigation_complier')
}

使用说明

一、初始化

使用ENavigation前请务必先初始化,建议初始化代码放在Application中,初始化方式:

class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 自动注册组件,注意需要在app模块添加自动注册插件
        ENavigation.init(this)
        // 手动注册组件,需要传入每个模块的build.gradle中配置的host
        ENavigation.init(this, "base", "app", "module1", "module1")
    }
}

自动注册和手动注册选择其中一个即可,注意如果要使用自动注册,需要在app模块的build.gradle文件中apply自动注册插件:

plugins {
    id 'com.ricky.enavigation.plugin'
}
// 或者
apply plugin: 'com.ricky.enavigation.plugin'

如果报找不到插件,请检查项目的build.gradle中是否添加了插件依赖:

buildscript {
    ...
    dependencies {
        classpath "com.ricky.enavigation:plugin:$lateast_plugin_version"
    }

    ...
}

二、基础跳转

使用路由跳转时需要在目标Activity上添加@HostAndPathAnno注解:

@HostAndPathAnno("app/test")
class TestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test1)
    }
}

这里的app/test1即是TestActivity的路由地址,之后便可通过这个地址跳转到TestActivity。
注意第一个单词代表host,必须与Activity所在模块的build.gradle中配置的host一致,第二个单词可根据Activity用途随便配置。

同模块跳转

MainActivity跳转到TestActivity,两个Activity都在app模块。

ENavigation.with(context)
    .setHostAndPath(“ app / test ”)
.navigate()

跨模块跳转

  • 目标Activity可见 MainActivity跳转到TestActivity1,TestActivity1在module1模块,app模块引用module1模块,因此TestActivity1类对MainActivity可见。
ENavigation.with(context)
    .setHostAndPath(“ app / test1 ”)
.navigate()
  • 目标Activity不可见 MainActivity跳转到TestActivity2,TestActivity2在module2模块,app模块引用base和module2模块,TestActivity2类对base模块不可见,在base模块中执行跳转。
ENavigation.with(context)
    .setHostAndPath(“ app / test1 ”)
.navigate()

子线程跳转

因为ENavigation真正执行跳转是在拦截器中执行的,而拦截器又会转移到UI线程执行,所以在子线程中也能实现跳转界面。

Thread {
    ENavigation.with(context)
        .setHostAndPath(“ app / test1 ”)
    .navigate()
}.start()

但是通常在子线程中是拿不到或者说不好拿context的,此时不传入context,也能实现跳转:

Thread {
    ENavigation.with()
        .setHostAndPath(“ app / test1 ”)
    .navigate()
}.start()

Scheme跳转

直接通过setScheme方法设置跳转的scheme即可

// 内部scheme
ENavigation.with(context)
    .setScheme("enavigation://test")
    .navigate()

// 外部scheme
ENavigation.with(context)
    .setScheme("taobao://item?id=97896794126846128")
    .navigate()

支持内部app和外部app的scheme。

系统界面跳转

这里举个跳转系统相册选照片的例子

ENavigation.with(context)
    .setAction(Intent.ACTION_GET_CONTENT)
    .setType("image/*")
    .onResult { _, _, data ->
        // 通过onResult回调取到返回的值,这里下面会讲到
        Toast.makeText(this, "uri=${data?.data}", Toast.LENGTH_SHORT).show()
    }
    .navigate()

直接设置action和type即可,ENavigation还封装了其它诸如setData,setFlags,setCategories等方法,其作用于内部的intent,在跳转时传入startActivity方法。

跳转监听

ENavigation支持跳转前后的监听,方便执行相关操作。

ENavigation.with(context)
    .setHostAndPath(PathConfig.Module1.Test4.PATH)
    .beforeAction { activity ->
        // 跳转页面之前
    }
    .afterAction { activity ->
        // 跳转页面之后
    }
    .navigate()

注意目前这两个回调都是在UI线程中执行的。

三、参数传递

参数传入

ENavigation封装了一系列设置传入参数的方法,其都作用于内部的intent,具体方法可参考系统地Intent类。这里举个跳转页面传字符串的例子。

ENavigation.with(context)
    .setHostAndPath("app/test")
    .putString("text", "hello world")
    .navigate()

上面向app/test这个路由对应的Activity传入了一个key为text,value为hello world的值。取值的方式和通过intent传值一样。

数据回传

可通过封装的onResult回调取到上个页面回传的值。

ENavigation.with(context)
    .setHostAndPath("app/test")
    .onResult { requestCode, resultCode, data ->
        val text = data?.getStringExtra("result")
        Toast.makeText(this, "requestCode=$requestCode\nresultCode=$resultCode\n收到回传的值:$text", Toast.LENGTH_SHORT).show()
    }
    .navigate()

四、拦截器

拦截器作用于目标Activity,可用于目标Activity的权限限制,如某些页面需要登录后才能访问,此时就可以使用拦截器来实现这一功能。

全局拦截器

全局拦截器作用于所有的页面跳转,实现全局拦截器需要自定义一个类,继承自INavigationInterceptor,重写intercept方法,然后给自定义的类加上@GlobalInterceptorAnno注解。

@GlobalInterceptorAnno
class MyGlobalInterceptor : INavigationInterceptor {
    override fun intercept(chain: INavigationInterceptor.Chain) {
        if (isLogin) {
            // 如果不拦截,执行chain.proceed
            chain.proceed(chain.request)
        }
    }
}

如果有多个全局拦截器,可以设置拦截器的优先级

@GlobalInterceptorAnno(priority = 1)
class MyGlobalInterceptor : INavigationInterceptor {
    override fun intercept(chain: INavigationInterceptor.Chain) {
        if (isLogin) {
            // 如果不拦截,执行chain.proceed
            chain.proceed(chain.request)
        }
    }
}

priority的值越大,优先级越高。

注解拦截器

注解拦截器作用于单个页面跳转,同样需要自定义一个拦截器类

@InterceptorAnno(name = "app/test_interceptor")
class TestInterceptor1 : INavigationInterceptor {
    override fun intercept(chain: INavigationInterceptor.Chain) {
        Toast.makeText(chain.request.activity, "拦截器1", Toast.LENGTH_SHORT).show()
        chain.proceed(chain.request)
    }
}

同路由地址一样,拦截器也需要给一个name,第一个单词对应host,第二个对应名称。当然也可以不使用注解,直接定义

class TestInterceptor2 : INavigationInterceptor {
    override fun intercept(chain: INavigationInterceptor.Chain) {
        Toast.makeText(chain.request.activity, "拦截器2", Toast.LENGTH_SHORT).show()
        chain.proceed(chain.request)
    }
}

之后需要在目标Activity的注解上添加拦截器

@HostAndPathAnno(
    PathConfig.Module1.Test6.PATH,
    interceptors = [TestInterceptor2::class],
    interceptorNames = ["app/test_interceptor1"],
)
class TestActivity6 : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test6)
    }
}

可以通过interceptors或者interceptorNames的方式为目标Activity添加拦截器。注意通过interceptors设置的拦截器类不需要添加@InterceptorAnno注解,否则就会出现拦截两次的情况。

动态添加拦截器

也可以在跳转时添加拦截器

ENavigation.with(context)
    .setHostAndPath("app/test")
    .setInterceptors(TestInterceptor())
    .navigate()

通过setInterceptors设置即可,如果需要添加多个

ENavigation.with(context)
    .setHostAndPath("app/test")
    .setInterceptors(TestInterceptor1(), TestInterceptor2(), TestInterceptor3())
    .navigate()

跳转时添加的拦截器类同样不需要添加@InterceptorAnno注解。

五、跳转动画

ENavigation封装了一系列的跳转动画,举个Activity从下面弹出的例子

ENavigation.with(this)
    .setHostAndPath("app/test")
    .bottom()
    .navigate()

跳转动画方法说明

方法名入场出场
fade()渐显渐隐
top()上面进入渐隐
right()右边进入渐隐
bottom()底部进入渐隐
left()左边进入渐隐
fadeIn()渐显-
topIn()上面进入-
rightIn()右边进入-
bottomIn()底部进入-
leftIn()左边进入-
expandTopLeftIn()左上角展开-
expandTopCenterIn()顶部展开-
expandTopRightIn()右上角展开-
expandCenterLeftIn()左边展开-
expandCenterIn()中间展开-
expandCenterRightIn()右边展开-
expandBottomLeftIn()左下角展开-
expandBottomCenterIn()下面展开-
expandBottomRightIn()右下角展开-
fadeOut()-渐隐
topOut()-顶部退出
rightOut()-右边退出
bottomOut()-下面退出
leftOut()-左边退出
shrinkTopLeftOut()-右上角退出
shrinkTopCenterOut()-上面退出
shrinkTopRightOut()-右上角退出
shrinkCenterLeftOut()-左边退出
shrinkCenterOut()-中间退出
shrinkCenterRightOut()-右边退出
shrinkBottomLeftOut()-左下角退出
shrinkBottomCenterOut()-下面退出
shrinkBottomRightOut()-右下角退出

当然也可以使用自定义的动画

ENavigation.with(this)
    .setHostAndPath("app/test")
    .animateIn(R.anim.custom_in)
    .animateOut(R.anim.custom_out)
    .navigate()

animateIn作用于跳转的Activity,animateOut作用于当前Activity。

六、异常处理

路由跳转常见的就是找不到路由的情况,再就是跳转的时候Activity已经销毁了,ENavigation封装了一些常见的异常,放在NavigationException类中

异常说明
NavigationException.ActivityDetachedExceptionActivity已经销毁
NavigationException.NullTargetException路由或Scheme不存在
NavigationException.NullActivityExceptionActivity为空,比如在Fragment中跳转
NavigationException.InvalidActivityExceptionActivity只能为FragmentActivity
NavigationException.InvalidCodeExceptionresultCode != Activity.RESULT_OK

通过onError回调监听异常

ENavigation.with(this)
    .setScheme("app/test")
    .onError { exception ->
        when (exception) {
            is NavigationException.ActivityDetachedException -> {
            }
            is NavigationException.NullActivityException -> {
            }
            is NavigationException.NullTargetException -> {
            }
            is NavigationException.InvalidCodeException -> {
            }
            is NavigationException.InvalidActivityException -> {
            }
            else -> {
                // 其它异常
            }
        }
    }
    .navigate()