Template
chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库 7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系 5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter 通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sunny_mochi/core/error/exception_mapper.dart';
|
||||
import 'package:sunny_mochi/features/auth/auth_providers.dart';
|
||||
import 'package:sunny_mochi/features/auth/domain/entities/user_entity.dart';
|
||||
import 'package:sunny_mochi/features/auth/presentation/notifiers/auth_status_provider.dart';
|
||||
|
||||
part 'auth_notifier.freezed.dart';
|
||||
part 'auth_notifier.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class AuthState with _$AuthState {
|
||||
const factory AuthState.initial() = AuthInitial;
|
||||
const factory AuthState.loading() = AuthLoading;
|
||||
const factory AuthState.authenticated(UserEntity user) = AuthAuthenticated;
|
||||
const factory AuthState.unauthenticated() = AuthUnauthenticated;
|
||||
const factory AuthState.error(String message) = AuthError;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class AuthNotifier extends _$AuthNotifier {
|
||||
static const _mapper = ExceptionMapper();
|
||||
|
||||
// generation 计数:防止旧 async 回调覆盖最新登录态
|
||||
int _generation = 0;
|
||||
|
||||
@override
|
||||
AuthState build() {
|
||||
final gen = ++_generation;
|
||||
unawaited(_checkAuth(gen));
|
||||
return const AuthState.initial();
|
||||
}
|
||||
|
||||
Future<void> _checkAuth(int gen) async {
|
||||
final user = await ref.read(authRepositoryProvider).getCurrentUser();
|
||||
if (_generation != gen) return;
|
||||
if (user != null) {
|
||||
state = AuthState.authenticated(user);
|
||||
ref.read(authStatusProvider).markLoggedIn();
|
||||
unawaited(refreshUserInfo());
|
||||
} else {
|
||||
state = const AuthState.unauthenticated();
|
||||
ref.read(authStatusProvider).markLoggedOut();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loginWithPhone(String phone, String code) async {
|
||||
_generation++;
|
||||
state = const AuthState.loading();
|
||||
try {
|
||||
final user = await ref
|
||||
.read(authRepositoryProvider)
|
||||
.loginWithPhone(phone: phone, code: code);
|
||||
state = AuthState.authenticated(user);
|
||||
ref.read(authStatusProvider).markLoggedIn();
|
||||
unawaited(refreshUserInfo());
|
||||
} on Object catch (e, st) {
|
||||
final failure = _mapper.fromUnknown(e, st);
|
||||
state = AuthState.error(failure.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loginWithPassword(String phone, String password) async {
|
||||
_generation++;
|
||||
state = const AuthState.loading();
|
||||
try {
|
||||
final user = await ref
|
||||
.read(authRepositoryProvider)
|
||||
.loginWithPassword(phone: phone, password: password);
|
||||
state = AuthState.authenticated(user);
|
||||
ref.read(authStatusProvider).markLoggedIn();
|
||||
unawaited(refreshUserInfo());
|
||||
} on Object catch (e, st) {
|
||||
final failure = _mapper.fromUnknown(e, st);
|
||||
state = AuthState.error(failure.message);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_generation++;
|
||||
state = const AuthState.unauthenticated();
|
||||
ref.read(authStatusProvider).markLoggedOut();
|
||||
unawaited(ref.read(authRepositoryProvider).logout());
|
||||
}
|
||||
|
||||
/// 发送短信验证码。返回 dev/test 环境后端回显的验证码(release 为 null),供 UI 自动填入。
|
||||
Future<String?> sendSmsCode(String phone) async {
|
||||
try {
|
||||
return await ref.read(authRepositoryProvider).sendSmsCode(phone);
|
||||
} on Object catch (e, st) {
|
||||
throw _mapper.fromUnknown(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshUserInfo() async {
|
||||
final current = state;
|
||||
if (current is! AuthAuthenticated) return;
|
||||
final gen = _generation;
|
||||
try {
|
||||
final fresh = await ref.read(authRepositoryProvider).fetchUserProfile();
|
||||
if (_generation != gen) return;
|
||||
state = AuthState.authenticated(
|
||||
fresh.copyWith(
|
||||
token: current.user.token,
|
||||
userId: fresh.userId ?? current.user.userId,
|
||||
),
|
||||
);
|
||||
} on Object {
|
||||
// profile 拉取失败不应改变登录状态
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'auth_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AuthState {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthState);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $AuthStateCopyWith<$Res> {
|
||||
$AuthStateCopyWith(AuthState _, $Res Function(AuthState) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AuthState].
|
||||
extension AuthStatePatterns on AuthState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( AuthInitial value)? initial,TResult Function( AuthLoading value)? loading,TResult Function( AuthAuthenticated value)? authenticated,TResult Function( AuthUnauthenticated value)? unauthenticated,TResult Function( AuthError value)? error,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial() when initial != null:
|
||||
return initial(_that);case AuthLoading() when loading != null:
|
||||
return loading(_that);case AuthAuthenticated() when authenticated != null:
|
||||
return authenticated(_that);case AuthUnauthenticated() when unauthenticated != null:
|
||||
return unauthenticated(_that);case AuthError() when error != null:
|
||||
return error(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( AuthInitial value) initial,required TResult Function( AuthLoading value) loading,required TResult Function( AuthAuthenticated value) authenticated,required TResult Function( AuthUnauthenticated value) unauthenticated,required TResult Function( AuthError value) error,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial():
|
||||
return initial(_that);case AuthLoading():
|
||||
return loading(_that);case AuthAuthenticated():
|
||||
return authenticated(_that);case AuthUnauthenticated():
|
||||
return unauthenticated(_that);case AuthError():
|
||||
return error(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( AuthInitial value)? initial,TResult? Function( AuthLoading value)? loading,TResult? Function( AuthAuthenticated value)? authenticated,TResult? Function( AuthUnauthenticated value)? unauthenticated,TResult? Function( AuthError value)? error,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial() when initial != null:
|
||||
return initial(_that);case AuthLoading() when loading != null:
|
||||
return loading(_that);case AuthAuthenticated() when authenticated != null:
|
||||
return authenticated(_that);case AuthUnauthenticated() when unauthenticated != null:
|
||||
return unauthenticated(_that);case AuthError() when error != null:
|
||||
return error(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? initial,TResult Function()? loading,TResult Function( UserEntity user)? authenticated,TResult Function()? unauthenticated,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial() when initial != null:
|
||||
return initial();case AuthLoading() when loading != null:
|
||||
return loading();case AuthAuthenticated() when authenticated != null:
|
||||
return authenticated(_that.user);case AuthUnauthenticated() when unauthenticated != null:
|
||||
return unauthenticated();case AuthError() when error != null:
|
||||
return error(_that.message);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() initial,required TResult Function() loading,required TResult Function( UserEntity user) authenticated,required TResult Function() unauthenticated,required TResult Function( String message) error,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial():
|
||||
return initial();case AuthLoading():
|
||||
return loading();case AuthAuthenticated():
|
||||
return authenticated(_that.user);case AuthUnauthenticated():
|
||||
return unauthenticated();case AuthError():
|
||||
return error(_that.message);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? initial,TResult? Function()? loading,TResult? Function( UserEntity user)? authenticated,TResult? Function()? unauthenticated,TResult? Function( String message)? error,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AuthInitial() when initial != null:
|
||||
return initial();case AuthLoading() when loading != null:
|
||||
return loading();case AuthAuthenticated() when authenticated != null:
|
||||
return authenticated(_that.user);case AuthUnauthenticated() when unauthenticated != null:
|
||||
return unauthenticated();case AuthError() when error != null:
|
||||
return error(_that.message);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class AuthInitial implements AuthState {
|
||||
const AuthInitial();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthInitial);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState.initial()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class AuthLoading implements AuthState {
|
||||
const AuthLoading();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthLoading);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState.loading()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class AuthAuthenticated implements AuthState {
|
||||
const AuthAuthenticated(this.user);
|
||||
|
||||
|
||||
final UserEntity user;
|
||||
|
||||
/// Create a copy of AuthState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AuthAuthenticatedCopyWith<AuthAuthenticated> get copyWith => _$AuthAuthenticatedCopyWithImpl<AuthAuthenticated>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthAuthenticated&&(identical(other.user, user) || other.user == user));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,user);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState.authenticated(user: $user)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AuthAuthenticatedCopyWith<$Res> implements $AuthStateCopyWith<$Res> {
|
||||
factory $AuthAuthenticatedCopyWith(AuthAuthenticated value, $Res Function(AuthAuthenticated) _then) = _$AuthAuthenticatedCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
UserEntity user
|
||||
});
|
||||
|
||||
|
||||
$UserEntityCopyWith<$Res> get user;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AuthAuthenticatedCopyWithImpl<$Res>
|
||||
implements $AuthAuthenticatedCopyWith<$Res> {
|
||||
_$AuthAuthenticatedCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AuthAuthenticated _self;
|
||||
final $Res Function(AuthAuthenticated) _then;
|
||||
|
||||
/// Create a copy of AuthState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? user = null,}) {
|
||||
return _then(AuthAuthenticated(
|
||||
null == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
|
||||
as UserEntity,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of AuthState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$UserEntityCopyWith<$Res> get user {
|
||||
|
||||
return $UserEntityCopyWith<$Res>(_self.user, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class AuthUnauthenticated implements AuthState {
|
||||
const AuthUnauthenticated();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthUnauthenticated);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState.unauthenticated()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class AuthError implements AuthState {
|
||||
const AuthError(this.message);
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
/// Create a copy of AuthState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AuthErrorCopyWith<AuthError> get copyWith => _$AuthErrorCopyWithImpl<AuthError>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthError&&(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AuthState.error(message: $message)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AuthErrorCopyWith<$Res> implements $AuthStateCopyWith<$Res> {
|
||||
factory $AuthErrorCopyWith(AuthError value, $Res Function(AuthError) _then) = _$AuthErrorCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AuthErrorCopyWithImpl<$Res>
|
||||
implements $AuthErrorCopyWith<$Res> {
|
||||
_$AuthErrorCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AuthError _self;
|
||||
final $Res Function(AuthError) _then;
|
||||
|
||||
/// Create a copy of AuthState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
|
||||
return _then(AuthError(
|
||||
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(AuthNotifier)
|
||||
final authProvider = AuthNotifierProvider._();
|
||||
|
||||
final class AuthNotifierProvider
|
||||
extends $NotifierProvider<AuthNotifier, AuthState> {
|
||||
AuthNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AuthNotifier create() => AuthNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authNotifierHash() => r'40b3d6e25632667d1c904bec4fd347ac6b32848c';
|
||||
|
||||
abstract class _$AuthNotifier extends $Notifier<AuthState> {
|
||||
AuthState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<AuthState, AuthState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AuthState, AuthState>,
|
||||
AuthState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart' show ValueNotifier;
|
||||
import 'package:sunny_mochi/core/observability/talker_setup.dart';
|
||||
import 'package:sunny_mochi/core/storage/secure_storage.dart'
|
||||
show secureStorageProvider;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'auth_status_provider.g.dart';
|
||||
|
||||
/// 同步可读的登录态门面 — 给 GoRouter `redirect` 用。
|
||||
///
|
||||
/// 三态语义:
|
||||
/// - `null` → 启动期未知(异步 storage 还没读完,redirect 应不动)
|
||||
/// - `true` → 已登录
|
||||
/// - `false` → 未登录(redirect 把非 /login 的访问跳 /login)
|
||||
class AuthStatusController {
|
||||
AuthStatusController() : _notifier = ValueNotifier<bool?>(null);
|
||||
|
||||
final ValueNotifier<bool?> _notifier;
|
||||
|
||||
ValueNotifier<bool?> get listenable => _notifier;
|
||||
|
||||
bool? get value => _notifier.value;
|
||||
|
||||
void markLoggedIn() {
|
||||
appTalker.info('[AuthStatus] markLoggedIn');
|
||||
_notifier.value = true;
|
||||
}
|
||||
|
||||
void markLoggedOut() {
|
||||
appTalker.info('[AuthStatus] markLoggedOut');
|
||||
_notifier.value = false;
|
||||
}
|
||||
|
||||
void dispose() => _notifier.dispose();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AuthStatusController authStatus(Ref ref) {
|
||||
final controller = AuthStatusController();
|
||||
unawaited(_bootstrap(ref, controller));
|
||||
ref.onDispose(controller.dispose);
|
||||
return controller;
|
||||
}
|
||||
|
||||
Future<void> _bootstrap(Ref ref, AuthStatusController controller) async {
|
||||
try {
|
||||
final token = await ref.read(secureStorageProvider).getToken();
|
||||
final loggedIn = token != null && token.isNotEmpty;
|
||||
if (loggedIn) {
|
||||
controller.markLoggedIn();
|
||||
} else {
|
||||
controller.markLoggedOut();
|
||||
}
|
||||
} on Object catch (e, st) {
|
||||
appTalker.warning('[AuthStatus] bootstrap 异常 → 视为未登录', e, st);
|
||||
controller.markLoggedOut();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_status_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(authStatus)
|
||||
final authStatusProvider = AuthStatusProvider._();
|
||||
|
||||
final class AuthStatusProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AuthStatusController,
|
||||
AuthStatusController,
|
||||
AuthStatusController
|
||||
>
|
||||
with $Provider<AuthStatusController> {
|
||||
AuthStatusProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authStatusProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authStatusHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AuthStatusController> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AuthStatusController create(Ref ref) {
|
||||
return authStatus(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthStatusController value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthStatusController>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authStatusHash() => r'ef142e4b7c9544c953e6da320fd43f0ee020da53';
|
||||
Reference in New Issue
Block a user