英文:
Declarative Navigation using AutoRoute in Flutter
问题
我正在尝试在我的Flutter应用中使用AutoRoute
包实现声明式导航。我在我的AppRouter
类中定义了一个复杂的导航结构,并且我正在使用AuthCubit
来管理用户的身份验证状态。我希望根据用户的身份验证状态实现声明式导航,使用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;
}
接下来,您希望根据用户的身份验证状态进行条件导航。例如,如果用户已登录,则应将他们导航到DashboardRoute
页面。如果他们未登录,则应将他们导航到LoginRoute
页面。
要实现这一点,您可以在AppRouter
中使用AutoRoute
的onGenerate
回调来进行条件导航。以下是一个示例:
final appRouter = AppRouter(
onGenerate: (settings) {
if (context.read<AuthCubit>().state is AuthState.isLoggedIn) {
// 用户已登录,导航到DashboardRoute
return DashboardRoute();
} else {
// 用户未登录,导航到LoginRoute
return LoginRoute();
}
},
// 其他路由配置...
);
这样,每当尝试导航到一个新的路由时,onGenerate
回调将根据用户的身份验证状态决定要导航到的路由。这是一种基于声明的导航方式,根据用户状态自动选择正确的目标路由。
希望这可以帮助您实现声明式导航并根据用户的身份验证状态导航到正确的页面。如果您需要更多帮助或有其他问题,请随时提出。
英文:
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
答案1
得分: 0
如果有人遇到与我一样的问题,这里有一个解决方案。
我创建了一个名为AuthWrapperPage
的包装类,将auth
和dashboard
页面包装在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),
],
),
],
),
];
}
这是包装类的代码:
@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,
);
}
}
我在AutoRouter.declarative
中使用了blocProvider
和blocbuilder
,将状态映射到我要导航到的路由。以下是更新后的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);
}
}
现在,当我调用API时,onResponse
方法会获取访问令牌和刷新令牌,并将它们保存到SecureStorage
中。当重新打开应用程序时,tokenRepository
的初始化函数会检查令牌是否保存在SecureStorage
中,如果保存,则初始屏幕将是adoptionPage
,如果任何调用返回响应代码401或403,则会调用refreshToken
。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论