main function
Implementation
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
LicenseRegistry.addLicense(() async* {
final license = await rootBundle.loadString('assets/fonts/OFL.txt');
yield LicenseEntryWithLineBreaks(['Noto Sans TC'], license);
});
// TODO: Remove orientation restriction after responsive layouts are complete.
await SystemChrome.setPreferredOrientations([
.portraitUp,
]);
if (useFirebase) {
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} catch (e) {
log(e.toString(), name: 'Firebase Initialization');
}
}
final container = ProviderContainer();
final pendingWidgetRoute = await container
.read(courseWidgetPlatformProvider)
.takePendingRoute();
container.read(pendingAppRouteProvider.notifier).set(pendingWidgetRoute);
void showErrorDialog(
Object error, {
ErrorType type = .unknown,
StackTrace? stackTrace,
}) {
final rootContext = rootNavigatorKey.currentContext;
if (rootContext == null) return;
final errorMessage = error.toString();
final copyText = [
errorMessage,
if (stackTrace != null) stackTrace.toString(),
].join('\n');
final errorTitle = switch (type) {
.flutter => t.errors.flutterError.spaced,
.async => t.errors.asyncError,
.unknown => t.errors.occurred,
};
showDialog(
context: rootContext,
builder: (dialogContext) => AlertDialog(
title: Text(errorTitle),
// TODO: Remove technical details from user-facing error messages
content: SelectableText(errorMessage),
actions: [
TextButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: copyText));
if (!dialogContext.mounted) return;
Navigator.of(dialogContext).pop();
rootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(content: Text(t.general.copied)),
);
},
child: Text(t.general.copy),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(t.general.ok),
),
],
),
);
}
void showErrorSnackBar(Object error) {
final message = isNetworkError(error)
? t.errors.networkError
: t.errors.unexpected;
rootScaffoldMessengerKey.currentState
?..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(message),
),
);
}
Future<bool> shouldShowErrorDialog() async {
try {
return await container
.read(preferencesRepositoryProvider)
.get(.showErrorDialog);
} catch (e) {
log('Failed to resolve error display preference: $e');
return PrefKey.showErrorDialog.defaultValue;
}
}
Future<void> handleUncaughtError(
Object error, {
ErrorType type = .unknown,
StackTrace? stackTrace,
}) async {
final showDialog = await shouldShowErrorDialog();
if (showDialog) {
showErrorDialog(error, type: type, stackTrace: stackTrace);
} else {
showErrorSnackBar(error);
}
}
// Network failures are expected when the device cannot reach an external
// service and should not be reported as Crashlytics fatal errors.
FlutterError.onError = (details) {
if (shouldReportToCrashlytics(details.exception)) {
firebaseService.crashlytics?.recordFlutterFatalError(details);
}
FlutterError.dumpErrorToConsole(details);
WidgetsBinding.instance.addPostFrameCallback((_) {
handleUncaughtError(
details.exception,
type: .flutter,
stackTrace: details.stack,
);
});
};
// Pass unexpected uncaught asynchronous errors to Crashlytics.
PlatformDispatcher.instance.onError = (error, stack) {
if (shouldReportToCrashlytics(error)) {
firebaseService.crashlytics?.recordError(error, stack, fatal: true);
}
log('Uncaught asynchronous error: $error', stackTrace: stack);
handleUncaughtError(error, type: .async, stackTrace: stack);
return true;
};
firebaseService.analytics?.logAppOpen();
// Restore the platform language selection before mounting the app.
await container.read(languageRepositoryProvider).restore();
// Initialize Remote Config and preference defaults
final preferencesRepository = container.read(preferencesRepositoryProvider);
await preferencesRepository.init();
// Run force-update check and wire Remote Config live updates.
await UpdateService.init(container);
final database = container.read(databaseProvider);
var user = await database.select(database.users).getSingleOrNull();
var hadStoredLogin = false;
if (user == null) {
final restoration = await container
.read(authRepositoryProvider)
.restoreSession();
user = restoration.user;
hadStoredLogin = restoration.hadStoredLogin;
}
if (user != null) {
// Restore demo mode if the stored user is the demo account
if (user.studentId == demoUsername) {
container.read(isDemoProvider.notifier).set(true);
}
// Idempotent: restoreSession's login already created the session.
container.read(sessionProvider.notifier).create();
}
// Prewarm the dependency graph before widgets subscribe. Riverpod cannot
// invalidate its root scope while Flutter is mounting the initial frame.
container.read(courseRepositoryProvider);
final landingLocation = await resolveLandingLocation(
preferencesRepository,
);
final authenticatedInitialLocation = user == null
? landingLocation
: container.read(pendingAppRouteProvider) ?? landingLocation;
final initialLocation = switch ((user, hadStoredLogin)) {
(final User _, _) => authenticatedInitialLocation,
(null, true) => AppRoutes.login,
(null, false) => AppRoutes.intro,
};
final router = createAppRouter(
initialLocation: initialLocation,
landingLocation: authenticatedInitialLocation,
container: container,
);
runApp(
UncontrolledProviderScope(
container: container,
child: TranslationProvider(
child: MyApp(router: router),
),
),
);
}