如何使用 BLoC 提供程序在 Flutter 中注销并重定向到登录屏幕?

问题描述 投票:0回答:1

我对 BLoC 提供程序还很陌生,这是我实现该提供程序的第一个更大的项目。为了向您简要介绍该项目,它有一个登录屏幕,成功登录后,它会将您重定向到“预订”屏幕,您可以在其中查看所有预订。

我正在对 API 调用使用改造,身份验证是使用密码授予类型 (OAuth) 进行的,这意味着当登录详细信息正确时,它会返回访问和刷新令牌。

我实现了一个拦截器,因此当访问令牌过期时,它会使用存储中的刷新令牌对 /oauth/refresh 发起新的 API 调用,以获得新的访问令牌和刷新令牌。

我想要实现的是以下场景:如果访问令牌已过期,则它会尝试通过传递刷新令牌来获取新的令牌,但是如果访问令牌也已过期,则它会返回错误(401未验证) - 如果这种情况碰巧我想从应用程序中注销用户并将其重定向到/登录屏幕。

我已经实现了它,它成功地将用户从安全存储中删除,但是,屏幕卡在“预订”屏幕上,并加载圆形进度指示器,而不是导航到登录屏幕。

这是代码细分:

这是我的三个屏幕。 main.dart

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDependencies();
  runApp(MyApp(
    appRouter: AppRouter(),
  ));
}

class MyApp extends StatelessWidget {
  final AppRouter appRouter;

  const MyApp({Key? key, required this.appRouter}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ScreenUtilInit(
        designSize: const Size(430, 932),
        minTextAdapt: true,
        splitScreenMode: true,
        builder: (_, child) {
          return MultiBlocProvider(
            providers: [
              BlocProvider<AuthBloc>(
                create: (context) =>
                    sl<AuthBloc>()..add(const CheckAuthentication()),
              ),
              BlocProvider<ReservationsFindAllBloc>(
                create: (context) => sl<ReservationsFindAllBloc>(),
              ),
            ],
            child: MaterialApp(
              theme: ThemeData(),
              home: BlocListener<AuthBloc, AuthState>(
                listener: (context, state) {
                  if (state is Authenticated) {
                    navigatorKey.currentState!.pushNamedAndRemoveUntil('/reservations', (route) => false);
                  } else if (state is Unauthenticated) {
                    navigatorKey.currentState!.pushNamedAndRemoveUntil('/login', (route) => false);
                  }
                },
                child: Navigator(
                  key: navigatorKey,
                  onGenerateRoute: AppRouter().onGenerateRoute,
                ),
              ),
            ),
          );
        });
  }
}

登录.dart

class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<StatefulWidget> createState() {
    return _LoginScreenState();
  }
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();

  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  final FocusNode _usernameFocusNode = FocusNode();
  final FocusNode _passwordFocusNode = FocusNode();

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    _usernameFocusNode.dispose();
    _passwordFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: colorGrayLighter,
        body: BlocBuilder<AuthBloc, AuthState>(
          builder: (context, state) {
            if (state is LoadingState) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            } else {
              return Padding(
                padding: const EdgeInsets.all(10),
                child: Container(
                  margin: EdgeInsets.only(
                      top: MediaQuery.of(context).viewPadding.top + 0.1.sh),
                  child: Column(
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Image.asset(
                            'assets/images/logo.png',
                            height: 0.06.sh,
                            fit: BoxFit.contain,
                          ),
                        ],
                      ),
                      Form(
                        key: _formKey,
                        child: Container(
                            width: double.infinity,
                            margin: EdgeInsets.only(top: 0.07.sh),
                            child: Column(
                              children: [
                                TextFieldWidget(
                                  textInputType: TextInputType.text,
                                  defaultText: 'Корисничко име',
                                  hintText: 'Корисничко име',
                                  focusNode: _usernameFocusNode,
                                  controller: _usernameController,
                                ),
                                SizedBox(
                                  height: 20.sp,
                                ),
                                TextFieldWidget(
                                  textInputType: TextInputType.text,
                                  defaultText: 'Лозинка',
                                  hintText: 'Лозинка',
                                  obscureText: true,
                                  focusNode: _passwordFocusNode,
                                  controller: _passwordController,
                                ),
                              ],
                            )),
                      ),
                      SizedBox(
                        height: 30.sp,
                      ),
                      CommonButton(
                        buttonText: 'Најави се',
                        buttonType: ButtonType.FilledRed,
                        onTap: () {
                          BlocProvider.of<AuthBloc>(context).add(LoginEvent(
                              LoginDto(
                                  username: _usernameController.text,
                                  password: _passwordController.text)));
                        },
                      )
                    ],
                  ),
                ),
              );
            }
          },
        ));
  }
}

预订.dart

class ReservationsScreen extends StatefulWidget {
  const ReservationsScreen({Key? key}) : super(key: key);

  @override
  State<ReservationsScreen> createState() => _ReservationsScreenState();
}

