遥遥领先?Flutter项目中如何实现拦截登录再执行

Flutter项目中拦截登录的常用做法

前言

最近也是比较忙着在搞 Flutter 新项目了,在我之前用 Android 开发的思路开发Flutter项目的指导下没什么大问题。

直到最近出现这么一个登陆拦截的问题。可能很多人没这方面的需求,不是很了解。两个痛点!一个是如何拦截各种场景与监听的拦截,一个是如何拦截完成之后再次执行的问题。

拦截?简单!这不是用 Flutter 的路由拦截器就能实现的逻辑吗?

类似如果我们使用 GetX 框架来管理的路由,大致如下实现:

scala 复制代码
class AuthMiddleware extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    // 判断用户是否已登录
    bool isLoggedIn = checkIfUserIsLoggedIn(); // 你需要根据实际情况来实现该方法

    if (!isLoggedIn) {
      // 用户未登录,重定向到登录页面
      return RouteSettings(name: '/login');
    }

    // 用户已登录,继续正常跳转
    return null;
  }
}

然后注入到容器中即可使用,通过以上配置,当进行路由跳转时,AuthMiddleware 中的登录拦截逻辑会自动触发。如果用户未登录,则会重定向到指定的登录页面;如果用户已登录,则会正常进行路由跳转。

但是这种通过路由拦截的方式,有很大的局限性,它只能拦截通过 Get.to、Get.off、Get.toNamed 等 GetX 路由方法进行的页面跳转。当你直接使用构造函数创建的页面时,不会触发 GetX 的路由系统,因此 GetMiddleware 无法拦截这种情况。

无法理解?

比如首页的底部5个Tab,只有首页能支持游客模式,而点击其他Tab的时候要拦截未登录。或者 PageView 滚动的时候只有第一个Page是支持游客模式,滑动到其他Page 就需要拦截未登录。再或者在首页的列表 ListView 滚动到一定的阈值需要拦截未登录。

要知道这些方式的拦截可并不走路由的。并且就算是正常的按钮点击跳转到路由,走到路由了,使用路由的拦截方式也只能做到拦截不能支持再执行的效果,还是需要再次点击按钮触发才行。

其中类似的效果如 Rxbool 之类的监听:

scala 复制代码
class AuthController extends GetxController {
  RxBool _isLoggedIn = false.obs;

  bool get isLoggedIn => _isLoggedIn.value;

  set isLoggedIn(bool value) {
    _isLoggedIn.value = value;
  }
}

也是类似的效果,确实是能做到监听,但是无法做到再执行的效果。而我们需要最终实现的效果如下:

那么到底最后有哪些方式能实现这种效果呢?

一、Future 的等待

其实在去年我写过 Android 的登录拦截与转发的几种方式,这里吸取其中几种思路,我们就能实现类似的效果。

类似 Kotlin 协程的做法,我们可以用 Dart 中的 Completer 对象来实现。

Completer 是 Dart 中的一个类,用于处理异步操作的完成通知。Completer 提供了一个 future 属性,该属性是一个 Future 对象,调用者可以监听该 Future 对象来获取异步操作的结果。

scss 复制代码
class LoginInterceptThreadManager {
  static LoginInterceptThreadManager? _threadManager;
  static Completer<bool>? _completer;

  LoginInterceptThreadManager._();

  static LoginInterceptThreadManager get() {
    _threadManager ??= LoginInterceptThreadManager._();
    return _threadManager!;
  }

  void checkLogin(void Function() nextRunnable) {
    if (LoginInterceptChain.isLogin()) {
      // 已经登录
      nextRunnable();
      return;
    }

    // 如果没有登录-先去登录页面
    LoginPage.startInstance();

    // 等待登录完成
    _completer = Completer<bool>();
    _completer?.future.then((result) {
      if (LoginInterceptChain.isLogin()) {
        // 已经登录
        nextRunnable();

        _completer = null;
      }
    });
  }

  // 设置登录完成
  void loginFinished() {
    _completer?.complete(true);
  }

}

在 checkLogin 方法中,首先创建了一个 Completer 对象 _completer,然后将其 future 属性(即 _completer?.future)传递给 then 方法,以添加一个监听器。当 _completer 的 complete 方法被调用时,该监听器会被触发,进而执行相应的逻辑。

在 loginFinished 方法中,调用 _completer?.complete(true) 来完成异步操作,并将结果设置为 true。这将触发之前通过 then 方法添加的监听器,并执行后续的操作。

使用的时候:

比如切换到其他Tab的时候做拦截

