使用AutoRoute在Flutter中进行声明性导航

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

Declarative Navigation using AutoRoute in Flutter

问题

我正在尝试在我的Flutter应用中使用AutoRoute包实现声明式导航。我在我的AppRouter类中定义了一个复杂的导航结构,并且我正在使用AuthCubit来管理用户的身份验证状态。我希望根据用户的身份验证状态实现声明式导航,使用AutoRoute

auth_cubit.dart:

part 'auth_state.dart';

part 'auth_cubit.freezed.dart';

@injectable
class AuthCubit extends Cubit<AuthState> {
  final AuthStateRepository _authStateRepository;
  StreamSubscription<bool>? _authSubscription;

  AuthCubit(this._authStateRepository) : super(const AuthState.isLoggedIn()) {
    _init();
  }

  void _init() {
    _authSubscription =
        _authStateRepository.isUserLoggedIn.listen((isLoggedIn) {
      if (isLoggedIn) {
        emit(const AuthState.isLoggedIn());
      } else {
        emit(const AuthState.isLoggedOut());
      }
    });
  }

  @override
  Future<void> close() {
    _authSubscription?.cancel();
    return super.close();
  }
}

auth_state.dart:

part of 'auth_cubit.dart';

@freezed
class AuthState with _$AuthState {
  const factory AuthState.isLoggedIn() = _IsLoggedIn;
  const factory AuthState.isLoggedOut() = _IsLoggedOut;
}

auth_state_repository.dart:

@lazySingleton
class AuthStateRepository {
  final BehaviorSubject<bool> _isUserLoggedIn = BehaviorSubject<bool>();

  void setUserLoggedIn(bool isLoggedIn) {
    _isUserLoggedIn.add(isLoggedIn);
  }

  Stream<bool> get isUserLoggedIn => _isUserLoggedIn.stream;
}

接下来,您希望根据用户的身份验证状态进行条件导航。例如,如果用户已登录,则应将他们导航到DashboardRoute页面。如果他们未登录,则应将他们导航到LoginRoute页面。

要实现这一点,您可以在AppRouter中使用AutoRouteonGenerate回调来进行条件导航。以下是一个示例:

final appRouter = AppRouter(
  onGenerate: (settings) {
    if (context.read<AuthCubit>().state is AuthState.isLoggedIn) {
      // 用户已登录,导航到DashboardRoute
      return DashboardRoute();
    } else {
      // 用户未登录,导航到LoginRoute
      return LoginRoute();
    }
  },
  // 其他路由配置...
);

这样,每当尝试导航到一个新的路由时,onGenerate回调将根据用户的身份验证状态决定要导航到的路由。这是一种基于声明的导航方式,根据用户状态自动选择正确的目标路由。

希望这可以帮助您实现声明式导航并根据用户的身份验证状态导航到正确的页面。如果您需要更多帮助或有其他问题,请随时提出。

英文:

I'm trying to implement declarative navigation in my Flutter app using the AutoRoute package. I have a complex navigation structure defined in my AppRouter class, and I'm using the AuthCubit for managing user authentication status. I would like to achieve declarative navigation based on the user's authentication status using AutoRoute.

auth_cubit.dart:

part &#39;auth_state.dart&#39;;

part &#39;auth_cubit.freezed.dart&#39;;

@injectable
class AuthCubit extends Cubit&lt;AuthState&gt; {
  final AuthStateRepository _authStateRepository;
  StreamSubscription&lt;bool&gt;? _authSubscription;

  AuthCubit(this._authStateRepository) : super(const AuthState.isLoggedIn()) {
    _init();
  }

  void _init() {
    _authSubscription =
        _authStateRepository.isUserLoggedIn.listen((isLoggedIn) {
      if (isLoggedIn) {
        emit(const AuthState.isLoggedIn());
      } else {
        emit(const AuthState.isLoggedOut());
      }
    });
  }

  @override
  Future&lt;void&gt; close() {
    _authSubscription?.cancel();
    return super.close();
  }
}

auth_state.dart:

part of &#39;auth_cubit.dart&#39;;

@freezed
class AuthState with _$AuthState {
  const factory AuthState.isLoggedIn() = _IsLoggedIn;
  const factory AuthState.isLoggedOut() = _IsLoggedOut;
}

auth_state_repository.dart:

  @lazySingleton
  class AuthStateRepository {

    final BehaviorSubject&lt;bool&gt; _isUserLoggedIn = BehaviorSubject&lt;bool&gt;();

    void setUserLoggedIn(bool isLoggedIn) {
      _isUserLoggedIn.add(isLoggedIn);
    }
    Stream&lt;bool&gt; get isUserLoggedIn =&gt; _isUserLoggedIn.stream;
  }

