I’m fairly new to the BLoC supplier and that is the primary larger undertaking the place I implement the supplier. To offer you a brief overview of the undertaking, it has a login display, after efficiently logging in – it redirects you to the ‘Reservations’ display the place you may view all of your reservations.
I’m utilizing retrofit for the API calls, the authentication is made with Password Grant sort (OAuth), which means it returns entry and refresh token when the login particulars are appropriate.
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 in an effort 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.
I’ve applied it, it efficiently removes the consumer from the safe storage, nevertheless, the display is caught on the ‘Reservations’ display with round progress indicator loading, moderately than navigating to the login display.
Here is a code breakdown:
These are my three screens.
most important.dart
ultimate GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void most important() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDependencies();
runApp(MyApp(
appRouter: AppRouter(),
));
}
class MyApp extends StatelessWidget {
ultimate 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: (_, youngster) {
return MultiBlocProvider(
suppliers: [
BlocProvider<AuthBloc>(
create: (context) =>
sl<AuthBloc>()..add(const CheckAuthentication()),
),
BlocProvider<ReservationsFindAllBloc>(
create: (context) => sl<ReservationsFindAllBloc>(),
),
],
youngster: MaterialApp(
theme: ThemeData(),
dwelling: 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);
}
},
youngster: 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> {
ultimate _formKey = GlobalKey<FormState>();
ultimate TextEditingController _usernameController = TextEditingController();
ultimate TextEditingController _passwordController = TextEditingController();
ultimate FocusNode _usernameFocusNode = FocusNode();
ultimate 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 Middle(
youngster: CircularProgressIndicator(),
);
} else {
return Padding(
padding: const EdgeInsets.all(10),
youngster: Container(
margin: EdgeInsets.solely(
high: MediaQuery.of(context).viewPadding.high + 0.1.sh),
youngster: Column(
youngsters: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
height: 0.06.sh,
fit: BoxFit.contain,
),
],
),
Type(
key: _formKey,
youngster: Container(
width: double.infinity,
margin: EdgeInsets.solely(high: 0.07.sh),
youngster: 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(
peak: 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),
youngster: 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(
youngster: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Unauthenticated) {
Navigator.pushReplacementNamed(context, '/login');
}
},
youngster: BlocBuilder<ReservationsFindAllBloc,
ReservationsFindAllState>(
builder: (_, state) {
if (state is ReservationsLoading) {
return const Middle(
youngster: 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> {
ultimate FindAllUseCase _findAllUseCase;
ReservationsFindAllBloc(this._findAllUseCase) : tremendous(const ReservationsLoading()) {
on<GetReservationsFindAll>(onFindAll);
}
void onFindAll(GetReservationsFindAll getReservationsFindAll, Emitter<ReservationsFindAllState> emit) async {
emit(const ReservationsLoading());
ultimate dataState = await _findAllUseCase.name(params: getReservationsFindAll.reservationsFindAllDto);
if (dataState is DataSuccess) {
emit(ReservationsLoaded(dataState.knowledge!));
}
if (dataState is DataFailed) {
emit(ReservationsError(dataState.exception!));
}
}
}
auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
ultimate LoginUseCase _loginUseCase;
ultimate LogoutUseCase _logoutUseCase;
ultimate 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());
ultimate dataState = await _loginUseCase(params: loginEvent.loginDto);
if (dataState is DataSuccess) {
emit(Authenticated(dataState.knowledge!));
}
if (dataState is DataFailed) {
emit(AuthenticationError(dataState.exception!));
}
}
void onLogout(LogoutEvent logoutEvent, Emitter<AuthState> emit) async {
emit(const LoadingState());
ultimate isLoggedOut = await _logoutUseCase.name();
if (isLoggedOut) {
emit(const Unauthenticated());
}
}
void onCheckAuthentication(CheckAuthentication checkAuthentication,
Emitter<AuthState> emit) async {
ultimate 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 must be logged out and brought to the /login display.
auth_interceptor.dart
class AuthInterceptor extends Interceptor {
ultimate TokenRepository _tokenRepository;
ultimate SecureStorage _secureStorage;
ultimate AuthBloc authBloc;
ultimate _dio = sl<Dio>();
AuthInterceptor(this._secureStorage, this._tokenRepository, this.authBloc);
@override
void onRequest(
RequestOptions choices, RequestInterceptorHandler handler) async {
print('Knowledge for request...');
print(choices.knowledge);
choices.headers['Accept'] = 'software/json';
if (choices.headers.containsKey('Content material-Sort')) {
choices.headers['Content-Type'] = 'software/json';
}
if (!choices.additional.containsKey('isRetry')) {
ultimate 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?.knowledge['error'] == 'Token expired.') {
attempt {
ultimate refreshToken = await _secureStorage.getRefreshToken();
if (refreshToken != null) {
ultimate response = await _tokenRepository.refresh(
RefreshTokenDto(refreshToken: refreshToken),
);
if (response is DataSuccess) {
// Retry the unique request with new entry token
ultimate RequestOptions retryOptions = err.requestOptions;
retryOptions.additional['isRetry'] = true; // Set flag to point retry
retryOptions.headers['Authorization'] =
'Bearer ${response.knowledge!.accessToken}';
// Resend the request with up to date choices
ultimate updatedResponse = await _dio.request(
retryOptions.uri.toString(),
choices: Choices(
methodology: retryOptions.methodology,
headers: retryOptions.headers,
responseType: retryOptions.responseType,
),
knowledge: retryOptions.knowledge,
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 so much!