4

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

Harshit
  • 95
  • 2
  • 8

2 Answers2

2

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;
},
saqib shafin
  • 98
  • 1
  • 8
  • 1
    Thank you for your answer. btw I did already find it earlier. But, hoping it might help someone else... – Harshit Jul 24 '22 at 05:47
  • You are welcome! I had to spend days trying to solve this issue when I first faced it. How did you solve it? – saqib shafin Jul 25 '22 at 18:59
  • actully I directly created issue to flutter github , there I got the answer. https://github.com/flutter/flutter/issues/104441 – Harshit Jul 26 '22 at 08:52
1

I think you misunderstood the redirect parameter of GoRouter.
This attribute is listening to every changement of route, and was made to ease the redirect on certain circonstances (for example, the user is disconnected, so you redirect him toward the /login page).

The official documentation explains it very well.

Removing this parameter fixes your problem.

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,
      debugLogDiagnostics: true,
    );
  }

and instead of using this method to create your navigation, your should use GoRouter.of(context).go('/profile') as explained here

Xerib
  • 21
  • 3
  • Thank you for the answer. But, then how would Router automatically navigate to Login Screen If didn't have redirect? Doesn't I then have to call manually? Because, as user can logout from any widget and my expection was that redirect listen to the Strem changes and based on that it do the navigation. – Harshit May 23 '22 at 20:04
  • Here, in your safePage() method, you were always redirecting to the current page. This is why you had a loop error. You can click on the link I gave you in my answer showcasing how to handle user disconnections and redirect him toward the login screen. – Xerib May 23 '22 at 20:20
  • I read the Doc but, it doesn't seem to answer the question. What I am trying to understand is why `safePage() ` method redirect to current page? I have if Statement as you can see, I don't understand why if statement doesn't work? – Harshit May 24 '22 at 03:34