MaterialApp RangeError into buildScope Exception

117 views Asked by At

I recently upgraded to Flutter to 3.13 from 3.3 and am now receiving these Exceptions back to back upon hot reloads.

I have included both exceptions as they may be linked and am uncertain if they are seperate problems.

How can I find the source of the problem? The exceptions don't land on any of my code, besides my root MaterialApp, which doesn't explain/lead to much.

Please request any additional information that might be useful and I will provide it.

Debug console:

════════ Exception caught by widgets library ═══════════════════════════════════
The following RangeError was thrown building DefaultSelectionStyle:
RangeError (startIndex): Invalid value: Only valid value is 0: 1

The relevant error-causing widget was
MaterialApp
When the exception was thrown, this was the stack
#0      RangeError.checkValueInInterval (dart:core/errors.dart:313:7)
#1      _StringBase.replaceFirst (dart:core-patch/string_patch.dart:591:16)

...

#259    RendererBinding._handlePersistentFrameCallback
binding.dart:360
#260    SchedulerBinding._invokeFrameCallback
binding.dart:1297
#261    SchedulerBinding.handleDrawFrame
binding.dart:1227
#262    SchedulerBinding.scheduleWarmUpFrame.<anonymous closure>
binding.dart:952
#266    _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)
(elided 3 frames from class _Timer and dart:async-patch)
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by widgets library ═══════════════════════════════════
RangeError (startIndex): Invalid value: Only valid value is 0: 1
The relevant error-causing widget was
MaterialApp
main.dart:71
════════════════════════════════════════════════════════════════════════════════

════════ Exception caught by scheduler library ═════════════════════════════════
buildScope missed some dirty elements.
The list of dirty elements at the end of the buildScope call was
    App
    StreamAuthScope
    MaterialApp
        state: _MaterialAppState#1c346
    MyRoot
        dependencies: [GoRouterStateRegistryScope, StreamAuthScope, _ModalScopeStatus]
        state: _MyRootState#4d14d
    MyTabBarScaffold
        state: _MyTabBarScaffoldState#a8f80
    Material
        type: canvas
        dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#20b1a]]
        state: _MaterialState#ee00a
    ...
════════════════════════════════════════════════════════════════════════════════
Restarted application in 1,785ms.

main.dart. Debug console points to child: MaterialApp.router as the offending widget.

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamAuthScope(
      child: MaterialApp.router(
        title: "My App",
        debugShowCheckedModeBanner: false,
        routerConfig: MyRouter().router,
      ),
    );
  }
}

MyRouter():

final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');

class MyRouter {
  static final MyRouter _singleton = MyRouter._internal();

  factory MyRouter() {
    return _singleton;
  }

  MyRouter._internal() {
    // init
    MyHelper().rootNavigatorKey = _rootNavigatorKey;
  }

  static String shortIdKey = 'shortId';
  static String tabRouteNameKey = 'tabRouteName';

  final router = GoRouter(
    navigatorKey: _rootNavigatorKey,
    initialLocation: MyRouter.loadingSplash.path,
    routes: [
      MyRouter.loadingSplash,
      MyRouter.signUp,
      MyRouter.myRoot,
    ],
  );

  static const String loadingSplashRouteName = 'loadingSplash';
  static GoRoute get loadingSplash {
    return GoRoute(
      name: loadingSplashRouteName,
      path: '/',
      builder: (context, state) => LoadingSplash(),
      redirect: (context, state) {
        String? user = _userRedirect(context);
        String? noUser = _noUserRedirect(context);

        String? redirect = user;
        if (noUser != null) {
          redirect = noUser;
        }
        return redirect;
      },
    );
  }

  // REDIRECTS
  //

  static String? _noUserRedirect(BuildContext context) {
    final StreamAuth streamAuth = StreamAuthScope.of(context);
    if (streamAuth.hasReceivedInitialData && !ProfilesManager.hasLoggedInUser) {
      return MyRouter.signUp.path;
    } else {
      return null;
    }
  }

  static String? _userRedirect(BuildContext context) {
    final StreamAuth streamAuth = StreamAuthScope.of(context);
    if (streamAuth.hasReceivedInitialData && ProfilesManager.hasLoggedInUser) {
      return '/${ProfilesManager().currentUserHelper!.profile.shortId}/${MyRouter.tcTabRouteName}}' //MyRouter.tRoot.path;
    } else {
      return null;
    }
  }

