如何更好地进行 Android 组件化开发(四)登录拦截篇

1,378 阅读8分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

路由框架基本都会有路由拦截器,能实现 AOP 的功能,在跳转前、跳转后做一些自定义的逻辑处理。还有一个比较经典的应用场景,就是在跳转过程中处理登录事件,这样就不需要在目标页重复做登录校验。

封装一套登录拦截功能可以提高一定的开发效率,不过如果尝试过用路由的拦截器去封装,会发现有一些地方不太好处理,封装起来可能并不是那么顺利。下面个人会分享一些思路来帮助大家使用路由框架来封装登录拦截功能。

实现登录拦截

先了解一下路由的拦截器怎么使用,本文使用的路由框架是 ARouter,其它路由框架的用法也是大同小异。

ARouter 需要创建一个类实现 IInterceptor 接口,并添加 @Interceptor 注解声明优先级。实现登录拦截要判断一下 postcard.path 是不是需要校验登录的 path,是的话就中断原本的路由,然后跳到登录页面。

@Interceptor(priority = 8, name = "登录拦截器")
class SignInInterceptor : IInterceptor {

  override fun process(postcard: Postcard, callback: InterceptorCallback) {
    if (isInterceptPath(postcard.path)) {
      ARouter.getInstance().build("/account/sign_in")
        .with(postcard.extras) // 传递参数
        .withString(KEY_NEXT_ROUTE_PATH, postcard.path) // 传递目标页面的 path
        .withTransition(postcard.enterAnim, postcard.exitAnim)
        .navigation()
      callback.onInterrupt(RuntimeException("The route path of '${postcard.path}' requires sign in."))
    } else {
      callback.onContinue(postcard)
    }
  }

  override fun init(context: Context) = Unit
}

重写的 process() 函数有两个参数,其中的 callback 参数是用于决定是否进行拦截,调用了 callback.onInterrupt(exception) 中断路由流程,而调用 callback.onContinue(postcard) 会交还控制权。至少需要调用其中一个,否则不会继续路由。

上面的拦截器仅仅实现了拦截页面跳转到登录页面,这当然还不够,因为我们登录完还需要继续跳转到目标页面。那么可以在登录之后查一下 intent 有没下一个页面的 path,有的话就进行路由跳转,没有的话就关闭页面。我们添加一个 Activity 的扩展:

fun Activity.signInSuccess(enterAnim: Int = -1, exitAnim: Int = -1) {
  val path = intent.getStringExtra(SignInInterceptor.KEY_NEXT_ROUTE_PATH)
  if (path != null) {
    intent.removeExtra(SignInInterceptor.KEY_NEXT_ROUTE_PATH)
    ARouter.getInstance().build(path)
      .with(intent.extras)
      .withTransition(enterAnim, exitAnim)
      .navigation()
  } else {
    setResult(Activity.RESULT_OK)
    if (enterAnim != -1 && exitAnim != -1) {
      overridePendingTransition(enterAnim, exitAnim)
    }
  }
  finish()
}

在登录成功之后需要调用一下 signInSuccess() 扩展,这样才会继续之前的路由跳转。

前面的 SignInInterceptor 再优化一下,通过一些配置去决定是否进行登录拦截。首先需要有登录页面的 path 和需要校验登录的 path,并且还要有个方法能验证 App 有没登录。我们给 SignInInterceptor 添加一个静态的初始化函数配置这些信息,这样就更加通用一点了。

@Interceptor(priority = Int.MAX_VALUE)
class SignInInterceptor : IInterceptor {

  override fun process(postcard: Postcard, callback: InterceptorCallback) {
    if (isInterceptPath(postcard.path)) {
      ARouter.getInstance().build("/account/sign_in")
        .with(postcard.extras)
        .withString(KEY_NEXT_ROUTE_PATH, postcard.path)
        .withTransition(postcard.enterAnim, postcard.exitAnim)
        .navigation()
      callback.onInterrupt(RuntimeException("The route path of '${postcard.path}' requires sign in."))
    } else {
      callback.onContinue(postcard)
    }
  }

  override fun init(context: Context) = Unit

