flutterdartstreamblocgorouter

(EXCEPTION CAUGHT BY GOROUTER) Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected:


I am trying to use GoRoute, Stream, and Bloc. So, I am implementing Auth Process. So, if the user is not log-in they can't access any page. And, `Stream allows to constantly listen to AuthStatus (Bloc State). But, so how I am not sure, why but I am getting this error

Exception: 'package:go_router/src/go_router_delegate.dart':
Failed assertion: line 299 pos 13: '!redirects.contains(redir)':
redirect loop detected:

I tried to create a dummy app to understand what is exactly going wrong. But, I wasn't able to fig-out what is the reason. Just below I am attaching the code it's just ~200 lines anyone can copy and paste it and then run the command flutter run and an app error appears in the screen.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

enum Pages {
  home,
  profile,
  settings,
}

class FooStream {
  final StreamController<Pages> _controller;
  FooStream() : _controller = StreamController.broadcast();

  Stream<Pages> get stream {
    final listOfPages = [Pages.home, Pages.profile, Pages.settings];
    //  every 3 seconds loop over enum and emit a new value to the stream

    Timer.periodic(const Duration(seconds: 3), (timer) {
      final index = timer.tick ~/ 3;
      if (index >= listOfPages.length) {
        timer.cancel();
      } else {
        _controller.add(listOfPages[index]);
      }
    });

    return _controller.stream;
  }

  void update(Pages page) {
    _controller.add(page);
  }

  void close() {
    _controller.close();
  }

  void dispose() {
    _controller.close();
  }
}

class FooNotifier extends ChangeNotifier {
  final ValueNotifier<Pages> _page = ValueNotifier(Pages.home);

  ValueNotifier<Pages> get page => _page;

  // listen to the stream and whenever it emits a new value, update the page
  final FooStream stream;
  FooNotifier(this.stream) {
    stream.stream.listen((Pages page) {
      _page.value = page;
      notifyListeners();
    });
  }

  void close() {
    notifyListeners();
  }

  @override
  void dispose() {
    notifyListeners();
    super.dispose();
  }
}

// my home page
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
          child: Text(
        "Home",
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      )),
    );
  }
}

// profile page
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
          child: Text(
        "Profile",
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      )),
    );
  }
}

// setting page
class SettingPage extends StatelessWidget {
  const SettingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
          child: Text(
        "Setting",
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      )),
    );
  }
}

class FooRoute {
  final FooNotifier notifier;
  FooRoute(this.notifier);
  GoRouter routeTo() {
    return GoRouter(
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const MyHomePage(),
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfilePage(),
        ),
        GoRoute(
          path: '/setting',
          builder: (context, state) => const SettingPage(),
        ),
      ],
      refreshListenable: notifier,
      redirect: safePage,
      debugLogDiagnostics: true,
    );
  }

  // on page change, return the new page
  String? safePage(GoRouterState state) {
    final newPage = notifier.page.value;

    // if the new page is the same as the current page, do nothing
    if (newPage == Pages.home) {
      return '/';
    }

    if (newPage == Pages.profile) {
      return '/profile';
    }

    if (newPage == Pages.settings) {
      return '/settings';
    }

    return null;
  }
}

class FooApp extends StatelessWidget {
  FooApp({
    Key? key,
  }) : super(key: key);

  final _router = FooRoute(FooNotifier(FooStream())).routeTo();

  // base the

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<Pages>(
      valueListenable: FooNotifier(FooStream()).page,
      builder: (context, value, child) {
        return MaterialApp.router(
          routerDelegate: _router.routerDelegate,
          routeInformationParser: _router.routeInformationParser,
        );
      },
    );
  }
}

void main(List<String> args) {
  runApp(FooApp());
}

I am also attaching the message which I get in the terminal. Once the above code runs.

