likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 18

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

前言

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

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

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

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

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 滚动到一定的阈值需要拦截未登录。

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

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

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

class AuthController extends GetxController {
  RxBool _isLoggedIn = false.obs;

  bool get isLoggedIn => _isLoggedIn.value;

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

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

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

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

一、Future 的等待

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

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

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

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的时候做拦截

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;
      }
    }
  }

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

void doLogin() {


    ... //调用接口完成

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

    LoginInterceptThreadManager.get().loginFinished();

    Get.back();
  }

二、新线程 Isolate 的通知

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

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 之间的消息传递,从而实现了异步操作的管理和通知。

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

  void gotoProfilePage() {

    LoginInterceptThreadManager2.get().checkLogin(() {

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

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

登录页面的处理:

void doLogin() {


    ... //调用接口完成

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

    LoginInterceptThreadManager2.loginFinished();

    Get.back();
  }

三、拦截器模式

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

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

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

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

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

abstract class Interceptor {
  void intercept(LoginInterceptChain chain);
}
abstract class BaseLoginInterceptImpl implements Interceptor {

  LoginInterceptChain? mChain;

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

}

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

class LoginInterceptor extends BaseLoginInterceptImpl {
  @override
  void intercept(LoginInterceptChain chain) {
    super.intercept(chain);

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

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

  final void Function() action;

  LoginNextInterceptor(this.action);

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

    chain.process();
  }
}

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

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);
  }


}

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

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

登录的处理:

void doLogin() {

    ... //调用接口完成

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

    LoginInterceptChain().loginFinished();

    Get.back();
  }

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

总结

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

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

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

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

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

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

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

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

Ok,这一期就此完结。

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