perf(startup): paint a splash immediately, run init off the first frame

main() awaited all of configureDependencies() (open the encrypted DB,
derive the social key, seed/scan the species catalog) before the first
runApp, so nothing painted until init finished — startup jank visible as
skipped frames. Now main() runs runApp immediately with a Bootstrap
widget that shows a native-splash-matching green splash while the heavy
work runs, then swaps in TaneApp. On failure it shows a localized error
with a retry (configureDependencies is idempotent, so retry is safe).

Adds a bootstrap i18n group (en/es/pt/ast) and a widget test for the
splash/error frame.
This commit is contained in:
vjrj 2026-07-10 17:34:08 +02:00
parent 33d8b2a4d7
commit 71413f3801
12 changed files with 289 additions and 53 deletions

View file

@ -1,60 +1,15 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'app.dart';
import 'data/species_repository.dart';
import 'data/variety_repository.dart';
import 'di/injector.dart';
import 'bootstrap.dart';
import 'i18n/strings.g.dart';
import 'services/auto_backup_service.dart';
import 'services/coarse_location.dart';
import 'services/inbox_service.dart';
import 'services/locale_store.dart';
import 'services/message_store.dart';
import 'services/offer_outbox.dart';
import 'services/onboarding_store.dart';
import 'services/profile_cache.dart';
import 'services/profile_store.dart';
import 'services/social_service.dart';
import 'services/social_settings.dart';
Future<void> main() async {
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Start from the device language, then let an explicit in-app choice win
// it's the only reliable way to reach languages the OS picker doesn't offer
// (e.g. Asturian). Restored after DI, since the store lives in the keystore.
// (e.g. Asturian). The saved choice is restored inside [Bootstrap], after DI.
LocaleSettings.useDeviceLocaleSync();
await configureDependencies();
final savedLocale = await getIt<LocaleStore>().saved();
if (savedLocale != null) LocaleSettings.setLocaleSync(savedLocale);
final onboarding = getIt<OnboardingStore>();
final inbox =
getIt.isRegistered<InboxService>() ? getIt<InboxService>() : null;
// Start listening for incoming private messages app-wide (foreground), so
// they arrive and populate the inbox list even before their chat is opened.
if (inbox != null) unawaited(inbox.start());
runApp(
TranslationProvider(
child: TaneApp(
repository: getIt<VarietyRepository>(),
species: getIt<SpeciesRepository>(),
onboarding: onboarding,
social: getIt.isRegistered<SocialService>()
? getIt<SocialService>()
: null,
socialSettings: getIt<SocialSettings>(),
location: const GeolocatorCoarseLocation(),
outbox: getIt<OfferOutbox>(),
messageStore: getIt<MessageStore>(),
profileStore: getIt<ProfileStore>(),
profileCache: getIt<ProfileCache>(),
inbox: inbox,
showIntro: !await onboarding.introSeen(),
autoBackup: getIt.isRegistered<AutoBackupService>()
? getIt<AutoBackupService>()
: null,
),
),
);
// Paint immediately: [Bootstrap] shows a splash and runs the heavy init off
// the first frame, so the app window appears at once instead of after DI.
runApp(TranslationProvider(child: const Bootstrap()));
}