GetX 路由栈(Route Stack)与弹框(Dialog)叠加之后导致获取路由错乱的踩坑记录

下面有一段演示代码:

class PageA extends StatelessWidget {
  test() {
    demo1();
    // demo2();
  }

  showDialog1(String text, {Function()? onTap}) async {
    await Get.dialog(
      TestDialog(
        text: text,
        onTapLog: () {
          clickLog("Dialog1--打印");
        },
        onTap: onTap,
      ),
      routeSettings: RouteSettings(
        name: "dialog1",
      ),
    );
  }

  showDialog2() async {
    await Get.dialog(
      TestDialog(
        text: "Dialog2",
        onTapLog: () {
          clickLog("Dialog2--打印");
        },
        onTap: () {},
      ),
      routeSettings: RouteSettings(
        name: "dialog2",
      ),
    );
  }

  //弹框上在显示弹框
  demo1() {
    showDialog1("显示弹框2", onTap: () {
      showDialog2();
    });
  }

  //弹框之后跳转路由
  demo2() {
    showDialog1("跳转到PageB", onTap: () {
      Get.to(PageB());
    });
  }

  clickLog(String title) {
    Log.d("$title---Get.history---${Get.history}");
    Log.d("$title---Get.currentRoute---${Get.currentRoute}");
    Log.d("$title---Get.previousRoute---${Get.previousRoute}");
    // Log.d("$title---Get.currentName---${Get.currentName}");
    // Log.d("$title---Get.previousName---${Get.previousName}");
    // Log.d("$title---Get.currentPage---${Get.currentPage}");
    // Log.d("$title---Get.previousPage---${Get.previousPage}");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      backgroundColor: Colors.grey,
      floatingActionButton: FloatingActionButton(
        onPressed: test,
      ),
      body: Column(
        children: [
          H(100.w),
          Tap(
            onTap: () {
              clickLog('PageA--打印');
            },
            child: Container(
              width: 300,
              height: 40,
              color: Colors.lightBlue,
              child: Center(
                child: Text("打印"),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class TestDialog extends StatelessWidget {
  final String text;
  final void Function()? onTap;
  final void Function()? onTapLog;

  TestDialog({
    super.key,
    required this.text,
    this.onTap,
    this.onTapLog,
  });

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 300,
        height: 300,
        color: Colors.pink,
        child: Column(
          children: [
            Tap(
              onTap: onTapLog,
              child: Container(
                width: 300,
                height: 40,
                color: Colors.lightBlue,
                child: Center(
                  child: Text("打印"),
                ),
              ),
            ),
            Spacer(),
            Tap(
              onTap: onTap,
              child: Container(
                width: 300,
                height: 40,
                color: Colors.lightBlue,
                child: Center(
                  child: Text(text),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PageB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Page B'),
      ),
      body: Column(
        children: [
          Tap(
            onTap: () {
              Log.d("PageB---Get.history---${Get.history}");
              Log.d("PageB---Get.currentRoute---${Get.currentRoute}");
              Log.d("PagB---Get.previousRoute---${Get.previousRoute}");
            },
            child: Container(
              width: 300,
              height: 40,
              color: Colors.lightBlue,
              child: Center(
                child: Text("打印"),
              ),
            ),
          ),
          Spacer(),
          Tap(
            onTap: () {
              Get.to(PageC());
            },
            child: Container(
              width: 300,
              height: 40,
              color: Colors.lightBlue,
              child: Center(
                child: Text("jump Page C"),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class PageC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PageC'),
      ),
      body: Column(
        children: [
          Tap(
            onTap: () {
              Log.d("PageC---Get.history---${Get.history}");
              Log.d("PageC---Get.currentRoute---${Get.currentRoute}");
              Log.d("PageC---Get.previousRoute---${Get.previousRoute}");
            },
            child: Container(
              width: 300,
              height: 40,
              color: Colors.lightBlue,
              child: Center(
                child: Text("打印"),
              ),
            ),
          ),
          Spacer(),
          Container(
            width: 300,
            height: 40,
            color: Colors.lightBlue,
            child: Center(
              child: Text("PageC"),
            ),
          ),
        ],
      ),
    );
  }
}

tip:打印里的前一个是"当前路由",后一个是”上一个路由“

场景1: 显示弹框之后直接返回
页面A打印:/PageA  /Main  
弹框1打印:/PageA  /PageA
返回页面A打印:/PageA  /PageA


场景2: 依次显示两个弹框再依次返回
页面A打印:/PageA  /Main  
弹框1打印:/PageA    /PageA
弹框2打印:/PageA    Dialog1
返回弹框1打印:Dialog1  Dialog1
返回页面A打印:/PageA  /PageA


场景3: 显示弹框之后在跳转一个页面
页面A打印:/PageA  /Main
弹框1打印:/PageA    /PageA
页面B打印:/PageB  Dialog1
返回弹框1打印:Dialog1  Dialog1
返回页面A打印:/PageA  /PageA

场景1~3问题:获取路由错乱
解决方法:自己实现路由监听,扩展get写法获取当前路由和上一个路由


场景4: 显示弹框之后再依次跳转两个页面
页面A打印:/PageA  /Main
弹框1打印:/PageA  /PageA
页面B打印:/PageB  Dialog1
页面C打印:/PageC  /PageB
返回页面B打印:/PageB  /PageB
返回弹框1打印:/PageB  /PageB
返回页面A打印:/PageA  /PageA

场景4问题:当通过弹框按钮跳转两个页面后,依次返回至弹框时,再次点击跳转按钮无效。 因为在返回弹框后,路由状态未正确更新,导致无法再次跳转。打印信息可以看到此时的当前路由显示的是/PageB,然后要跳转的路由也是/PageB,所以跳转是无法生效的。

这是GetX 路由栈(Route Stack)与弹框(Dialog)叠加层管理 之间的一个经典坑点。
我来帮你详细拆解一下成因和解决方案👇

成因:

1、Get.dialog实际上就是开了一个Route

2、我们来看一下GetX的源代码,这里只贴了关键的两段代码:

@override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    final newRoute = _RouteData.ofRoute(route);


    if (newRoute.isBottomSheet || newRoute.isDialog) {
      Get.log("OPEN ${newRoute.name}");
    } else if (newRoute.isGetPageRoute) {
      Get.log("GOING TO ROUTE ${newRoute.name}");
    }

    RouterReportManager.reportCurrentRoute(route);
    _routeSend?.update((value) {
      if (route is PageRoute) {
        value.current = newRoute.name ?? '';
      }
      final previousRouteName = _extractRouteName(previousRoute);
      if (previousRouteName != null) {
        value.previous = previousRouteName;
      }

      value.args = route.settings.arguments;
      value.route = route;
      value.isBack = false;
      value.removed = '';
      value.isBottomSheet =
          newRoute.isBottomSheet ? true : value.isBottomSheet ?? false;
      value.isDialog = newRoute.isDialog ? true : value.isDialog ?? false;
    });

    if (routing != null) {
      routing!(_routeSend);
    }
  }

  @override
  void didPop(Route route, Route? previousRoute) {
    super.didPop(route, previousRoute);
    final currentRoute = _RouteData.ofRoute(route);
    final newRoute = _RouteData.ofRoute(previousRoute);


    if (currentRoute.isBottomSheet || currentRoute.isDialog) {
      Get.log("CLOSE ${currentRoute.name}");
    } else if (currentRoute.isGetPageRoute) {
      Get.log("CLOSE TO ROUTE ${currentRoute.name}");
    }
    if (previousRoute != null) {
      RouterReportManager.reportCurrentRoute(previousRoute);
    }

    _routeSend?.update((value) {
      if (previousRoute is PageRoute) {
        value.current = _extractRouteName(previousRoute) ?? '';
        value.previous = newRoute.name ?? '';
      } else if (value.previous.isNotEmpty) {
        value.current = value.previous;
      }

      value.args = previousRoute?.settings.arguments;
      value.route = previousRoute;
      value.isBack = true;
      value.removed = '';
      value.isBottomSheet = newRoute.isBottomSheet;
      value.isDialog = newRoute.isDialog;
    });

 
    routing?.call(_routeSend);
  }

第一部分是打开新的路由是触发,第二部分是返回路由的时候触发

第一段代码主要影响部分是这一段:

      if (route is PageRoute) {
        value.current = newRoute.name ?? '';
      }
      final previousRouteName = _extractRouteName(previousRoute);
      if (previousRouteName != null) {
        value.previous = previousRouteName;
      }

意思是当打开一个新的页面时,只有当前路由是PageRoute时才更新当前路由的值,前一个路由则是直接获取的previousRoute的route!.settings.name属性。由于Get.dialog实际上就是开了一个Route, 所有当有弹框显示的时候这里获取到的currentRoute,previousRoute都是同一个。

第二段代码主要影响部分是这一段:

      if (previousRoute is PageRoute) {
        value.current = _extractRouteName(previousRoute) ?? '';
        value.previous = newRoute.name ?? '';
      } else if (value.previous.isNotEmpty) {
        value.current = value.previous;
      }

意思是当返回路由的时候,当上个路由是PageRoute时把currentRoute路由设置为上一个路由的名称,value.previous不为空的时候就直接设置为value.previous。

1、当有弹框弹出的情况下获取当前路由(Get.currentRoute)以及上个路由(Get.previousRoute)结果错误

解决方案:自己监听路由。

//历史路由栈记录
class HistoryRouteObserver extends RouteObserver<PageRoute> {
  List<Route<dynamic>> history = <Route<dynamic>>[];

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    super.didPop(route, previousRoute);
    history.remove(route);
    //调用Navigator.of(context).pop() 出栈时回调
  }

  @override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    history.add(route);
    //调用Navigator.of(context).push(Route()) 进栈时回调
  }

  @override
  void didRemove(Route route, Route? previousRoute) {
    super.didRemove(route, previousRoute);
    history.remove(route);
    //调用Navigator.of(context).removeRoute(Route()) 移除某个路由回调
  }

  @override
  void didReplace({Route? newRoute, Route? oldRoute}) {
    super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
    if (oldRoute != null) {
      history.remove(oldRoute);
    }
    if (newRoute != null) {
      history.add(newRoute);
    }
    //调用Navigator.of(context).replace( oldRoute:Route("old"),newRoute:Route("new")) 替换路由时回调
  }
}

在main.dart里使用自定义的路由监听方法

  HistoryRouteObserver routeObserver = HistoryRouteObserver();
  GetMaterialApp(
    navigatorObservers: [
      
      routeObserver,
      
    ],
    ....省略
  );

///对get拓展
extension GetExtension on GetInterface {
  ///路由历史List,包含dialog
  List<Route<dynamic>> get history => routeObserver.history;

  ///获取当前的路由名,包含dialog
  String? get currentName => history.last.settings.name;

  ///获取上一个的路由名,包含dialog
  String? get previousName {
    if (history.length >= 2) {
      return history[history.length - 2].settings.name;
    }
    return null;
  }

  ///获取当前页面名称,不包含dialog
  ///为了修复Get.currentRoute的bug,打开两个dialog,然后关闭一个dialog,这时Get.currentRoute的值为dialog
  String? get currentPage {
    for (int i = history.length - 1; i >= 0; i--) {
      var route = history[i];
      if (route is PageRoute) {
        return route.settings.name;
      }
    }
    return null;
  }

  ///获取上一个页面的名称,不包含dialog
  String? get previousPage {
    // List<Route> filtered = history.where((element) => element is PageRoute).toList();

    List<PageRoute> filtered = history.whereType<PageRoute>().toList();

    if (filtered.length >= 2) {
      return filtered[filtered.length - 2].settings.name;
    }
    return null;
  }

  ///是否已打开该页面
  bool containName(String name) {
    return getRouteByName(name) != null;
  }

  ///通过name获取route,从栈顶开始查找
  Route? getRouteByName(String name) {
    var index =
        history.lastIndexWhere((element) => element.settings.name == name);
    if (index != -1) {
      return history[index];
    }
    return null;
  }

  ///通过name获取route
  List<Route> getRoutesByName(String name) {
    return history.where((element) => element.settings.name == name).toList();
  }

  //获取所有路由名称
  List<String> getAllRouteNameList() {
    List<String> routeNames = [];
    for (var route in Get.routeTree.routes) {
      routeNames.add(route.name);
    }
    return routeNames;
  }

  ///移除指定的页面,一次
  void removeName(String name) {
    var route = getRouteByName(name);
    if (route != null) {
      if (history.last == route) {
        //移除当前页面,直接返回
        Get.back();
      } else {
        Get.removeRoute(route);
      }
    }
  }

  ///移除所有指定的页面
  void removeAllName(String name) {
    var routes = getRoutesByName(name);
    for (var o in routes) {
      Get.removeRoute(o);
    }
  }
}

开放演示案例里的注释部分的打印会发现,获取路由正常。

后续获取当前路由,上一路由可以使用一下四个扩展方法。

Get.currentName

Get.previousName

Get.currentPage

Get.previousPage

2、当从弹框跳转至两个页面后依次返回,再次点击弹框跳转失效。

原因分析:弹框获取的当前路由与目标路由相同,导致跳转逻辑被拦截。  

解决方法:

1、在跳转页面的时候先关闭弹框  推荐
 Get.back(); // 关闭弹框
 Future.delayed(const Duration(milliseconds: 100), () {
    Get.to(() => BPage());
  });

2、使用原始方法跳转、(虽然能够解决跳转无反应的问题,但是返回弹框后通过Get.currentRoute、Get.previousRoute获取路由依旧有问题)

  Get.key.currentState?.push(MaterialPageRoute(builder: (_) => PageB(),settings:RouteSettings(
    name: "/PageB",
  )));
  Navigator.of(Get.overlayContext!).push(MaterialPageRoute(builder: (_) => PageB(),settings:RouteSettings(
    name: "/PageB",
  )));

  Get.key.currentState?.pushNamed("/PageB");
  Navigator.of(Get.overlayContext!).pushNamed("/PageB");

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值