arduino 复制代码
void _selectPositionEvent(int position) {
    if (position != state.tabIndex) {
      if (position > 0) {
        LoginInterceptThreadManager.get().checkLogin(() {
          //监听事件,正常切换Tab页面
          _pageController.jumpToPage(position);

          setState(() {
            //状态更新
            state.tabIndex = position;
          });
        });
      }
    } else {
      //如果是重复点击
      switch (position) {
        case 0:
          Log.d('重复点击索引0');
          break;
        case 1:
          Log.d('重复点击索引1');
          break;
        case 2:
          Log.d('重复点击索引2');
          break;
        case 3:
          Log.d('重复点击索引3');
          break;
        case 4:
          Log.d('重复点击索引4');
          if (isPageLoadedList[position]) {
            Get.find<MeController>().forceRefreshPage();
          }
          break;
      }
    }
  }

在登录完成之后需要处理登录完成的逻辑。

scss 复制代码
void doLogin() {


    ... //调用接口完成

    StorageService.getInstance().setString(AppConstant.storageToken,"abc123");

    LoginInterceptThreadManager.get().loginFinished();

    Get.back();
  }

二、新线程 Isolate 的通知

除了上述的方法,我们还能模仿实现 Java 的线程等待唤醒的逻辑实现类似的功能,我们可以用 Dart 开启新的线程 Isolate。然后通过 SendPort 与 receivePort 等结合起来实现。

scss 复制代码
class LoginInterceptThreadManager2 {
  static LoginInterceptThreadManager2? _threadManager;

  late StreamSubscription _subscription;
  static Isolate? _isolate;
  static SendPort? _sendPort;

  LoginInterceptThreadManager2._();

  static LoginInterceptThreadManager2 get() {
    _threadManager ??= LoginInterceptThreadManager2._();
    return _threadManager!;
  }

  void checkLogin(void Function() nextRunnable) {
    if (LoginInterceptChain.isLogin()) {
      // 已经登录
      nextRunnable();
      return;
    }

    // 如果没有登录-先去登录页面
    LoginPage.startInstance();


    // 启动 Isolate 并等待登录完成
    final receivePort = ReceivePort();
    Isolate.spawn(_isolateEntry, receivePort.sendPort).then((isolate) {
      _isolate = isolate;
      _sendPort = receivePort.sendPort;
    });

    _subscription = receivePort.listen((result) {

      if (LoginInterceptChain.isLogin()) {
        // 已经登录
        _subscription.cancel();
        _isolate?.kill(priority: Isolate.immediate);

        nextRunnable();
      }
    });
  }

  static void _isolateEntry(SendPort sendPort) {
    final receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);

    receivePort.listen((_) {
      if (LoginInterceptChain.isLogin()) {
        sendPort.send(true);
      }
    });
  }

  // 设置登录完成
  static void loginFinished() {
    _sendPort?.send(true);
  }

}

整体流程是通过启动一个 Isolate 来执行异步操作,并使用 SendPort 和 ReceivePort 进行主 Isolate 与子 Isolate 之间的消息传递,从而实现了异步操作的管理和通知。

具体的使用与上述代码类似:

scss 复制代码
  void gotoProfilePage() {

    LoginInterceptThreadManager2.get().checkLogin(() {

      SmartDialog.showToast('已经登录了-去个人中心吧2');

      StorageService.getInstance().remove(AppConstant.storageToken);
    });
  }

登录页面的处理:

scss 复制代码
void doLogin() {


    ... //调用接口完成

    StorageService.getInstance().setString(AppConstant.storageToken,"abc123");

    LoginInterceptThreadManager2.loginFinished();

    Get.back();
  }

三、拦截器模式

没想到吧,Android 中常用的拦截器模式一样能在 Dart/Flutter 中使用。

因为它只是一种设计思想,不限制于语言与平台。在 Android 中的使用方式换成 Dart 语法实现之后在 Flutter 中一样好用。

我们可以定义两个固定的拦截器,一个是登录拦截的拦截器,用于拦截是否登录并且跳转到登录页面。完成登录之后我们就放行拦截器,走到下一个拦截器是具体的执行操作拦截器。从而实现登录的拦截与再执行效果。

由于内部的判断都是基于是否登录来判断的,所以我们实现一个简单的拦截器链即可,可以不通过值传递的方式来实现。

先定义拦截器和基类的实现:

csharp 复制代码
abstract class Interceptor {
  void intercept(LoginInterceptChain chain);
}
typescript 复制代码
abstract class BaseLoginInterceptImpl implements Interceptor {

  LoginInterceptChain? mChain;

  @override
  void intercept(LoginInterceptChain chain) {
    mChain = chain;
  }

}