  companion object {
    const val KEY_NEXT_ROUTE_PATH = "next_route_path"
    internal var signInActivityPath: String? = null
    internal var requireSignInPaths = arrayListOf<String>()
    internal var checkSignIn: () -> Boolean = { true }

    fun init(signInActivityPath: String, requireSignInPaths: List<String>, block: () -> Boolean) {
      this.signInActivityPath = signInActivityPath
      this.requireSignInPaths.addAll(requireSignInPaths)
      checkSignIn = block
    }

    fun isInterceptPath(path: String) =
      signInActivityPath != null && path != signInActivityPath && requireSignInPaths.contains(path) && !checkSignIn()
  }
}

我们初始化该拦截器就会启动登录拦截功能。

SignInInterceptor.init("/account/sign_in", listOf("/app/main", ...)) {
  AccountRepository.isSignIn
}

完善转场动画

路由的拦截功能通常是个异步操作,所以想跳转某个页面并关闭当前页面会有点麻烦。按照我们平时的习惯会这么来写代码:

ARouter.getInstance().build("/xx/xxx").navigation()
finish()

运行起来会发现有白屏的转场动画,这是因为 navigation() 函数内调用了这一段拦截代码:

image.png

这里判断了是不是绿色通道(Activity 类型默认为 false),不是的话就走拦截的流程,如果全部拦截器都不拦截,那才会回调到 onContinue() 函数继续跳转。我们再找一下 onContinue() 函数执行的地方:

image.png

可以很清楚地看到是这是一个异步回调,那么即使我们的代码是先调用路由跳转再调 finish(),但是最终的逻辑是先调用 finish() 再异步调用 startActivity(),导致转场动画会白屏。

要解决这个问题有两种方式,一种是屏蔽路由拦截功能,配置为绿色通道,这样路由跳转时就立即执行 startActivity()。

ARouter.getInstance().build("/xx/xxx").greenChannel().navigation()
finish()

这会忽略所有拦截器,一般不建议调用,除非真有这样的需求。路由框架一般还能监听导航的回调,其中会有个回调函数是在 startActivity() 之后才执行,那在该函数回调时才 finish() 就没有问题。我们在 navigation() 函数加多一个 NavCallback 参数:

ARouter.getInstance().build("/xx/xxx").navigation(this, object : NavCallback() {
  override fun onArrival(postcard: Postcard?) {
    finish()
  }
})

我们可以看下源码确认一下执行顺序,callback.onArrival() 确实在 startActivity() 之后调用。

image.png

我们优化一下前面的 signInSuccess(),从登录页面用路由跳转到目标页面要在 onArrival() 的时候才 finish()。

fun Activity.signInSuccess(enterAnim: Int = -1, exitAnim: Int = -1) {
  val path = intent.getStringExtra(SignInInterceptor.KEY_NEXT_ROUTE_PATH)
  if (path != null) {
    intent.removeExtra(SignInInterceptor.KEY_NEXT_ROUTE_PATH)
    ARouter.getInstance().build(path)
      .with(intent.extras)
      .withTransition(enterAnim, exitAnim)
      .navigation(this, object : NavCallback() {
        override fun onArrival(postcard: Postcard?) {
          finish()
        }
      })
  } else {
    setResult(Activity.RESULT_OK)
    if (enterAnim != -1 && exitAnim != -1) {
      overridePendingTransition(enterAnim, exitAnim)
    }
    finish()
  }
}

现在路由跳转后关闭当前页面是没问题了,但是如果被拦截跳到登录页面,是会中断原来要执行的 startActivity() 操作,那后续的 callback.onArrival() 也不会回调,本来该执行 finish() 也中断了。也就是说正常路由跳转会关闭当前页面,被拦截后不会关闭当前页面了。

想在拦截后仍然能保留 onArrival() 回调并不好处理,因为在拦截器是没法回调 callback.onArrival() 的。个人做些了尝试后想到了个思路,在登录的拦截器里不做跳转的操作,只调用 callback.onInterrupt(exception) 中断路由。

@Interceptor(priority = Int.MAX_VALUE)
class SignInInterceptor : IInterceptor {

  override fun process(postcard: Postcard, callback: InterceptorCallback) {
    if (isInterceptPath(postcard.path)) {
      callback.onInterrupt(RuntimeException("The route path of '${postcard.path}' requires sign in."))
    } else {
      callback.onContinue(postcard)
    }
  }

  // ...
}

