summaryrefslogtreecommitdiff
path: root/player-android/lib/providers/settings_provider.dart
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,
);