Launching lib\main.dart on Windows in debug mode...
package:bug_test/main.dart:1
Connecting to VM Service at ws://127.0.0.1:63905/cNY-SYgW7KE=/ws
[GoRouter] known full paths for routes:
[GoRouter]   => /
[GoRouter]   => /profile
[GoRouter]   => /setting
[GoRouter] setting initial location /
flutter: ══╡ EXCEPTION CAUGHT BY GOROUTER ╞══════════════════════════════════════════════════════════════════
flutter: The following _Exception was thrown Exception during GoRouter navigation:
flutter: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13:
package:go_router/src/go_router_delegate.dart:299
flutter: '!redirects.contains(redir)': redirect loop detected: / => /
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #2      GoRouterDelegate._getLocRouteMatchesWithRedirects.redirected
package:go_router/src/go_router_delegate.dart:299
flutter: #3      GoRouterDelegate._getLocRouteMatchesWithRedirects
package:go_router/src/go_router_delegate.dart:322
flutter: #4      GoRouterDelegate._go
package:go_router/src/go_router_delegate.dart:245
flutter: #5      new GoRouterDelegate
package:go_router/src/go_router_delegate.dart:58
flutter: #6      new GoRouter
package:go_router/src/go_router.dart:46
flutter: #7      FooRoute.routeTo
package:bug_test/main.dart:122
flutter: #8      new FooApp
package:bug_test/main.dart:169
flutter: #9      main
flutter: #10     _runMain.<anonymous closure> (dart:ui/hooks.dart:132:23)
flutter: #11     _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
flutter: (elided 3 frames from class _AssertionError and class _RawReceivePortImpl)
flutter: ════════════════════════════════════════════════════════════════════════════════════════════════════
flutter: Another exception was thrown: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected: / => /
package:go_router/src/go_router_delegate.dart:299
[GoRouter] MaterialApp found
[GoRouter] refreshing /
2
flutter: Another exception was thrown: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected: / => /
package:go_router/src/go_router_delegate.dart:299
[GoRouter] refreshing /
flutter: Another exception was thrown: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected: / => /profile => /profile
package:go_router/src/go_router_delegate.dart:299
[GoRouter] refreshing /
[GoRouter] redirecting to /profile
flutter: Another exception was thrown: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected: / => /profile => /profile
package:go_router/src/go_router_delegate.dart:299
[GoRouter] refreshing /
flutter: Another exception was thrown: Exception: 'package:go_router/src/go_router_delegate.dart': Failed assertion: line 299 pos 13: '!redirects.contains(redir)': redirect loop detected: / => /settings => /settings
package:go_router/src/go_router_delegate.dart:299
[GoRouter] refreshing /
[GoRouter] redirecting to /settings
Application finished.
Exited (sigterm)

I would be really grateful if someone can help me. Thank you

UPDATE

I have raised this issue in Flutter's official GitHub Repo here is the link please do check the below link https://github.com/flutter/flutter/issues/104441


Solution

  • I am posting a short answer. If it helps you in anyway, I will describe completely how I navigated around this issue.

    Long story short:

    The redirect function of go_router for some reason runs twice. For these reason, I have set up my if/else conditions in such a way that it always redirects to my desired destination.

    redirect: (state) {  
          final isLogging = state.location == '/login';
          final isLoggingByPhoneNumber =
              state.location == '/login/loginUsingPhoneNumber';
          final isSigning = state.location == '/signup';         
    
          if (!userAuthDao.isLoggedIn()) { //If user is not logged in.
            return isLogging
                ? null
                : isSigning
                    ? null
                    : isLoggingByPhoneNumber
                        ? null
                        : '/login';
          }
    
          final isLoggedIn = state.location == '/';
          if (userAuthDao.isLoggedIn() && isLogging) return isLoggedIn ? null : '/';
          if (userAuthDao.isLoggedIn() && isSigning) return isLoggedIn ? null : '/';
          if (userAuthDao.isLoggedIn() && isLoggingByPhoneNumber) {
            return isLoggedIn ? null : '/';
          }
    
          return null;  //if none of the conditions are triggered, you will be routed to where ever you were going.
        },
    

    A much more complex routing logic:

    redirect: (state) {
          final isLogging = state.location == '/login';
          final isInitializing = state.location == '/0';
          final isOnboarding = state.location == '/onboarding';
    
          final isGoingProfilePage = state.location == '/profile';
    
          if (!appStateManager.isInitialized) return isInitializing ? null : '/0';
    
          if (appStateManager.isInitialized && !appStateManager.isLoggedIn)
            return isLogging ? null : '/login';
    
          if (appStateManager.isLoggedIn && !appStateManager.isOnboardingComplete)
            return isOnboarding ? null : '/onboarding';
    
          x = appStateManager.getSelectedTab;
          final isGoingHome = state.location == '/home/tab/${x}';
    
          if (appStateManager.isOnboardingComplete &&
              !profileManager.didSelectUser &&
              !groceryManager.isCreatingNewItem &&
              !(groceryManager.selectedIndex != -1))
            return isGoingHome
                ? null
                    : '/home/tab/${x}';    
    
          final isGoingToCreate = state.location == '/home/tab/2/newitem';
    
          if (groceryManager.isCreatingNewItem)
            return isGoingToCreate ? null : '/home/tab/${x}/newitem';
    
          final isViewingExistingItem =
              state.location == '/home/tab/2/item/${groceryManager.selectedIndex}';
    
          if (groceryManager.selectedIndex != -1)
            return isViewingExistingItem
                ? null
                : '/home/tab/2/item/${groceryManager.selectedIndex}';
    
          if (profileManager.didSelectUser)
            return isGoingProfilePage ? null : '/profile';
    
          return null;
    },