如果路由操作被拦截器中断后,会回调 NavCallback 的 onInterrupt(postcard) 函数,我们在这里才用路由跳转到登录页面,这样在 NavCallback 内可以调用原本 onArrival(postcard) 函数。

open class SignInNavCallback : NavCallback() {

  override fun onArrival(postcard: Postcard) = Unit

  override fun onInterrupt(postcard: Postcard) {
    if (SignInInterceptor.isInterceptPath(postcard.path)) {
      ARouter.getInstance().build(SignInInterceptor.signInActivityPath!!)
        .with(postcard.extras)
        .withString(SignInInterceptor.KEY_NEXT_ROUTE_PATH, postcard.path)
        .withTransition(postcard.enterAnim, postcard.exitAnim)
        .navigation(postcard.context, object : NavCallback() {
          override fun onArrival(postcard: Postcard) {
            this@SignInNavCallback.onArrival(postcard)
          }
        })
    } else {
      onOtherInterrupt(postcard)
    }
  }

  open fun onOtherInterrupt(postcard: Postcard) = Unit
}

最开始路由跳转的 onArrival() 被中断了不会回调,那我们自己想办法在后面登录页面的路由跳转执行回原本 onArrival() 函数。

把 NavCallback 改成 SignInNavCallback,这样即使被登录拦截了也能关闭当前页面。

ARouter.getInstance().build("/xx/xxx").navigation(this, object : SignInNavCallback() {
  override fun onArrival(postcard: Postcard?) {
    finish()
  }
})

不过还有个问题,在不需要 onArrival() 的时候也得传入 SignInNavCallback,否则跳转不过登录页面。

ARouter.getInstance().build("/xx/xxx").navigation(this, SignInNavCallback())

这么用的话就有点奇怪了,所以个人建议再封装一个 startActivity() 函数的扩展把路由跳转的代码给隐藏了,内部会固定传一个 SignInNavCallback。

fun Context.startActivity(
  routePath: String, vararg pairs: Pair<String, Any?>, enterAnim: Int = -1, exitAnim: Int = -1,
  onArrival: ((Postcard) -> Unit)? = null, onInterrupt: ((Postcard) -> Unit)? = null,
  onFound: ((Postcard) -> Unit)? = null, onLost: ((Postcard) -> Unit)? = null,
) {
  ARouter.getInstance().build(routePath)
    .with(bundleOf(*pairs))
    .withTransition(enterAnim, exitAnim)
    .navigation(this, object : SignInNavCallback() {
      override fun onArrival(postcard: Postcard) {
        onArrival?.invoke(postcard)
      }

      override fun onOtherInterrupt(postcard: Postcard) {
        onInterrupt?.invoke(postcard)
      }

      override fun onFound(postcard: Postcard) {
        onFound?.invoke(postcard)
      }

      override fun onLost(postcard: Postcard) {
        onLost?.invoke(postcard)
      }
    })
}

这样路由跳转的用法就更类似于原生的跳转,并且内部的实现保证了登录拦截的逻辑是完整的。

startActivity("/xx/xxx")

如果需要传参或路由跳转后关闭当前页面,可以这么写:

startActivity("/xx/xxx", "email" to email, onArrival = {
  finish()
})

使用注解配置 path

我们回顾一下前面登录拦截器初始化的时候是直接传入了登录页面的 path 和其它需要验证登录的 path。

SignInInterceptor.init("/account/sign_in", listOf("/message/message_list", ...)) {
  AccountRepository.isSignIn
}

这个拦截器的初始化通常会在用户组件里执行,那么就会有个问题,用户组件怎么知道哪些 path 需要校验登录状态呢?有的人可能会说把全部需要登录拦截的 path 都写上不就好了,但是每当其它组件新增了 path 需要添加登录拦截,就会让负责用户组件的同事改一下代码,这样协作开发的方式并不好。

所以最好是哪个组件有 path 需要登录拦截都可以自己配置,这可以参考路由框架用注解来实现。比如我们给一个 path 常量添加 @RequireSignInPath 注解,在路由跳转时就会先验证一下有没登录。

@RequireSignInPath
const val PATH_MESSAGE_LIST = "/message/message_list"

这种效果要怎么实现呢?这会到 APT (Annotation Processing Tool) 注解处理器,APT 可以对源代码文件进行检测找出其中的注解,并对注解进行额外的处理,通常是生成一些我们所需的模板类。