  static const String signUpRouteName = 'signUp';
  static GoRoute get signUp {
    return GoRoute(
      name: signUpRouteName,
      path: '/sign-up',
      pageBuilder: (context, state) {
        return CustomTransitionPage(
          key: state.pageKey,
          child: kIsWeb ? MySignUp() : Login(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) =>
              child,
        );
      },
      redirect: (context, state) {
        String? user = _userRedirect(context);

        String? redirect = user;
        return redirect;
      },
    );
  }

  static String tcTabRouteName = 'tc';
  static String twTabRouteName = 'tw';
  static String teTabRouteName = 'te';
  static String tpTabRouteName = 'tp';
  static String tmTabRouteName = 'tm';

  static const String myRootRouteName = 'mr';
  static GoRoute get myRoot {
    return GoRoute(
      name: myRootRouteName,
      path: '/:$shortIdKey/:$tabRouteNameKey',
      pageBuilder: (context, state) {
        String shortId = state.pathParameters[shortIdKey]!;
        String tabRouteName = state.pathParameters[tabRouteNameKey]!;
        return CustomTransitionPage(
          key: state.pageKey,
          child: MyRoot(
            shortId: shortId,
            tabRouteName: tabRouteName,
            tabs: [
              PTTabBarItem(
                title: 'Tab One',
                isSelected: tabRouteName == tcTabRouteName,
                tabRouteName: tcTabRouteName,
                child: TabOneWidget(
                  shortId: shortId,
                ),
              ),
              PTTabBarItem(
                title: 'Tab Two',
                isSelected: tabRouteName == twTabRouteName,
                tabRouteName: twTabRouteName,
                child: TabTwoWidget(
                  shortId: shortId,
                ),
              ),
              PTTabBarItem(
                title: 'Tab Three',
                isSelected: tabRouteName == teTabRouteName,
                tabRouteName: teTabRouteName,
                child: TabThreeWidget(
                  shortId: shortId,
                ),
              ),
              PTTabBarItem(
                title: 'Tab Four',
                isSelected: tabRouteName == tpTabRouteName,
                tabRouteName: tpTabRouteName,
                child: TabFourWidget(),
              ),
            ],
          ),
          transitionsBuilder: (context, animation, secondaryAnimation, child) =>
              child,
        );
      },
      redirect: (context, state) {
        String? noUser = _noUserRedirect(context);

        String? redirect = noUser;
        return redirect;
      },
    );
  }

}

MyTabBarScaffold:

class MyTabBarScaffold extends StatefulWidget {
  final Widget child;
  final List<MyTabBarItem> tabs;
  final MyThemeData? themeData;
  final int selectedIndex;
  final Function(int index) onTabSelected;

  const MyTabBarScaffold({
    required this.child,
    required this.tabs,
    this.themeData,
    this.selectedIndex = 0,
    required this.onTabSelected,
    Key? key,
  }) : super(key: key);

  @override
  State<MyTabBarScaffold> createState() => _MyTabBarScaffoldState();
}

class _MyTabBarScaffoldState extends State<MyTabBarScaffold> {
  void _onItemTapped(int index, BuildContext context) {
    widget.onTabSelected(index);
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: KeyboardVisibilityBuilder(
        builder: (context, isKeyboardVisible) {
          return Stack(
            alignment: Alignment.bottomCenter,
            children: [
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                bottom: isKeyboardVisible
                    ? 0
                    : MyTheme.tabBarHeight(
                        context: context,
                        includeSystemPadding: true,
                      ),
                child: widget.child,
              ),
              Positioned(
                bottom: isKeyboardVisible
                    ? (-MyTheme.tabBarHeight(
                        context: context,
                        includeSystemPadding: true,
                      ))
                    : 0,
                left: 0,
                right: 0,
                child: _buildTabBar(),
              ),
            ],
          );
        },
      ),
    );
  }

  Widget _buildTabBar() {
    return MyTabBar(
      themeData: widget.themeData ?? MyTheme().themeData,
      includeSystemPadding: true,
      tabs: widget.tabs,
      selectedIndex: widget.selectedIndex,
      onIndexChanged: (index) => _onItemTapped(index, context),
    );
  }
}
1

There are 1 answers

0
Josh Kahane On BEST ANSWER

While the exceptions/debug console offered no clear explanation, it is caused by a go_router package update.

go_router 8.0 introduced this breaking change:

Imperatively pushed GoRoute no longer change URL.

The solution, without making URL/routing changes, is to set the following variable at the root of your app:

GoRouter.optionURLReflectsImperativeAPIs = true;