class _ReservationsScreenState extends State<ReservationsScreen> {
  void showAlert(String message) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Error"),
          content: Text(message),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text("OK"),
            ),
          ],
        );
      },
    );
  }

  @override
  void initState() {
    super.initState();

    BlocProvider.of<ReservationsFindAllBloc>(context)
        .add(const GetReservationsFindAll());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: colorGrayLighter,
      body: Padding(
        padding: const EdgeInsets.all(10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
              height: MediaQuery
                  .of(context)
                  .viewPadding
                  .top,
            ),
            Text("Резервации", style: font28Medium),
            Container(
              height: 40.h,
              padding: const EdgeInsets.only(top: 8),
              child: ListView(
                scrollDirection: Axis.horizontal,
                children: [
                  Padding(
                      padding: const EdgeInsets.only(right: 8),
                      child: OutlinedButton(
                          onPressed: () {
                            BlocProvider.of<ReservationsFindAllBloc>(context)
                                .add(GetReservationsFindAll(
                                reservationsFindAllDto:
                                ReservationsFindAllDto(
                                    filterReservationsEnum:
                                    FilterReservationsEnum.ALL)));
                          },
                          style: OutlinedButton.styleFrom(
                            padding: EdgeInsets.fromLTRB(20, 0, 20, 0),
                            side: const BorderSide(
                                color: colorBlack), // Set border color
                          ),
                          child: Text(
                            'Сите (1029)',
                            style: GoogleFonts.montserrat(
                                fontWeight: FontWeight.w500,
                                fontSize: 12,
                                textStyle: const TextStyle(
                                  color: colorBlack,
                                )),
                          ))),
                  Padding(
                      padding: const EdgeInsets.only(right: 8),
                      child: OutlinedButton(
                          onPressed: () {
                            BlocProvider.of<ReservationsFindAllBloc>(context)
                                .add(GetReservationsFindAll(
                                reservationsFindAllDto:
                                ReservationsFindAllDto(
                                    filterReservationsEnum:
                                    FilterReservationsEnum
                                        .NOT_APPROVED)));
                          },
                          style: OutlinedButton.styleFrom(
                            minimumSize: Size.zero,
                            padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                            side: const BorderSide(
                                color: colorGrayLight), // Set border color
                          ),
                          child: Text(
                            'Непотврдени (1029)',
                            style: GoogleFonts.montserrat(
                                fontWeight: FontWeight.normal,
                                fontSize: 12,
                                textStyle: const TextStyle(
                                  color: colorGrayLight,
                                )),
                          ))),
                  Padding(
                      padding: const EdgeInsets.only(right: 8),
                      child: OutlinedButton(
                          onPressed: () {
                            BlocProvider.of<ReservationsFindAllBloc>(context)
                                .add(GetReservationsFindAll(
                                reservationsFindAllDto:
                                ReservationsFindAllDto(
                                    filterReservationsEnum:
                                    FilterReservationsEnum
                                        .APPROVED)));
                          },
                          style: OutlinedButton.styleFrom(
                            minimumSize: Size.zero,
                            padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                            side: const BorderSide(
                                color: colorGrayLight), // Set border color
                          ),
                          child: Text(
                            'Потврдени (1029)',
                            style: GoogleFonts.montserrat(
                                fontWeight: FontWeight.normal,
                                fontSize: 12,
                                textStyle: const TextStyle(
                                  color: colorGrayLight,
                                )),
                          ))),
                ],
              ),
            ),
            // Other widgets...
            Expanded(
              child: BlocListener<AuthBloc, AuthState>(
                listener: (context, state) {
                  if (state is Unauthenticated) {
                    Navigator.pushReplacementNamed(context, '/login');
                  }
                },
                child: BlocBuilder<ReservationsFindAllBloc,
                    ReservationsFindAllState>(
                  builder: (_, state) {
                    if (state is ReservationsLoading) {
                      return const Center(
                        child: CircularProgressIndicator(),
                      );
                    }

                    if (state is ReservationsLoaded) {
                      return ReservationsList(
                        reservationListEntity: state.reservationListEntity!,
                      );
                    }

                    if (state is ReservationsError) {
                      Future.microtask(() {
                        showAlert(Utils.getErrorResponseMessage(
                            state.exception!.response!));
                      });
                    }

                    return const SizedBox();
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

这些是集团: servations_find_all_bloc.dart

class ReservationsFindAllBloc extends Bloc<ReservationsFindAllEvent, ReservationsFindAllState> {
  final FindAllUseCase _findAllUseCase;

  ReservationsFindAllBloc(this._findAllUseCase) : super(const ReservationsLoading()) {
    on<GetReservationsFindAll>(onFindAll);
  }

  void onFindAll(GetReservationsFindAll getReservationsFindAll, Emitter<ReservationsFindAllState> emit) async {
    emit(const ReservationsLoading());

    final dataState = await _findAllUseCase.call(params: getReservationsFindAll.reservationsFindAllDto);

    if (dataState is DataSuccess) {
      emit(ReservationsLoaded(dataState.data!));
    }

    if (dataState is DataFailed) {
      emit(ReservationsError(dataState.exception!));
    }
  }
}

auth_bloc.dart

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final LoginUseCase _loginUseCase;
  final LogoutUseCase _logoutUseCase;
  final SecureStorage _secureStorage;

  AuthBloc(this._loginUseCase, this._logoutUseCase, this._secureStorage)
      : super(const Unauthenticated()) {
    on<LoginEvent>(onLogin);
    on<LogoutEvent>(onLogout);
    on<CheckAuthentication>(onCheckAuthentication);
  }

  void onLogin(LoginEvent loginEvent, Emitter<AuthState> emit) async {
    emit(const LoadingState());
    final dataState = await _loginUseCase(params: loginEvent.loginDto);

    if (dataState is DataSuccess) {
      emit(Authenticated(dataState.data!));
    }

    if (dataState is DataFailed) {
      emit(AuthenticationError(dataState.exception!));
    }
  }

  void onLogout(LogoutEvent logoutEvent, Emitter<AuthState> emit) async {
    emit(const LoadingState());

    final isLoggedOut = await _logoutUseCase.call();

    if (isLoggedOut) {
      emit(const Unauthenticated());
    }
  }

  void onCheckAuthentication(CheckAuthentication checkAuthentication,
      Emitter<AuthState> emit) async {
    final user = await _secureStorage.getUser();

    if (user != null) {
      emit(Authenticated(user));
    } else {
      emit(const Unauthenticated());
    }
  }
}

这是拦截器:

如果以下语句为真

else if (response is DataFailed)
那么用户应该注销并进入 /login 屏幕。

auth_interceptor.dart

class AuthInterceptor extends Interceptor {
  final TokenRepository _tokenRepository;
  final SecureStorage _secureStorage;
  final AuthBloc authBloc;
  final _dio = sl<Dio>();

  AuthInterceptor(this._secureStorage, this._tokenRepository, this.authBloc);

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    print('Data for request...');
    print(options.data);
    options.headers['Accept'] = 'application/json';
    if (options.headers.containsKey('Content-Type')) {
      options.headers['Content-Type'] = 'application/json';
    }

    if (!options.extra.containsKey('isRetry')) {
      final accessToken = await _secureStorage.getAccessToken();

      if (accessToken != null) {
        options.headers['Authorization'] = 'Bearer $accessToken';
      }
    }

    return handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401 &&
        err.response?.data['error'] == 'Token expired.') {
      try {
        final refreshToken = await _secureStorage.getRefreshToken();

        if (refreshToken != null) {
          final response = await _tokenRepository.refresh(
            RefreshTokenDto(refreshToken: refreshToken),
          );

          if (response is DataSuccess) {
            // Retry the original request with new access token
            final RequestOptions retryOptions = err.requestOptions;
            retryOptions.extra['isRetry'] = true; // Set flag to indicate retry
            retryOptions.headers['Authorization'] =
                'Bearer ${response.data!.accessToken}';
            // Resend the request with updated options
            final updatedResponse = await _dio.request(
              retryOptions.uri.toString(),
              options: Options(
                method: retryOptions.method,
                headers: retryOptions.headers,
                responseType: retryOptions.responseType,
              ),
              data: retryOptions.data,
              queryParameters: retryOptions.queryParameters,
              cancelToken: retryOptions.cancelToken,
              onReceiveProgress: retryOptions.onReceiveProgress,
              onSendProgress: retryOptions.onSendProgress,
            );

            // Forward the response to the original handler
            return handler.resolve(updatedResponse);
          } else if (response is DataFailed) {
            // Logout user if refresh token fails
            authBloc.add(const LogoutEvent());
            return;
          }
        }
      } catch (e) {
        print('Error refreshing tokens: $e');
      }
      return handler.next(err);
    }

    super.onError(err, handler);
  }
}

如果我在整个应用程序中的 Bloc Providers、Listeners 或 Builders 的某个地方搞砸了,请随时告诉我,因为我对这个概念还很陌生。

非常感谢!

android ios flutter mobile retrofit
1个回答
0
投票

您可以注销:

void onLogout(LogoutEvent logoutEvent, Emitter<AuthState> emit) async {
  emit(const LoadingState());

  final isLoggedOut = await _logoutUseCase.call();

  if (isLoggedOut) {
    emit(const Unauthenticated());
  }
}

并且你需要正确处理

Unauthenticated
状态

    if (state is Unauthenticated) {
          Navigator.pushReplacementNamed(context, '/login');

        //or

        Navigator.pushAndRemoveUntil(
        context,
        MaterialPageRoute(builder: (context) => LoginScreen()),
        (route) => false,
      );
     }
© www.soinside.com 2019 - 2024. All rights reserved.