首先我们创建一个接口,定义个 loadInto() 函数,参数是字符串数组。

interface IRoutePaths {
  fun loadInto(list: ArrayList<String>)
}

调用 loadInto() 函数就把一些字符串添加到一个数组中。我们可以用 APT 生成 IRoutePaths 的实现类,添加 @RequireSignInPath 注解修饰的字符串常量,比如生成了以下的类类:

public class ARouter$$Paths$$message implements IRoutePaths {
  @Override
  public void loadInto(ArrayList<String> paths) {
    paths.add(com.dylanc.componentization.message.constants.ConstantsKt.PATH_MESSAGE_LIST);
  }
}

有这个类后只需反射该类的实例调用 loadInto(paths) 函数就完成了登录拦截的 path 配置,ARouter 的路由表也是这样配置的。接下来我们讲一下怎么用 APT 生成文件。

使用 APT 生成 Java 文件

首先得创建一个 Java 模块用来处理注解,这里我们创建一个 router-compiler 模块,记得要选择 Java or Kotlin Library

image.png

使用 kapt 的方式依赖该模块,如果是 Java 项目把 kapt 改成 annotationProcessor。

dependencies {
    kapt project(path: ':router-compiler')
}

这样会访问 router-compiler 里一个固定的文件:

image.png

注意这里并不是有一个叫 META-INF.services 的文件夹,而是有 META-INF 和 services 两个文件夹,文件名是 javax.annotation.processing.Processor,这些都是固定的。

文件的内容就只有注解处理器的全类名,比如:

com.dylanc.componentization.router.compiler.PathsProcessor

那么在编译的时候会扫描 javax.annotation.processing.Processor 的内容,我们写了一个 PathsProcessor 注解处理器,就会根据该注解处理器的逻辑去生成文件。

在编写 PathsProcessor 注解处理器的代码之前我们还得先有注解,我们定义一个 RequireSignInPath 注解,用来标记需要验证登录的 path 常量。

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
annotation class RequireSignInPath

给 router-compiler 模块添加 JavaPoet 依赖,JavaPoet 提供了一些生成 Java 文件的 API。

dependencies {
  implementation "com.squareup:javapoet:1.13.0"
}

之后就能编写注解处理器了,需要继承 AbstractProcessor 类。重写 getSupportedAnnotationTypes() 声明会用到哪些注解,然后就能在 process() 函数编写生成文件的代码了。通过注解能获取到类或属性的信息,再调用 JavaPoet 的 API 去生成自己所需的 Java 文件。API 怎么用就不介绍了,大家自行了解一下。下面是我们这次需要生成的代码,稍微阅读下其实也很容易理解生成了怎么样的 Java 代码。

class PathsProcessor : AbstractProcessor() {
  // ...

  override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
    val checkLoginPathElements = roundEnv.getElementsAnnotatedWith(RequireSignInPath::class.java).map { it as VariableElement }
    val arrayListType = ParameterizedTypeName.get(ClassName.get(ArrayList::class.java), ClassName.get(String::class.java))
    val paramSpec = ParameterSpec.builder(arrayListType, "paths").build()
    val typeIRoutePaths = elementUtils.getTypeElement(IROUTE_PATHS)

    val loadIntoMethodBuilder = MethodSpec.methodBuilder("loadInto")
      .addAnnotation(Override::class.java)
      .addModifiers(Modifier.PUBLIC)
      .addParameter(paramSpec)

    checkLoginPathElements.forEach { element ->
      loadIntoMethodBuilder.addStatement("paths.add(${element.fullClassName}.${element.simpleName})")
    }

    val typeSpec = TypeSpec.classBuilder(ClassName.get(PACKAGE_NAME, "$PROJECT${SEPARATOR}Paths$SEPARATOR$moduleName"))
      .addSuperinterface(ClassName.get(typeIRoutePaths))
      .addModifiers(Modifier.PUBLIC)
      .addMethod(loadIntoMethodBuilder.build())
      .build()

    try {
      JavaFile.builder(PACKAGE_NAME, typeSpec).build().writeTo(filer)
    } catch (e: IOException) {
      e.printStackTrace()
    }
    return false
  }

  override fun getSupportedAnnotationTypes() = setOf(RequireSignInPath::class.java.canonicalName)

  override fun getSupportedSourceVersion() = SourceVersion.RELEASE_8

  private val VariableElement.fullClassName: String
    get() = ClassName.get(enclosingElement.asType()).toString()
}

