Reactive redirection using go_router and flutter_bloc with Auth and UnAuth Flow

huangapple go评论61阅读模式
英文:

Reactive redirection using go_router and flutter_bloc with Auth and UnAuth Flow

问题

使用flutter_bloc和go_router根据身份验证更改实现导航。

在开始时,如果用户已经经过身份验证,应将其重定向到HomeScreen,并能够在身份验证流程中导航...(HomeScreen、TestScreen1、TestScreen2)。

如果用户在任何时候未经身份验证,应将其重定向到LoginScreen,如果身份未知,则转至Splash Screen。

我已经查阅了许多stackoverflow问题和github问题,但没有提供如何实现这一目标的最佳实践。

问题:

- 无法在身份验证流程中导航,HomeScreen -> Test1 || HomeScreen -> Test2

会出现以下断言:

试图从小部件树外部侦听提供程序公开的值。

这可能是由于事件处理程序(例如按钮的onPressed)调用Provider.of时没有传递listen: false引起的。

要修复,请写:
Provider.of<$T>(context, listen: false);

这是不受支持的,因为当小部件树不关心该值时,可能会毫无意义地重建与事件处理程序关联的小部件。

使用的上下文是:$context


- 在使用flutter_web时,如果我在主屏幕(/auth/home_screen)上,并手动将URL更改为/login_screen,它会抛出异常:

======== 基础库捕获的异常 ====================================================
在为GoRouteInformationProvider分发通知时发生以下断言:
试图从小部件树外部侦听提供程序公开的值。

这可能是由于事件处理程序(例如按钮的onPressed)调用Provider.of时没有传递listen: false引起的。


将`context.read`更改为`watch`不是我真正想要的,因为那样我就无法从bloc中监听更改。

到目前为止,我尝试过的是:

我的AppRouter:

class AppRouter {
GoRouter get router => _goRouter;

late final GoRouter _goRouter = GoRouter(
routes: [
GoRoute(
path: '/',
name: 'SPLASH',
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: '/auth',
name: 'AUTH',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'home_screen',
name: 'HOME',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: 'test_screen_1',
name: 'TEST_SCREEN_1',
builder: (context, state) => TESTSCREEN1(),
),
GoRoute(
path: 'test_screen_2',
name: 'TEST_SCREEN_2',
builder: (context, state) => TESTSCREEN2(),
),
],
),
GoRoute(
path: '/login_screen',
name: 'LOGIN',
builder: (context, state) => LoginScreen(),
),
],
redirect: (context, state) {
final authState = context.watch().state;
final loginLocation = state.namedLocation('LOGIN');
final authFlow = state.namedLocation('AUTH');
final splashLocation = state.namedLocation('SPLASH');

  if (authState is AuthStateAuthenticated) {
    return authFlow;
  } else if (authState is AuthStateUnAuthenticated) {
    return loginLocation;
  } else {
    return splashLocation;
  }
},

);
}


应用程序:

class App extends StatelessWidget {

AppRouter appRouter = AppRouter();
@override
Widget build(BuildContext context) {
return RepositoryProvider(
create: (context) => AuthService(),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (context) => AuthBloc(authService: RepositoryProvider.of(context))),
],
child: MaterialApp.router(
routerDelegate: appRouter.router.routerDelegate,
routeInformationParser: appRouter.router.routeInformationParser,
routeInformationProvider: appRouter.router.routeInformationProvider,
),
),
);
}
}

英文:

Implementing navigation based on auth changes using flutter_bloc and go_router.

What i need to do is at start if User is authenticated it should be redirected to HomeScreen with the ability to navigate through the auth flow...(HomeScreen,TestScreen1,TestScreen2)

If User is unAunthenticated at any point it should be redirected to LoginScreen and if Unknown then to Splash Screen.

I have gone through many stackoverflow questions and github issues but none of them provides a best pratice of how could we acheive this.

Problems:

  • Cannot navigate it in the auth flow, HomeScreen -> Test1 || HomeScreen -> Test2

Gives the following assetion:

Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button&#39;s onPressed) that called
Provider.of without passing `listen: false`.

To fix, write:
Provider.of&lt;$T&gt;(context, listen: false);

It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn&#39;t care about the value.

The context used was: $context
  • When working with flutter_web, if I'm on Home Screen (/auth/home_screen) and change the URL manually to /login_screen it throws an exception:
======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for GoRouteInformationProvider:
Tried to listen to a value exposed with provider, from outside of the widget tree.

