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 '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;
}
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<String?> get refreshToken async {
if (_refreshToken != null) {
return _refreshToken;
}
return readRefreshToken().then((token) {
_refreshToken = token;
return token;
});
}
//get access token
FutureOr<String?> get accessToken async {
if (_accessToken != null) {
return _accessToken;
}
return readAccessToken().then((token) {
_accessToken = token;
return token;
});
}
//check if token is expired
Future<bool> tokenIsExpired() async {
final token = await readAccessToken();
return token == null || token.isEmpty;
}
//load token from secure storage
Future<void> loadAccessToken() async {
final token = await readAccessToken();
if (token != null) {
_accessToken = token;
}
}
//load refresh token from secure storage
Future<void> loadRefreshToken() async {
final token = await readRefreshToken();
if (token != null) {
_refreshToken = token;
}
}
//check if refresh token exists
Future<bool> _hasRefreshToken() async {
final token = await readRefreshToken();
return token != null && token.isNotEmpty;
}
Future<void> _checkForRefreshToken() async {
_hasRefreshToken().then((hasToken) {
_authStateRepository.setUserLoggedIn(hasToken);
});
}
//check if access token exists
Future<bool> _hasAccessToken() async {
final token = await readAccessToken();
return token != null && token.isNotEmpty;
}
Future<void> _checkForAccessToken() async {
_hasAccessToken().then((hasToken) {
_authStateRepository.setUserLoggedIn(hasToken);
});
}
//read refresh token from secure storage
Future<String?> readRefreshToken() async {
try {
const secureStorage = FlutterSecureStorage();
return secureStorage.read(key: _refreshTokenKey);
} catch (e) {
return null;
}
}
//read access token from secure storage
Future<String?> readAccessToken() async {
try {
const secureStorage = FlutterSecureStorage();
return secureStorage.read(key: _accessTokenKey);
} catch (e) {
return null;
}
}
//save refresh token to secure storage
Future<bool> 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<bool> saveAccessToken(String? token) async {
try {
const secureStorage = FlutterSecureStorage();
await secureStorage.write(
key: _accessTokenKey,
value: token,
);
_accessToken = token;
return true;
} catch (e) {
return false;
}
}
Future<void> refreshAccessToken() async {
if (await tokenIsExpired()) {
final refreshToken = await readRefreshToken();
final accessToken = await readAccessToken();
if (refreshToken != null && accessToken != null) {
final response = await getIt<ApiDatasource>().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<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);
}
}
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
If anyone encounters same problem as mine, here is a solution.
I've made AuthWrapperPage
that wraps auth
and dashboard
pages in 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),
],
),
],
),
];
}
here is the wrapper class:
@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,
);
}
}
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<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);
}
}
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.