我对 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 的某个地方搞砸了,请随时告诉我,因为我对这个概念还很陌生。
非常感谢!
您可以注销:
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,
);
}