This is likely caused by an event handler (like a button&#39;s onPressed) that called
Provider.of without passing `listen: false`.

Changing to context.read instead of watch is not what i'm really into, because then i cannot listen to changes from the bloc.

What i have tried soo far is:

My AppRouter:

class AppRouter {
  GoRouter get router =&gt; _goRouter;

  late final GoRouter _goRouter = GoRouter(
    routes: &lt;GoRoute&gt;[
      GoRoute(
        path: &#39;/&#39;,
        name: &#39;SPLASH&#39;,
        builder: (context, state) =&gt; const SplashScreen(),
      ),
      GoRoute(
        path: &#39;/auth&#39;,
        name: &#39;AUTH&#39;,
        builder: (context, state) =&gt; HomeScreen(),
        routes: &lt;GoRoute&gt;[
          GoRoute(
            path: &#39;home_screen&#39;,
            name: &#39;HOME&#39;,
            builder: (context, state) =&gt; HomeScreen(),
          ),
          GoRoute(
            path: &#39;test_screen_1&#39;,
            name: &#39;TEST_SCREEN_1&#39;,
            builder: (context, state) =&gt; TESTSCREEN1(),
          ),
          GoRoute(
            path: &#39;test_screen_2&#39;,
            name: &#39;TEST_SCREEN_2&#39;,
            builder: (context, state) =&gt; TESTSCREEN2(),
          ),
        ],
      ),
      GoRoute(
        path: &#39;/login_screen&#39;,
        name: &#39;LOGIN&#39;,
        builder: (context, state) =&gt; LoginScreen(),
      ),
    ],
    redirect: (context, state) {
      final authState = context.watch&lt;AuthBloc&gt;().state;
      final loginLocation = state.namedLocation(&#39;LOGIN&#39;);
      final authFlow = state.namedLocation(&#39;AUTH&#39;);
      final splashLocation = state.namedLocation(&#39;SPLASH&#39;);

      if (authState is AuthStateAuthenticated) {
        return authFlow;
      } else if (authState is AuthStateUnAuthenticated) {
        return loginLocation;
      } else {
        return splashLocation;
      }
    },
  );
}

App:

class App extends StatelessWidget {

  AppRouter appRouter = AppRouter();
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider&lt;AuthService&gt;(
      create: (context) =&gt; AuthService(),
      child: MultiBlocProvider(
        providers: [
          BlocProvider&lt;AuthBloc&gt;(create: (context) =&gt; AuthBloc(authService:    RepositoryProvider.of&lt;AuthService&gt;(context))),
                  ],
        child: MaterialApp.router(
          routerDelegate: appRouter.router.routerDelegate,
          routeInformationParser: appRouter.router.routeInformationParser,
          routeInformationProvider: appRouter.router.routeInformationProvider,
        ),
      ),
    );
  }
}

答案1

得分: 1

I'm using flutter_bloc and get_it for dependency injection, and was also struggling with this topic (mainly due to lack of proper documentation).

Eventually I based my solution on this example from the official docs.

Step 1: in your auth_bloc.dart file add AuthStreamScope and AuthStream classes.
Note: in the code below, replace getIt.get&lt;AuthBloc&gt;() with your auth bloc instance:

class AuthStreamScope extends InheritedNotifier&lt;AuthStream&gt; {
  AuthStreamScope({super.key, required super.child}) : super(notifier: AuthStream(getIt.get&lt;AuthBloc&gt;().stream));
  static AuthStream of(BuildContext ctx) =&gt; ctx.dependOnInheritedWidgetOfExactType&lt;AuthStreamScope&gt;()!.notifier!;
}

class AuthStream extends ChangeNotifier {
  late final StreamSubscription&lt;dynamic&gt; _subscription;

  AuthStream(Stream&lt;dynamic&gt; stream) {
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen((_) =&gt; notifyListeners());
  }

  bool isSignedIn() =&gt; getIt.get&lt;AuthBloc&gt;().state.user != null;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

Step 2: wrap your app with the AuthStreamScope:

// from with your app&#39;s build method:
return AuthStreamScope(
    child: MaterialApp.router(
      theme: theme,
      darkTheme: darkTheme,
      routerConfig: _routerInstance, // your GoRouter
    ),
  ),
);

Step 3: in your GoRouter initialization, listen to auth changes as follows.

GoRouter(
  redirect: (BuildContext context, GoRouterState state) async {
    // calling `of` method creates a dependency of AuthStreamScope. It will make go_router to reparse
    // current route if AuthStream has new sign-in information.
    final bool loggedIn = AuthStreamScope.of(context).isSignedIn();

    // implement your redirection logic here

    return null; // no need to redirect
  },

  // routes, etc.
)

So essentially GoRouter is listening to any AuthBloc stream updates and redirects accordingly. Auth in particular, unlike other blocs can be defined as (lazy) singletons, so GetIt comes very handy as shown in the code above.

I hope it helps.

英文:

I'm using flutter_bloc and get_it for dependency injection, and was also struggling with this topic (mainly due to lack of proper documentation).

Eventually I based my solution on this example from the official docs.

Step 1: in your auth_bloc.dart file add AuthStreamScope and AuthStream classes.<br/>Note: in the code below, replace getIt.get&lt;AuthBloc&gt;() with your auth bloc instance:

class AuthStreamScope extends InheritedNotifier&lt;AuthStream&gt; {
  AuthStreamScope({super.key, required super.child}) : super(notifier: AuthStream(getIt.get&lt;AuthBloc&gt;().stream));
  static AuthStream of(BuildContext ctx) =&gt; ctx.dependOnInheritedWidgetOfExactType&lt;AuthStreamScope&gt;()!.notifier!;
}

class AuthStream extends ChangeNotifier {
  late final StreamSubscription&lt;dynamic&gt; _subscription;

  AuthStream(Stream&lt;dynamic&gt; stream) {
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen((_) =&gt; notifyListeners());
  }

  bool isSignedIn() =&gt; getIt.get&lt;AuthBloc&gt;().state.user != null;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

Step 2: wrap your app with the AuthStreamScope:

// from with your app&#39;s build method:
return AuthStreamScope(
    child: MaterialApp.router(
      theme: theme,
      darkTheme: darkTheme,
      routerConfig: _routerInstance, // your GoRouter
    ),
  ),
);

Step 3: in your GoRouter initialization, listen to auth changes as follows.

GoRouter(
  redirect: (BuildContext context, GoRouterState state) async {
    // calling `of` method creates a dependency of AuthStreamScope. It will make go_router to reparse
    // current route if AuthStream has new sign-in information.
    final bool loggedIn = AuthStreamScope.of(context).isSignedIn();

    // implement your redirection logic here

    return null; // no need to redirect
  },

  // routes, etc.
)

So essentially GoRouter is listening to any AuthBloc stream updates and redirects accordingly. Auth in particular, unlike other blocs can be defined as (lazy) singletons, so GetIt comes very handy as shown in the code above.

I hope it helps.

huangapple
  • 本文由 发表于 2023年5月13日 10:05:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76240799.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定