blob: 8ffec0f2c1799efa366db996473cd5a4c8a05d41 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
// SharedPreferences key for the server base URL setting.
const _kBaseUrlKey = 'server_base_url';
// Default base URL used when the user has not yet configured one.
// Points to the local Android emulator loopback address so the app is
// runnable out-of-the-box without any manual configuration.
// Private: only referenced within this file; callers read the resolved URL
// through [AppSettings.serverBaseUrl] obtained from [settingsProvider].
const _kDefaultBaseUrl = 'http://10.0.2.2:8080';
/// Immutable snapshot of persisted app settings.
///
/// Keeping settings as a value object means every state change produces a new
/// instance, which plays well with Riverpod's equality-based rebuild suppression
/// and keeps the notifier's contract straightforward.
class AppSettings {
const AppSettings({required this.serverBaseUrl});
/// The base URL of the player-server API (e.g. "https://player.example.com").
final String serverBaseUrl;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettings &&
runtimeType == other.runtimeType &&
serverBaseUrl == other.serverBaseUrl;
@override
int get hashCode => serverBaseUrl.hashCode;
@override
String toString() => 'AppSettings(serverBaseUrl: $serverBaseUrl)';
}
/// Manages persisted app settings via [SharedPreferences].
///
/// Uses [AsyncNotifier] because the initial state load is async (disk read).
/// After initialisation, [setServerBaseUrl] writes to disk and updates state
/// synchronously so the UI reflects changes immediately.
///
/// Design notes (SRP / ISP):
/// - This notifier owns only settings persistence; auth is handled separately
/// by [AuthStateNotifier] to maintain single responsibility.
/// - [SharedPreferences] is created internally rather than injected because
/// it is a platform singleton; tests override the entire provider via
/// [ProviderScope] overrides instead.
class SettingsNotifier extends AsyncNotifier<AppSettings> {
@override
Future<AppSettings> build() async {
// Load persisted settings from disk on first access. The platform
// SharedPreferences instance is a singleton; obtaining it here is cheap
// because subsequent calls return the cached instance.
final prefs = await SharedPreferences.getInstance();
final url = prefs.getString(_kBaseUrlKey) ?? _kDefaultBaseUrl;
return AppSettings(serverBaseUrl: url);
}
/// Persists [url] as the new server base URL and updates the in-memory state.
///
/// The UI calls this when the user edits the URL field and submits. The
/// async write to [SharedPreferences] is awaited so that a subsequent cold
/// start will see the new value; the in-memory state is updated first so the
/// UI is not blocked on the disk write.
Future<void> setServerBaseUrl(String url) async {
// Update in-memory state first for immediate UI feedback.
state = AsyncData(AppSettings(serverBaseUrl: url));
// Persist to disk so the value survives app restarts.
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kBaseUrlKey, url);
}
}
/// The single source of truth for persisted app settings.
///
/// Currently consumed by [SettingsScreen] for displaying and editing settings.
/// Will also be consumed by [apiClientProvider] (for the server base URL) once
/// that provider is wired to read from settings rather than
/// [String.fromEnvironment] — tracked as a future task.
final settingsProvider =
AsyncNotifierProvider<SettingsNotifier, AppSettings>(
SettingsNotifier.new,
);
|