I’m fairly new to the BLoC supplier and that is the primary larger undertaking the place I implement the supplier. To provide you a brief overview of the undertaking, it has a login display screen, after efficiently logging in – it redirects you to the ‘Reservations’ display screen the place you may view all of your reservations.
I’m utilizing retrofit for the API calls, the authentication is made with Password Grant kind (OAuth), which means it returns entry and refresh token when the login particulars are right.
I applied an interceptor so when the entry token is expired, it fires a brand new API name to the /oauth/refresh with the refreshToken from storage so as to get hold of a brand new entry and refresh tokens.
What I need to obtain is the next situation: if the entry token is expired, then it tries to acquire new one by passing the refresh token, nevertheless if that’s expired too then it return an error (401 Unauthenticated) – if this situation occurs I want to logout the consumer from the app and redirect them to the /login display screen.
I’ve applied it, it efficiently removes the consumer from the safe storage, nevertheless, the display screen is caught on the ‘Reservations’ display screen with round progress indicator loading, fairly than navigating to the login display screen.
Here is a code breakdown:
These are my three screens.
principal.dart
remaining GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void principal() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDependencies();
runApp(MyApp(
appRouter: AppRouter(),
));
}
class MyApp extends StatelessWidget {
remaining AppRouter appRouter;
const MyApp({Key? key, required this.appRouter}) : tremendous(key: key);
@override
Widget construct(BuildContext context) {
return ScreenUtilInit(
designSize: const Dimension(430, 932),
minTextAdapt: true,
splitScreenMode: true,
builder: (_, little one) {
return MultiBlocProvider(
suppliers: [
BlocProvider<AuthBloc>(
create: (context) =>
sl<AuthBloc>()..add(const CheckAuthentication()),
),
BlocProvider<ReservationsFindAllBloc>(
create: (context) => sl<ReservationsFindAllBloc>(),
),
],
little one: MaterialApp(
theme: ThemeData(),
residence: 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);
}
},
little one: Navigator(
key: navigatorKey,
onGenerateRoute: AppRouter().onGenerateRoute,
),
),
),
);
});
}
}
login.dart
class LoginScreen extends StatefulWidget {
const LoginScreen({tremendous.key});
@override
State<StatefulWidget> createState() {
return _LoginScreenState();
}
}
class _LoginScreenState extends State<LoginScreen> {
remaining _formKey = GlobalKey<FormState>();
remaining TextEditingController _usernameController = TextEditingController();
remaining TextEditingController _passwordController = TextEditingController();
remaining FocusNode _usernameFocusNode = FocusNode();
remaining FocusNode _passwordFocusNode = FocusNode();
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
_usernameFocusNode.dispose();
_passwordFocusNode.dispose();
tremendous.dispose();
}
@override
Widget construct(BuildContext context) {
return Scaffold(
backgroundColor: colorGrayLighter,
physique: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is LoadingState) {
return const Heart(
little one: CircularProgressIndicator(),
);
} else {
return Padding(
padding: const EdgeInsets.all(10),
little one: Container(
margin: EdgeInsets.solely(
prime: MediaQuery.of(context).viewPadding.prime + 0.1.sh),
little one: Column(
youngsters: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
height: 0.06.sh,
fit: BoxFit.contain,
),
],
),
Type(
key: _formKey,
little one: Container(
width: double.infinity,
margin: EdgeInsets.solely(prime: 0.07.sh),
little one: Column(
youngsters: [
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(
top: 30.sp,
),
CommonButton(
buttonText: 'Најави се',
buttonType: ButtonType.FilledRed,
onTap: () {
BlocProvider.of<AuthBloc>(context).add(LoginEvent(
LoginDto(
username: _usernameController.textual content,
password: _passwordController.textual content)));
},
)
],
),
),
);
}
},
));
}
}
reservations.dart
class ReservationsScreen extends StatefulWidget {
const ReservationsScreen({Key? key}) : tremendous(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 Textual content("Error"),
content material: Textual content(message),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("OK"),
),
],
);
},
);
}
@override
void initState() {
tremendous.initState();
BlocProvider.of<ReservationsFindAllBloc>(context)
.add(const GetReservationsFindAll());
}
@override
Widget construct(BuildContext context) {
return Scaffold(
backgroundColor: colorGrayLighter,
physique: Padding(
padding: const EdgeInsets.all(10),
little one: Column(
crossAxisAlignment: CrossAxisAlignment.begin,
youngsters: [
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,
)),
))),
],
),
),
// Different widgets...
Expanded(
little one: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Unauthenticated) {
Navigator.pushReplacementNamed(context, '/login');
}
},
little one: BlocBuilder<ReservationsFindAllBloc,
ReservationsFindAllState>(
builder: (_, state) {
if (state is ReservationsLoading) {
return const Heart(
little one: 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();
},
),
),
),
],
),
),
);
}
}
These are the Blocs:
reservations_find_all_bloc.dart
class ReservationsFindAllBloc extends Bloc<ReservationsFindAllEvent, ReservationsFindAllState> {
remaining FindAllUseCase _findAllUseCase;
ReservationsFindAllBloc(this._findAllUseCase) : tremendous(const ReservationsLoading()) {
on<GetReservationsFindAll>(onFindAll);
}
void onFindAll(GetReservationsFindAll getReservationsFindAll, Emitter<ReservationsFindAllState> emit) async {
emit(const ReservationsLoading());
remaining dataState = await _findAllUseCase.name(params: getReservationsFindAll.reservationsFindAllDto);
if (dataState is DataSuccess) {
emit(ReservationsLoaded(dataState.information!));
}
if (dataState is DataFailed) {
emit(ReservationsError(dataState.exception!));
}
}
}
auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
remaining LoginUseCase _loginUseCase;
remaining LogoutUseCase _logoutUseCase;
remaining SecureStorage _secureStorage;
AuthBloc(this._loginUseCase, this._logoutUseCase, this._secureStorage)
: tremendous(const Unauthenticated()) {
on<LoginEvent>(onLogin);
on<LogoutEvent>(onLogout);
on<CheckAuthentication>(onCheckAuthentication);
}
void onLogin(LoginEvent loginEvent, Emitter<AuthState> emit) async {
emit(const LoadingState());
remaining dataState = await _loginUseCase(params: loginEvent.loginDto);
if (dataState is DataSuccess) {
emit(Authenticated(dataState.information!));
}
if (dataState is DataFailed) {
emit(AuthenticationError(dataState.exception!));
}
}
void onLogout(LogoutEvent logoutEvent, Emitter<AuthState> emit) async {
emit(const LoadingState());
remaining isLoggedOut = await _logoutUseCase.name();
if (isLoggedOut) {
emit(const Unauthenticated());
}
}
void onCheckAuthentication(CheckAuthentication checkAuthentication,
Emitter<AuthState> emit) async {
remaining consumer = await _secureStorage.getUser();
if (consumer != null) {
emit(Authenticated(consumer));
} else {
emit(const Unauthenticated());
}
}
}
and that is the interceptor:
If the observe assertion is true else if (response is DataFailed)
then the consumer needs to be logged out and brought to the /login display screen.
auth_interceptor.dart
class AuthInterceptor extends Interceptor {
remaining TokenRepository _tokenRepository;
remaining SecureStorage _secureStorage;
remaining AuthBloc authBloc;
remaining _dio = sl<Dio>();
AuthInterceptor(this._secureStorage, this._tokenRepository, this.authBloc);
@override
void onRequest(
RequestOptions choices, RequestInterceptorHandler handler) async {
print('Information for request...');
print(choices.information);
choices.headers['Accept'] = 'utility/json';
if (choices.headers.containsKey('Content material-Sort')) {
choices.headers['Content-Type'] = 'utility/json';
}
if (!choices.further.containsKey('isRetry')) {
remaining accessToken = await _secureStorage.getAccessToken();
if (accessToken != null) {
choices.headers['Authorization'] = 'Bearer $accessToken';
}
}
return handler.subsequent(choices);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 &&
err.response?.information['error'] == 'Token expired.') {
strive {
remaining refreshToken = await _secureStorage.getRefreshToken();
if (refreshToken != null) {
remaining response = await _tokenRepository.refresh(
RefreshTokenDto(refreshToken: refreshToken),
);
if (response is DataSuccess) {
// Retry the unique request with new entry token
remaining RequestOptions retryOptions = err.requestOptions;
retryOptions.further['isRetry'] = true; // Set flag to point retry
retryOptions.headers['Authorization'] =
'Bearer ${response.information!.accessToken}';
// Resend the request with up to date choices
remaining updatedResponse = await _dio.request(
retryOptions.uri.toString(),
choices: Choices(
technique: retryOptions.technique,
headers: retryOptions.headers,
responseType: retryOptions.responseType,
),
information: retryOptions.information,
queryParameters: retryOptions.queryParameters,
cancelToken: retryOptions.cancelToken,
onReceiveProgress: retryOptions.onReceiveProgress,
onSendProgress: retryOptions.onSendProgress,
);
// Ahead the response to the unique handler
return handler.resolve(updatedResponse);
} else if (response is DataFailed) {
// Logout consumer if refresh token fails
authBloc.add(const LogoutEvent());
return;
}
}
} catch (e) {
print('Error refreshing tokens: $e');
}
return handler.subsequent(err);
}
tremendous.onError(err, handler);
}
}
Be happy to let me know if I’ve tousled someplace with the Bloc Suppliers, Listeners, or Builders all through the app as a result of I’m nonetheless new to this idea.
Thanks lots!