perf(startup): bootstrap persisted app state

This commit is contained in:
zarzet
2026-07-16 08:08:27 +07:00
parent 10d59bc2b6
commit 5a970ac83d
4 changed files with 109 additions and 21 deletions
+70 -18
View File
@@ -11,6 +11,7 @@ import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/services/notification_service.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/services/share_intent_service.dart';
@@ -37,7 +38,10 @@ void main() {
return true;
};
final runtimeProfile = await _resolveRuntimeProfile();
final prefs = await SharedPreferences.getInstance();
final bootstrapSettings = loadBootstrapSettings(prefs);
final bootstrapTheme = loadBootstrapThemeSettings(prefs);
final runtimeProfile = await _resolveRuntimeProfile(prefs);
_configureImageCache(runtimeProfile);
runApp(
@@ -46,6 +50,11 @@ void main() {
lowEndDeviceProvider.overrideWithValue(
runtimeProfile.disableOverscrollEffects,
),
backdropBlurEnabledProvider.overrideWithValue(
runtimeProfile.enableBackdropBlur,
),
initialSettingsProvider.overrideWithValue(bootstrapSettings),
initialThemeSettingsProvider.overrideWithValue(bootstrapTheme),
],
child: _EagerInitialization(
child: SpotiFLACApp(
@@ -61,14 +70,21 @@ void main() {
);
}
Future<_RuntimeProfile> _resolveRuntimeProfile() async {
const defaults = _RuntimeProfile(
imageCacheMaximumSize: 240,
imageCacheMaximumSizeBytes: 60 << 20,
disableOverscrollEffects: false,
);
const _runtimeProfileTierKey = 'runtime_profile_tier_v1';
if (!Platform.isAndroid) return defaults;
Future<_RuntimeProfile> _resolveRuntimeProfile(SharedPreferences prefs) async {
final cachedTier = prefs.getString(_runtimeProfileTierKey);
if (cachedTier != null) {
final cached = _RuntimeProfile.fromTier(cachedTier);
if (cached != null) return cached;
}
const defaults = _RuntimeProfile.standard();
if (!Platform.isAndroid) {
await prefs.setString(_runtimeProfileTierKey, defaults.tier);
return defaults;
}
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
@@ -76,15 +92,13 @@ Future<_RuntimeProfile> _resolveRuntimeProfile() async {
final isLowRamDevice =
androidInfo.isLowRamDevice || androidInfo.physicalRamSize <= 2500;
if (!isArm32Only && !isLowRamDevice) {
return defaults;
}
return _RuntimeProfile(
imageCacheMaximumSize: 120,
imageCacheMaximumSizeBytes: 24 << 20,
disableOverscrollEffects: true,
);
final profile = (isArm32Only || isLowRamDevice)
? const _RuntimeProfile.low()
: androidInfo.physicalRamSize >= 6000
? const _RuntimeProfile.high()
: defaults;
await prefs.setString(_runtimeProfileTierKey, profile.tier);
return profile;
} catch (e) {
debugPrint('Failed to resolve runtime profile: $e');
return defaults;
@@ -100,15 +114,53 @@ void _configureImageCache(_RuntimeProfile runtimeProfile) {
}
class _RuntimeProfile {
final String tier;
final int imageCacheMaximumSize;
final int imageCacheMaximumSizeBytes;
final bool disableOverscrollEffects;
final bool enableBackdropBlur;
const _RuntimeProfile({
const _RuntimeProfile._({
required this.tier,
required this.imageCacheMaximumSize,
required this.imageCacheMaximumSizeBytes,
required this.disableOverscrollEffects,
required this.enableBackdropBlur,
});
const _RuntimeProfile.low()
: this._(
tier: 'low',
imageCacheMaximumSize: 120,
imageCacheMaximumSizeBytes: 24 << 20,
disableOverscrollEffects: true,
enableBackdropBlur: false,
);
const _RuntimeProfile.standard()
: this._(
tier: 'standard',
imageCacheMaximumSize: 240,
imageCacheMaximumSizeBytes: 60 << 20,
disableOverscrollEffects: false,
enableBackdropBlur: false,
);
const _RuntimeProfile.high()
: this._(
tier: 'high',
imageCacheMaximumSize: 320,
imageCacheMaximumSizeBytes: 80 << 20,
disableOverscrollEffects: false,
enableBackdropBlur: true,
);
static _RuntimeProfile? fromTier(String tier) => switch (tier) {
'low' => const _RuntimeProfile.low(),
'standard' => const _RuntimeProfile.standard(),
'high' => const _RuntimeProfile.high(),
_ => null,
};
}
class _EagerInitialization extends ConsumerStatefulWidget {
@@ -4,3 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
/// main.dart via a ProviderScope override). UI uses it to skip expensive
/// effects like the shell's backdrop blur.
final lowEndDeviceProvider = Provider<bool>((ref) => false);
/// Generic visual capability decided by the startup runtime profile. Effects
/// can consume this without hardcoding a device model or platform source.
final backdropBlurEnabledProvider = Provider<bool>((ref) => false);
+21 -2
View File
@@ -19,6 +19,25 @@ const _spotifyClientSecretKey = 'spotify_client_secret';
const _retiredBuiltInProviderIds = {'deezer', 'qobuz', 'tidal', 'youtube'};
final _log = AppLogger('SettingsProvider');
/// Startup override used to render the correct route/locale on the first
/// frame. The notifier still performs migrations and full validation after it
/// mounts.
final initialSettingsProvider = Provider<AppSettings>(
(ref) => const AppSettings(),
);
AppSettings loadBootstrapSettings(SharedPreferences prefs) {
final rawSettings = prefs.getString(_settingsKey);
if (rawSettings == null || rawSettings.isEmpty) return const AppSettings();
try {
final decoded = jsonDecode(rawSettings);
if (decoded is! Map) return const AppSettings();
return AppSettings.fromJson(Map<String, dynamic>.from(decoded));
} catch (_) {
return const AppSettings();
}
}
class SettingsNotifier extends Notifier<AppSettings> {
static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$');
static const Set<String> _searchTabValues = {
@@ -41,8 +60,8 @@ class SettingsNotifier extends Notifier<AppSettings> {
@override
AppSettings build() {
_loadSettings();
return const AppSettings();
unawaited(_loadSettings());
return ref.read(initialSettingsProvider);
}
Future<void> _loadSettings() async {
+14 -1
View File
@@ -3,6 +3,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
final initialThemeSettingsProvider = Provider<ThemeSettings>(
(ref) => const ThemeSettings(),
);
ThemeSettings loadBootstrapThemeSettings(SharedPreferences prefs) {
return ThemeSettings(
themeMode: themeModeFromString(prefs.getString(kThemeModeKey)),
useDynamicColor: prefs.getBool(kUseDynamicColorKey) ?? true,
seedColorValue: prefs.getInt(kSeedColorKey) ?? kDefaultSeedColor,
useAmoled: prefs.getBool(kUseAmoledKey) ?? false,
);
}
final themeProvider = NotifierProvider<ThemeNotifier, ThemeSettings>(() {
return ThemeNotifier();
});
@@ -13,7 +26,7 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
@override
ThemeSettings build() {
_loadFromStorage();
return const ThemeSettings();
return ref.read(initialThemeSettingsProvider);
}
Future<void> _loadFromStorage() async {