上面其实还少了获取当前模块名的逻辑,如果多个模块生成的都是同名类就会有问题。

那要怎么获取模块名呢?个人之前也不是很清楚,阅读了 ARouter 源码找到了以下逻辑:

class PathsProcessor : AbstractProcessor() {
  private lateinit var filer: Filer
  private lateinit var elementUtils: Elements
  private var moduleName: String? = null

  override fun init(processingEnv: ProcessingEnvironment) {
    super.init(processingEnv)
    filer = processingEnv.filer
    elementUtils = processingEnv.elementUtils

    val options = processingEnv.options
    if (options.isNotEmpty()) {
      moduleName = options["AROUTER_MODULE_NAME"]
    }
    if (!moduleName.isNullOrEmpty()) {
      moduleName = moduleName!!.replace("[^0-9a-zA-Z_]+".toRegex(), "")
      print("The user has configuration the module name, it was [$moduleName]")
    } else {
      throw RuntimeException("ARouter::Compiler >>> No module name, for more information, look at gradle log.")
    }
  }

  override fun getSupportedOptions() = setOf("AROUTER_MODULE_NAME")

  // ...
}

可以看到 moduleName 是通过 processingEnv.options 获取的,key 值是 AROUTER_MODULE_NAME。用过 ARouter 肯定对这个 key 不陌生,因为要在 build.gradle 添加下面的代码:

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

我们在 build.gradle 给 AROUTER_MODULE_NAME 设置了 project.getName(),也就是说配置了模块名。那么在注解处理器的 getSupportedOptions() 函数返回 AROUTER_MODULE_NAME 字符串,就声明了需要用到这个 key 的参数,之后能通过 processingEnv.options 获取到对应值,从而得到模块名。

这样通过 PathsProcessor 就能生成我们所需的 Java 文件:

image.png

实例化 Java 类配置 path

生成文件的类名是 ARouter$$Paths$$modulename,这是参考了 ARouter 的 APT 命名规则,并且包名也保持了一致。我们找一下编译生成的代码,就能看到在一堆 ARouter 生成的 Java 文件中混进去了我们自己生成的文件。

image.png

为什么要这么做呢?因为 ARouter 会遍历 com.alibaba.android.arouter.routes 包下的文件,那么我们伪装成 ARouter 生成的文件,让 ARouter 顺便把我们生成的类找出来,这就能省去遍历找类的步骤了。

我们可以用 App Startup 实现自动初始化,对 App Startup 不熟悉的小伙伴可以看看前面的文章

通过阅读 ARouter 的源码能发现 ARouter 在遍历文件后,会将遍历到的类名保存到 SharedPreferences 中。那么我们在初始化 ARouter 后,就能用 SharedPreferences 获取 ARouter 找过的类名,如果类名是我们定义的 ARouter$$Paths$$ 开头,就使用反射实例化对象,并强转成 IRoutePaths 接口调用 loadInto() 函数。

class RouterInitializer : Initializer<Unit> {
  
  @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
  override fun create(context: Context) {
    val applicationInfo = context.packageManager.getApplicationInfo(context.packageName, 0)
    val isDebug = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
    if (isDebug) {
      ARouter.openLog()
      ARouter.openDebug()
    }
    ARouter.init(context as Application)

    try {
      val sharedPreferences = context.getSharedPreferences(Consts.AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE)
      val routerMap = HashSet(sharedPreferences.getStringSet(Consts.AROUTER_SP_KEY_MAP, HashSet()))
      for (className in routerMap) {
        if(className.startsWith("${Consts.ROUTE_ROOT_PAKCAGE}.ARouter$$Paths$$")){
          (Class.forName(className).newInstance() as? IRoutePaths)?.loadInto(SignInInterceptor.requireSignInPaths)
        }
      }

      ARouter.logger.debug("Paths", SignInInterceptor.requireSignInPaths.toString())
    } catch (_: NoSuchFieldException) {
    } catch (e: Exception) {
      e.printStackTrace()
    }
  }

  override fun dependencies() = emptyList<Class<Initializer<*>>>()
}

在 manifests 声明 Provider 就能自动初始化了。

