summaryrefslogtreecommitdiff
path: root/player-android/lib/providers
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-20 23:42:34 +0300
committerPaul Buetow <paul@buetow.org>2026-05-20 23:42:34 +0300
commitef0c310211d180d57f060aca3802bfed0958d312 (patch)
tree7c3fedd84931dcc97aa7b27206c3797f38c32325 /player-android/lib/providers
parent1a68b1aead492b50d824bde99fb1f30e1360ed26 (diff)
Add go_router + Riverpod wiring and extract SOLID-clean screen/nav layers (na)
Introduces flutter_riverpod and go_router dependencies. Wires up the app with a Riverpod-managed GoRouter, auth-guarded redirect logic, and a shared navigator key used by DioClient for 401 → /login redirects. SOLID fixes applied: - navigatorKey extracted to navigation_key.dart (DIP: breaks cross-layer import from router.dart into api_client_provider.dart) - AppRoutes extracted to app_routes.dart (SRP + avoids circular import when screen files reference route constants) - LoginScreen, HomeScreen, MediaDetailScreen, ShareScreen each moved to their own file under lib/screens/ (SRP: router.dart is routing-only) - router.dart re-exports AppRoutes for backward-compatible callers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'player-android/lib/providers')
-rw-r--r--player-android/lib/providers/api_client_provider.dart45
-rw-r--r--player-android/lib/providers/auth_state_provider.dart93
2 files changed, 138 insertions, 0 deletions
diff --git a/player-android/lib/providers/api_client_provider.dart b/player-android/lib/providers/api_client_provider.dart
new file mode 100644
index 0000000..6ca177f
--- /dev/null
+++ b/player-android/lib/providers/api_client_provider.dart
@@ -0,0 +1,45 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../api/dio_client.dart';
+import '../api/player_api_client.dart';
+import '../navigation_key.dart';
+
+/// Base URL for the player-server API.
+///
+/// In production this is injected from the environment or a config file.
+/// The default points to a local dev instance so the app is runnable
+/// without extra configuration.
+const _kBaseUrl = String.fromEnvironment(
+ 'PLAYER_BASE_URL',
+ defaultValue: 'http://10.0.2.2:8080',
+);
+
+/// Provides the production [TokenStorage] backed by the OS keychain.
+///
+/// Riverpod keeps a single instance for the lifetime of [ProviderScope], so
+/// there is exactly one [SecureTokenStorage] in the app — consistent with the
+/// singleton intent of flutter_secure_storage.
+final tokenStorageProvider = Provider<TokenStorage>((ref) {
+ return SecureTokenStorage();
+});
+
+/// Provides a fully configured [PlayerApiClient] wired with bearer-token
+/// injection and global 401 → /login redirect.
+///
+/// Depends on [tokenStorageProvider] and [navigatorKey] (both singletons) so
+/// the same [Dio] instance is reused for every call site — avoiding redundant
+/// interceptor stacks.
+final apiClientProvider = Provider<PlayerApiClient>((ref) {
+ final storage = ref.watch(tokenStorageProvider);
+
+ final dioClient = DioClient(
+ baseUrl: Uri.parse(_kBaseUrl),
+ storage: storage,
+ // Share the navigator key with go_router so 401 redirects go through the
+ // correct router instance rather than the raw Navigator.
+ navigatorKey: navigatorKey,
+ loginRoute: '/login',
+ );
+
+ return PlayerApiClient(dio: dioClient.dio);
+});
diff --git a/player-android/lib/providers/auth_state_provider.dart b/player-android/lib/providers/auth_state_provider.dart
new file mode 100644
index 0000000..0e0c6fb
--- /dev/null
+++ b/player-android/lib/providers/auth_state_provider.dart
@@ -0,0 +1,93 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'api_client_provider.dart';
+
+/// All possible authentication states for the app.
+///
+/// Using a sealed-like enum keeps the router redirect logic exhaustive and
+/// avoids stringly-typed checks throughout the codebase.
+enum AuthStatus {
+ /// Initial state while the app checks whether a stored token exists.
+ loading,
+
+ /// A valid token was found in secure storage; the user is logged in.
+ authenticated,
+
+ /// No token exists or it has been purged (e.g. after a 401 response).
+ unauthenticated,
+}
+
+/// Immutable snapshot of the auth state passed through the provider graph.
+///
+/// Keeping this as a value object (rather than a mutable notifier field)
+/// makes it safe to pass into go_router's redirect callback and to compare
+/// with `==` in tests.
+class AuthState {
+ const AuthState({required this.status});
+
+ final AuthStatus status;
+
+ /// Convenience constructors reduce noise at call sites.
+ const AuthState.loading() : status = AuthStatus.loading;
+ const AuthState.authenticated() : status = AuthStatus.authenticated;
+ const AuthState.unauthenticated() : status = AuthStatus.unauthenticated;
+
+ bool get isLoading => status == AuthStatus.loading;
+ bool get isAuthenticated => status == AuthStatus.authenticated;
+ bool get isUnauthenticated => status == AuthStatus.unauthenticated;
+
+ @override
+ String toString() => 'AuthState(${status.name})';
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is AuthState &&
+ runtimeType == other.runtimeType &&
+ status == other.status;
+
+ @override
+ int get hashCode => status.hashCode;
+}
+
+/// Notifier that owns the mutable [AuthState] and exposes mutation methods
+/// for login / logout.
+///
+/// [AsyncNotifier] is used because the initial state check is async (it reads
+/// the secure token store). Downstream consumers can call [login] and
+/// [logout] to drive route redirects via the router's [refreshListenable].
+class AuthStateNotifier extends AsyncNotifier<AuthState> {
+ @override
+ Future<AuthState> build() async {
+ // Determine whether a token already exists on app startup. This drives
+ // the initial route decision inside the go_router redirect callback.
+ final storage = ref.read(tokenStorageProvider);
+ final token = await storage.readToken();
+
+ return token != null && token.isNotEmpty
+ ? const AuthState.authenticated()
+ : const AuthState.unauthenticated();
+ }
+
+ /// Called after a successful login; persists [token] and updates state.
+ Future<void> login(String token) async {
+ final storage = ref.read(tokenStorageProvider);
+ await storage.writeToken(token);
+ state = const AsyncData(AuthState.authenticated());
+ }
+
+ /// Called on explicit logout or after [_UnauthorizedInterceptor] purges the
+ /// token. Clears the stored token and moves to the unauthenticated state.
+ Future<void> logout() async {
+ final storage = ref.read(tokenStorageProvider);
+ await storage.deleteToken();
+ state = const AsyncData(AuthState.unauthenticated());
+ }
+}
+
+/// The single source of truth for authentication status, consumed by the
+/// router's redirect callback and any widget that needs to gate on auth.
+final authStateProvider =
+ AsyncNotifierProvider<AuthStateNotifier, AuthState>(
+ AuthStateNotifier.new,
+);