token_repository.dart:

@singleton
class TokenRepository {
  TokenRepository(this._authStateRepository) {
    _init();
  }

  void _init() {
    _checkForAccessToken();
    _checkForRefreshToken();
  }

  final AuthStateRepository _authStateRepository;

  String? _accessToken;
  String? _refreshToken;

  //get refresh token
  FutureOr&lt;String?&gt; get refreshToken async {
    if (_refreshToken != null) {
      return _refreshToken;
    }
    return readRefreshToken().then((token) {
      _refreshToken = token;
      return token;
    });
  }

  //get access token
  FutureOr&lt;String?&gt; get accessToken async {
    if (_accessToken != null) {
      return _accessToken;
    }
    return readAccessToken().then((token) {
      _accessToken = token;
      return token;
    });
  }

  //check if token is expired
  Future&lt;bool&gt; tokenIsExpired() async {
    final token = await readAccessToken();
    return token == null || token.isEmpty;
  }

  //load token from secure storage
  Future&lt;void&gt; loadAccessToken() async {
    final token = await readAccessToken();
    if (token != null) {
      _accessToken = token;
    }
  }

  //load refresh token from secure storage
  Future&lt;void&gt; loadRefreshToken() async {
    final token = await readRefreshToken();
    if (token != null) {
      _refreshToken = token;
    }
  }

  //check if refresh token exists
  Future&lt;bool&gt; _hasRefreshToken() async {
    final token = await readRefreshToken();
    return token != null &amp;&amp; token.isNotEmpty;
  }

  Future&lt;void&gt; _checkForRefreshToken() async {
    _hasRefreshToken().then((hasToken) {
      _authStateRepository.setUserLoggedIn(hasToken);
    });
  }

  //check if access token exists
  Future&lt;bool&gt; _hasAccessToken() async {
    final token = await readAccessToken();
    return token != null &amp;&amp; token.isNotEmpty;
  }

  Future&lt;void&gt; _checkForAccessToken() async {
    _hasAccessToken().then((hasToken) {
      _authStateRepository.setUserLoggedIn(hasToken);
    });
  }

  //read refresh token from secure storage
  Future&lt;String?&gt; readRefreshToken() async {
    try {
      const secureStorage = FlutterSecureStorage();
      return secureStorage.read(key: _refreshTokenKey);
    } catch (e) {
      return null;
    }
  }

  //read access token from secure storage
  Future&lt;String?&gt; readAccessToken() async {
    try {
      const secureStorage = FlutterSecureStorage();
      return secureStorage.read(key: _accessTokenKey);
    } catch (e) {
      return null;
    }
  }

  //save refresh token to secure storage
  Future&lt;bool&gt; saveRefreshToken(String? token) async {
    try {
      const secureStorage = FlutterSecureStorage();
      await secureStorage.write(
        key: _refreshTokenKey,
        value: token,
      );
      _refreshToken = token;
      return true;
    } catch (e) {
      return false;
    }
  }

  //save access token to secure storage
  Future&lt;bool&gt; saveAccessToken(String? token) async {
    try {
      const secureStorage = FlutterSecureStorage();
      await secureStorage.write(
        key: _accessTokenKey,
        value: token,
      );
      _accessToken = token;
      return true;
    } catch (e) {
      return false;
    }
  }

  Future&lt;void&gt; refreshAccessToken() async {
    if (await tokenIsExpired()) {
      final refreshToken = await readRefreshToken();
      final accessToken = await readAccessToken();
      if (refreshToken != null &amp;&amp; accessToken != null) {
          final response = await getIt&lt;ApiDatasource&gt;().refreshToken(
            UseTokenModel(accessToken: accessToken, refreshToken: refreshToken),
          );
          final newAccessToken = response.accessToken;

          await saveAccessToken(newAccessToken);

          _accessToken = newAccessToken;
      }
    }
  }
}

token_interceptor.dart:

@injectable
class TokenInterceptor extends Interceptor {
  final TokenRepository _tokenRepository;


  TokenInterceptor(this._tokenRepository);