<application>
  <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
      android:name="com.dylanc.componentization.router.RouterInitializer"
      android:value="androidx.startup" />
  </provider>
</application>

这样 SignInInterceptor.requireSignInPaths 数组就会自动添加需要验证登录的 path 了。

同理登录页面的 path 也能用注解配置,我们可以再添加一个 @SignInActivityPath 注解实现类似的逻辑。既然 path 都用注解配置了,那么 SignInInterceptor 的初始化只需传入一个回调去判断是否登录了。

SignInInterceptor.init { AccountRepository.isSignIn }

优化依赖关系

前面登录拦截的封装代码我们会放到一个 router 模块,用 APT 生成 Java 文件的代码放在 router-compiler 模块,那么注解类 RequireSignInPath 和 SignInActivityPath 该放在哪个模块呢?

注解类需要在两个地方使用,首先会在 router-compiler 模块生成 Java 文件时用到,其次要在各个组件模块修饰需要登录拦截的 path 常量。如果注解类放在 router-compiler 模块,用了 kapt 依赖的模块是没法访问到里面的代码的。通常把注解类抽取到一个 router-annotations 中,给 router-compiler 模块和各个组件模块依赖。

这样我们就有了三个模块:

image.png

那么我们在组件模块需要添加三个依赖才能使用登录拦截功能。

implementation project(path: ':router')              // 用生成的文件配置 path
implementation project(path: ':router-annotations')  // 提供注解
kapt project(path: ':router-compiler')               // 生成文件

这样使用的话就有点麻烦了,可以优化 router-annotations 依赖放到 router 模块中,通过 api 的依赖方式会对上层模块可见。

api project(path: ':router-annotations')

这就只需要添加两行依赖了。

implementation project(path: ':router')
kapt project(path: ':router-compiler')

使用了 APT 的第三方库通常也是这样的结构,比如 ButterKnife 也会有个 butterknife-annotations 依赖,但是我们重来没有添加过,因为放到了 butterknife 依赖中。

完整用法

首先在账户组件初始化 SignInInterceptor,告诉拦截器如何判断是否登录了。

SignInInterceptor.init { AccountRepository.isSignIn }

给登录页面的 path 添加 @SignInActivityPath 注解。

@SignInActivityPath
const val PATH_SIGN_IN = "/account/sign_in"

在登录页面请求登录成功后调用了 signInSuccess() 扩展函数才能继续跳转之前拦截的页面,这样准备工作就完成了。

@Route(path = PATH_SIGN_IN)
class SignInActivity : BaseActivity<AccountActivitySignInBinding>() {
  // ...
  
  private fun onRequestSuccess(account: Account) {
    // ...
   
    signInSuccess()
  }
}

如果有页面要做登录拦截,就给对应的 path 常量添加 @RequireSignInPath 注解。

@RequireSignInPath
const val PATH_NAIN = "/app/main"

需要路由跳转的时候要改成更简洁的 startActivity() 扩展进行跳转,这样没有登录才会先跳到登录页面。

lifecycleScope.launch { 
  delay(2000)
  startActivity(PATH_MAIN, onArrival = { finish() })
}

总结

本文介绍路由拦截器的用法,可以在跳转前、跳转后做一些自定义的逻辑处理,可以在跳转过程中处理登录事件。

之后带着大家用 ARouter 封装了一套登录拦截的功能,用法比较通用,换成其它路由框架实现也是可以的,整体的封装的思路和步骤都是类似的:

  1. 使用拦截器中断拦截一些需要校验登录的路由,并跳转到登录页面;
  2. 在登录页面登录成功后需要继续跳转之前拦截的路由,提供一个 signInSuccess() 的扩展在登录成功后调用;
  3. 适配路由跳转后关闭当前页面的用法,通常监听路由导航的回调,可以封装一个路由的 startActivity() 扩展来隐藏回调的参数;
  4. 结合路由框架的实现原理,使用注解来配置登录页面的 path 和需要校验登录的 path,使其可以在各自的组件模块配置登录拦截;

关于我

一个兴趣使然的程序“工匠”  。有代码洁癖,喜欢封装,对封装有一定的个人见解,有不少个人原创的封装思路。GitHub 有分享一些帮助搭建开发框架的开源库,有任何使用上的问题或者需求都可以提 issues 或者加我微信直接反馈。