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,这一期就此完结。