  @override
  Future&lt;void&gt; onRequest(
      RequestOptions options,
      RequestInterceptorHandler handler,
      ) async {
    final String? token = await _tokenRepository.accessToken;
    if (token != null) {
      options.headers[&#39;Authorization&#39;] = &#39;Bearer $token&#39;;
    }
    handler.next(options);
  }

  @override
  Future&lt;void&gt; onError(
      DioException err,
      ErrorInterceptorHandler handler,
      ) async {
    if (err.response?.statusCode == 401 || err.response?.statusCode == 403) {
      _tokenRepository.refreshAccessToken();
    }
    handler.next(err);
  }
}

I want to conditionally navigate users based on their authentication status. For example, if a user is logged in, they should be directed to the DashboardRoute page. If they are not logged in, they should be directed to the LoginRoute page.

How can I achieve declarative navigation in this setup? Should I modify my AppRouter or my AuthCubit? Can you provide an example of how to conditionally navigate users using the AutoRoute package?

Thank you in advance for your help!

Feel free to ask for more details in comments section

答案1

得分: 0

如果有人遇到与我一样的问题,这里有一个解决方案。

我创建了一个名为AuthWrapperPage的包装类,将authdashboard页面包装在AppRouter中:

part 'app_router.gr.dart';

@AutoRouterConfig()
@lazySingleton
class AppRouter extends _$AppRouter {
  @override
  List<AutoRoute> get routes => [
        AutoRoute(
          page: AuthWrapperRoute.page,
          initial: true,
          children: [
            AutoRoute(
              page: DashboardRoute.page,
              children: [
                AutoRoute(page: AdoptionRoute.page),
                AutoRoute(page: FavoritePetsRoute.page),
                AutoRoute(page: MissingPetsRoute.page),
                AutoRoute(page: MyPetsRoute.page),
                AutoRoute(page: MessagesRoute.page),
                AutoRoute(page: SettingsRoute.page),
                AutoRoute(page: SubmissionsRoute.page),
                AutoRoute(page: MissingRoute.page),
                AutoRoute(page: VolunteeringRoute.page),
              ],
            ),
            AutoRoute(
              page: AuthRoute.page,
              children: [
                AutoRoute(page: LoginRoute.page,),
                AutoRoute(page: RegisterRoute.page),
                AutoRoute(page: PasswordResetEmailRoute.page),
                AutoRoute(page: PasswordResetRoute.page),
                AutoRoute(page: PasswordResetSuccessRoute.page),
              ],
            ),
          ],
        ),
      ];
}

这是包装类的代码:

@RoutePage()
class AuthWrapperPage extends StatelessWidget implements AutoRouteWrapper {
  AuthWrapperPage({super.key});

  final AuthCubit authCubit = getIt<AuthCubit>();
  final AuthStateRepository authStateRepository = getIt<AuthStateRepository>();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<AuthCubit, AuthState>(
      builder: (context, state) {
        return WillPopScope(
          onWillPop: () async => false,
          child: AutoRouter.declarative(routes: (_) {
            return [
              state.map(
                isLoggedIn: (_) {
                  return const AdoptionRoute();
                },
                isLoggedOut: (_) {
                  return const LoginRoute();
                },
              )
            ];
          }),
        );
      },
    );
  }

  @override
  Widget wrappedRoute(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<AuthCubit>(
          create: (context) => authCubit,
        ),
      ],
      child: this,
    );
  }
}

我在AutoRouter.declarative中使用了blocProviderblocbuilder,将状态映射到我要导航到的路由。以下是更新后的TokenInterceptor

@injectable
class TokenInterceptor extends Interceptor {
  final TokenRepository _tokenRepository;

  TokenInterceptor(this._tokenRepository);

  @override
  Future<void> onRequest(
      RequestOptions options,
      RequestInterceptorHandler handler,
  ) async {
    final String? token = await _tokenRepository.accessToken;
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  Future<void> onError(
      DioException err,
      ErrorInterceptorHandler handler,
  ) async {
    if (err.response?.statusCode == 401 || err.response?.statusCode == 403) {
      _tokenRepository.refreshAccessToken();
    }
    handler.next(err);
  }

  @override
  Future<void> onResponse(
      Response response,
      ResponseInterceptorHandler handler,
  ) async {
    final responseBody = response.data as Map<String, dynamic>?;
    if (responseBody != null) {
      final String? accessToken = responseBody['accessToken'] as String?;
      final String? refreshToken = responseBody['refreshToken'] as String?;
      if (accessToken != null) {
        _tokenRepository.saveAccessToken(accessToken);
        _tokenRepository.recheckForAccessToken();
      }
      if (refreshToken != null) {
        _tokenRepository.saveRefreshToken(refreshToken);
      }
    }
    handler.next(response);
  }
}

现在,当我调用API时,onResponse方法会获取访问令牌和刷新令牌,并将它们保存到SecureStorage中。当重新打开应用程序时,tokenRepository的初始化函数会检查令牌是否保存在SecureStorage中,如果保存,则初始屏幕将是adoptionPage,如果任何调用返回响应代码401或403,则会调用refreshToken

英文:

If anyone encounters same problem as mine, here is a solution.

I've made AuthWrapperPage that wraps auth and dashboard pages in AppRouter:

part &#39;app_router.gr.dart&#39;;

@AutoRouterConfig()
@lazySingleton
class AppRouter extends _$AppRouter {
  @override
  List&lt;AutoRoute&gt; get routes =&gt; [
        AutoRoute(
          page: AuthWrapperRoute.page,
          initial: true,
          children: [
            AutoRoute(
              page: DashboardRoute.page,
              children: [
                AutoRoute(page: AdoptionRoute.page),
                AutoRoute(page: FavoritePetsRoute.page),
                AutoRoute(page: MissingPetsRoute.page),
                AutoRoute(page: MyPetsRoute.page),
                AutoRoute(page: MessagesRoute.page),
                AutoRoute(page: SettingsRoute.page),
                AutoRoute(page: SubmissionsRoute.page),
                AutoRoute(page: MissingRoute.page),
                AutoRoute(page: VolunteeringRoute.page),
              ],
            ),
            AutoRoute(
              page: AuthRoute.page,
              children: [
                AutoRoute(page: LoginRoute.page,),
                AutoRoute(page: RegisterRoute.page),
                AutoRoute(page: PasswordResetEmailRoute.page),
                AutoRoute(page: PasswordResetRoute.page),
                AutoRoute(page: PasswordResetSuccessRoute.page),
              ],
            ),
          ],
        ),
      ];
}

here is the wrapper class:

@RoutePage()
class AuthWrapperPage extends StatelessWidget implements AutoRouteWrapper {
  AuthWrapperPage({super.key});

  final AuthCubit authCubit = getIt&lt;AuthCubit&gt;();
  final AuthStateRepository authStateRepository = getIt&lt;AuthStateRepository&gt;();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder&lt;AuthCubit, AuthState&gt;(
      builder: (context, state) {
        return WillPopScope(
          onWillPop: () async =&gt; false,
          child: AutoRouter.declarative(routes: (_) {
            return [
              state.map(
                isLoggedIn: (_) {
                  return const AdoptionRoute();
                },
                isLoggedOut: (_) {
                  return const LoginRoute();
                },
              )
            ];
          }),
        );
      },
    );
  }

  @override
  Widget wrappedRoute(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider&lt;AuthCubit&gt;(
          create: (context) =&gt; authCubit,
        ),
      ],
      child: this,
    );
  }
}

I've used the blocProvider and blocbuilder, inside the AutoRouter.declarative I've mapped the states to the routes that I want navigate to.
Here is updated TokenInterceptor

   @injectable
    class TokenInterceptor extends Interceptor {
      final TokenRepository _tokenRepository;


      TokenInterceptor(this._tokenRepository);

      @override
      Future&lt;void&gt; onRequest(
          RequestOptions options,
          RequestInterceptorHandler handler,
          ) async {
        final String? token = await _tokenRepository.accessToken;
        if (token != null) {
          options.headers[&#39;Authorization&#39;] = &#39;Bearer $token&#39;;
        }
        handler.next(options);
      }

      @override
      Future&lt;void&gt; onError(
          DioException err,
          ErrorInterceptorHandler handler,
          ) async {
        if (err.response?.statusCode == 401 || err.response?.statusCode == 403) {
          _tokenRepository.refreshAccessToken();
        }
        handler.next(err);
      }

      @override
      Future&lt;void&gt; onResponse(
          Response response,
          ResponseInterceptorHandler handler,
          ) async {
        final responseBody = response.data as Map&lt;String, dynamic&gt;?;
        if (responseBody != null) {
          final String? accessToken = responseBody[&#39;accessToken&#39;] as String?;
          final String? refreshToken = responseBody[&#39;refreshToken&#39;] as String?;
          if (accessToken != null) {
            _tokenRepository.saveAccessToken(accessToken);
            _tokenRepository.recheckForAccessToken();
          }
          if (refreshToken != null) {
            _tokenRepository.saveRefreshToken(refreshToken);
          }
        }
        handler.next(response);
      }
    }

now when I make call to the API the onResponse method gets access and refresh tokens and saves them to the SecureStorage, when you reopen the app tokenReposiitory init function checks if the token is saved to the SecureStorage, if it is the initial screen will be adoptionPage if any call will return response code 401 or 403 the refreshToken will be called.

huangapple
  • 本文由 发表于 2023年8月10日 19:49:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76875471.html
匿名

发表评论

匿名网友

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

确定