然后我们定义两个固定的拦截器一个是登录拦截器,一个是继续执行拦截器:

scala 复制代码
class LoginInterceptor extends BaseLoginInterceptImpl {
  @override
  void intercept(LoginInterceptChain chain) {
    super.intercept(chain);

    if (LoginInterceptChain.isLogin()) {
      // 如果已经登录 -> 放行,转交给下一个拦截器
      chain.process();
    } else {
      // 如果未登录 -> 去登录页面
      LoginPage.startInstance();
    }
  }

  void loginFinished() {
    // 如果登录完成,调用方法放行到下一个拦截器
    mChain?.process();
  }
}
scss 复制代码
class LoginNextInterceptor implements Interceptor {

  final void Function() action;

  LoginNextInterceptor(this.action);

  @override
  void intercept(LoginInterceptChain chain) {
    if (LoginInterceptChain.isLogin()) {
      // 如果已经登录执行当前的任务
      action();
    }

    chain.process();
  }
}

最后我们通过一个管理类来统一处理:

scss 复制代码
class LoginInterceptChain {
  int index = 0;
  List<Interceptor> _interceptors = [];
  late LoginInterceptor _loginIntercept;

  // 私有构造函数
  LoginInterceptChain._() {
    // 默认初始化Login的拦截器
    _loginIntercept = LoginInterceptor();
  }

  // 单例实例
  static final LoginInterceptChain _instance = LoginInterceptChain._();

  // 获取单例实例
  factory LoginInterceptChain() {
    return _instance;
  }

  // 执行拦截器
  void process() {
    if (_interceptors.isEmpty) return;

    if (index < _interceptors.length) {
      Interceptor interceptor = _interceptors[index];
      index++;
      interceptor.intercept(this);

    } else if (index == _interceptors.length) {
      clearAllInterceptors();
    }

  }

  // 添加默认的再执行拦截器
  LoginInterceptChain addInterceptor(Interceptor interceptor) {
    // 默认添加Login判断的拦截器
    if (!_interceptors.contains(_loginIntercept)) {
      _interceptors.add(_loginIntercept);
    }

    if (!_interceptors.contains(interceptor)) {
      _interceptors.add(interceptor);
    }

    return this;
  }

  // 放行登录判断拦截器
  void loginFinished() {
    if (_interceptors.contains(_loginIntercept) && _interceptors.length > 1) {
      _loginIntercept.loginFinished();
    }
  }

  // 清除全部的拦截器
  void clearAllInterceptors() {
    index = 0;
    _interceptors.clear();
  }

  // 是否已经登录
  static bool isLogin() {
    String token = StorageService.getInstance().getString(AppConstant.storageToken);
    return !TextUtil.isEmpty(token);
  }


}

使用起来差别其实也不大:

scss 复制代码
  void gotoProfilePage() {
   
     LoginInterceptChain chain = LoginInterceptChain();
    
     chain.addInterceptor(LoginNextInterceptor((){
    
       SmartDialog.showToast('已经登录了-去个人中心吧');
    
       StorageService.getInstance().remove(AppConstant.storageToken);
    
     })).process();
  }

登录的处理:

scss 复制代码
void doLogin() {

    ... //调用接口完成

    StorageService.getInstance().setString(AppConstant.storageToken,"abc123");

    LoginInterceptChain().loginFinished();

    Get.back();
  }

三者实现最终的效果是一致的,这里就不反复的贴图。

总结

通过上面的几种方式(不限于这些方式还有很多其他方式)我们就可以脱离框架层面,不需要定义路由拦截或者每一个地方手写判断。或者一些拦截不走路由等等各种情况的统一管理。

只需要实现一处代码,可以达到统一拦截的效果。

以上的这几种方式总的分两种思路,一种通过线程,Future等方法的通知与回调处理,一种是通过拦截器的模式来处理。你 pick 哪一种。

iOS同事觉得拦截器的方式也太妙了,哎,属实是少见多怪了,Android开发也太卷了其中一些开发设计思路感觉遥遥领先(点个题开个玩笑)。拿出一点用在 Flutte 中受用无穷。😂

所以我们后面还是用选用的 Completer 的方式,感觉也蛮轻量挺好用的。😅

代码比较简单都已经在文中贴出。

如果本文的讲解有什么错漏的地方,希望同学们一定要指出哦。有疑问也可以评论区交流学习进步,谢谢!

当然如果觉得本文还不错对你有些帮助的话,还请点赞支持一下哦,你的支持是我最大的动力啦!

Ok,这一期就此完结。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui