Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Created June 17, 2023 15:58
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PlugFox/e58a773a84e815f817ddd75f95c7b263 to your computer and use it in GitHub Desktop.
Save PlugFox/e58a773a84e815f817ddd75f95c7b263 to your computer and use it in GitHub Desktop.
/// Dependencies
abstract interface class Dependencies {
/// The state from the closest instance of this class.
factory Dependencies.of(BuildContext context) => InheritedDependencies.of(context);
/// App metadata
abstract final AppMetadata appMetadata;
/// Database
abstract final Database database;
/// Authentication controller
abstract final AuthenticationController authenticationController;
/// Settings controller
abstract final SettingsController settingsController;
/// Cloud repository
abstract final IMyRepository myRepository;
}
final class $MutableDependencies implements Dependencies {
$MutableDependencies() : context = <String, Object?>{};
/// Initialization context
final Map<Object?, Object?> context;
@override
late AppMetadata appMetadata;
@override
late Database database;
@override
late AuthenticationController authenticationController;
@override
late SettingsController settingsController;
@override
late IMyRepository myRepository;
Dependencies freeze() => _$ImmutableDependencies(
appMetadata: appMetadata,
database: database,
authenticationController: authenticationController,
settingsController: settingsController,
myRepository: myRepository,
);
}
final class _$ImmutableDependencies implements Dependencies {
_$ImmutableDependencies({
required this.appMetadata,
required this.database,
required this.authenticationController,
required this.settingsController,
required this.myRepository,
});
@override
final AppMetadata appMetadata;
@override
final Database database;
@override
final AuthenticationController authenticationController;
@override
final SettingsController settingsController;
@override
final IMyRepository myRepository;
}
import 'package:flutter/material.dart';
import 'package:project/src/common/model/dependencies.dart';
/// {@template inherited_dependencies}
/// InheritedDependencies widget.
/// {@endtemplate}
class InheritedDependencies extends InheritedWidget {
/// {@macro inherited_dependencies}
const InheritedDependencies({
required this.dependencies,
required super.child,
super.key,
});
final Dependencies dependencies;
/// The state from the closest instance of this class
/// that encloses the given context, if any.
/// e.g. `InheritedDependencies.maybeOf(context)`.
static Dependencies? maybeOf(BuildContext context) =>
(context.getElementForInheritedWidgetOfExactType<InheritedDependencies>()?.widget as InheritedDependencies?)
?.dependencies;
static Never _notFoundInheritedWidgetOfExactType() => throw ArgumentError(
'Out of scope, not found inherited widget '
'a InheritedDependencies of the exact type',
'out_of_scope',
);
/// The state from the closest instance of this class
/// that encloses the given context.
/// e.g. `InheritedDependencies.of(context)`
static Dependencies of(BuildContext context) => maybeOf(context) ?? _notFoundInheritedWidgetOfExactType();
@override
bool updateShouldNotify(InheritedDependencies oldWidget) => false;
}
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:project/src/common/model/dependencies.dart';
import 'package:project/src/feature/initialization/data/initialize_dependencies.dart';
/// Ephemerally initializes the app and prepares it for use.
Future<Dependencies>? _$initializeApp;
/// Initializes the app and prepares it for use.
Future<Dependencies> $initializeApp({
void Function(int progress, String message)? onProgress,
FutureOr<void> Function(Dependencies dependencies)? onSuccess,
void Function(Object error, StackTrace stackTrace)? onError,
}) =>
_$initializeApp ??= Future<Dependencies>(() async {
late final WidgetsBinding binding;
final stopwatch = Stopwatch()..start();
try {
binding = WidgetsFlutterBinding.ensureInitialized()..deferFirstFrame();
/* await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]); */
await _catchExceptions();
final dependencies = await $initializeDependencies(onProgress: onProgress).timeout(const Duration(minutes: 5));
await onSuccess?.call(dependencies);
return dependencies;
} on Object catch (error, stackTrace) {
onError?.call(error, stackTrace);
ErrorUtil.logError(error, stackTrace, hint: 'Failed to initialize app').ignore();
rethrow;
} finally {
stopwatch.stop();
binding.addPostFrameCallback((_) {
// Closes splash screen, and show the app layout.
binding.allowFirstFrame();
//final context = binding.renderViewElement;
});
_$initializeApp = null;
}
});
/// Resets the app's state to its initial state.
@visibleForTesting
Future<void> $resetApp(Dependencies dependencies) async {}
/// Disposes the app and releases all resources.
@visibleForTesting
Future<void> $disposeApp(Dependencies dependencies) async {}
Future<void> _catchExceptions() async {
try {
PlatformDispatcher.instance.onError = (error, stackTrace) {
ErrorUtil.logError(
error,
stackTrace,
hint: 'ROOT ERROR\r\n${Error.safeToString(error)}',
).ignore();
return true;
};
final sourceFlutterError = FlutterError.onError;
FlutterError.onError = (final details) {
ErrorUtil.logError(
details.exception,
details.stack ?? StackTrace.current,
hint: 'FLUTTER ERROR\r\n$details',
).ignore();
// FlutterError.presentError(details);
sourceFlutterError?.call(details);
};
} on Object catch (error, stackTrace) {
ErrorUtil.logError(error, stackTrace).ignore();
}
}
/// Initializes the app and returns a [Dependencies] object
Future<Dependencies> $initializeDependencies({
void Function(int progress, String message)? onProgress,
}) async {
final dependencies = $MutableDependencies();
final totalSteps = _initializationSteps.length;
var currentStep = 0;
for (final step in _initializationSteps.entries) {
currentStep++;
final percent = (currentStep * 100 ~/ totalSteps).clamp(0, 100);
onProgress?.call(percent, step.key);
l.v6('Initialization | $currentStep/$totalSteps ($percent%) | "${step.key}"');
await step.value(dependencies);
}
return dependencies.freeze();
}
typedef _InitializationStep = FutureOr<void> Function($MutableDependencies dependencies);
final Map<String, _InitializationStep> _initializationSteps = <String, _InitializationStep>{
'Platform pre-initialization': (_) => $platformInitialization(),
'Creating app metadata': (dependencies) => dependencies.appMetadata = AppMetadata(
isWeb: platform.isWeb,
isRelease: platform.buildMode.isRelease,
appName: pubspec.name,
appVersion: pubspec.version,
appVersionMajor: pubspec.major,
appVersionMinor: pubspec.minor,
appVersionPatch: pubspec.patch,
appBuildTimestamp: pubspec.build.isNotEmpty ? (int.tryParse(pubspec.build.firstOrNull ?? '-1') ?? -1) : -1,
operatingSystem: platform.operatingSystem.name,
processorsCount: platform.numberOfProcessors,
appLaunchedTimestamp: DateTime.now(),
locale: platform.locale,
deviceVersion: platform.version,
deviceScreenSize: ScreenUtil.screenSize().representation,
),
'Observer state managment': (_) => Controller.observer = ControllerObserver(),
'Initializing analytics': (_) async {/* ... */},
'Log app open': (_) {},
'Get remote config': (_) async {/* ... */},
'Preparing secure storage': (dependencies) =>
dependencies.context['SECURE_STORAGE'] = const fss.FlutterSecureStorage(),
'Initializing the database': (dependencies) => dependencies.database = Database.lazy(),
'Shrink database': (dependencies) async {
if (!Config.environment.isProduction) {
await dependencies.database.transaction(() async {
final log = await (dependencies.database.select<LogTbl, LogTblData>(dependencies.database.logTbl)
..orderBy([(tbl) => OrderingTerm(expression: tbl.id, mode: OrderingMode.desc)])
..limit(1, offset: 1000))
.getSingleOrNull();
if (log != null) {
await (dependencies.database.delete(dependencies.database.logTbl)
..where((tbl) => tbl.time.isSmallerOrEqualValue(log.time)))
.go();
}
});
}
if (DateTime.now().second % 10 == 0) await dependencies.database.customStatement('VACUUM;');
},
'Refresh key value storage': (dependencies) => dependencies.database.refresh(),
'Restore settings': (dependencies) async {/* ... */},
'Migrate app from previous version': (dependencies) => AppMigrator.migrate(dependencies.database),
'Collect logs': (dependencies) async {
if (Config.environment.isProduction) return;
await (dependencies.database.select<LogTbl, LogTblData>(dependencies.database.logTbl)
..orderBy([(tbl) => OrderingTerm(expression: tbl.time, mode: OrderingMode.desc)])
..limit(LogBuffer.bufferLimit))
.get()
.then<List<LogMessage>>((logs) => logs
.map((l) => l.stack != null
? LogMessageWithStackTrace(
date: DateTime.fromMillisecondsSinceEpoch(l.time * 1000),
level: LogLevel.fromValue(l.level),
message: l.message,
stackTrace: StackTrace.fromString(l.stack!))
: LogMessage(
date: DateTime.fromMillisecondsSinceEpoch(l.time * 1000),
level: LogLevel.fromValue(l.level),
message: l.message,
))
.toList())
.then<void>(LogBuffer.instance.addAll);
l.bufferTime(const Duration(seconds: 1)).where((logs) => logs.isNotEmpty).listen(LogBuffer.instance.addAll);
l
.map<LogTblCompanion>((log) => LogTblCompanion.insert(
level: log.level.level,
message: log.message.toString(),
time: Value<int>(log.date.millisecondsSinceEpoch ~/ 1000),
stack: Value<String?>(switch (log) { LogMessageWithStackTrace l => l.stackTrace.toString(), _ => null }),
))
.bufferTime(const Duration(seconds: 15))
.where((logs) => logs.isNotEmpty)
.listen(
(logs) =>
dependencies.database.batch((batch) => batch.insertAll(dependencies.database.logTbl, logs)).ignore(),
cancelOnError: false,
);
},
'Log app initialized': (_) {},
};
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:l/l.dart';
import 'package:project/src/common/widget/app.dart';
import 'package:project/src/common/widget/app_error.dart';
import 'package:project/src/feature/initialization/data/initialization.dart';
import 'package:project/src/feature/initialization/widget/inherited_dependencies.dart';
import 'package:project/src/feature/initialization/widget/initialization_splash_screen.dart';
/// Entry point of the app.
/// Initializes the app and prepares it for use.
void main() => l.capture<void>(
() => runZonedGuarded<void>(
() async {
// Splash screen
final initializationProgress = ValueNotifier<({int progress, String message})>((progress: 0, message: ''));
runApp(InitializationSplashScreen(progress: initializationProgress));
$initializeApp(
onProgress: (progress, message) => initializationProgress.value = (progress: progress, message: message),
onSuccess: (dependencies) => runApp(
InheritedDependencies(
dependencies: dependencies,
child: App(),
),
),
onError: (error, stackTrace) {
runApp(AppError(message: ErrorUtil.formatMessage(error)));
ErrorUtil.logError(error, stackTrace).ignore();
},
).ignore();
},
l.e,
),
const LogOptions(
handlePrint: true,
messageFormatting: _messageFormatting,
outputInRelease: false,
printColors: true,
),
);
/// Formats the log message.
Object _messageFormatting(Object message, LogLevel logLevel, DateTime now) => '${_timeFormat(now)} | $message';
/// Formats the time.
String _timeFormat(DateTime time) => '${time.hour}:${time.minute.toString().padLeft(2, '0')}';
@yehorh
Copy link

yehorh commented Nov 16, 2023

Очень понравилась идея вызывать на SplashScreen runApp отдельно без инициализации роутера и всего подобного 👍.
Понравилось, но что-то столкнулся с тем что при переходу по url PlatformDispatcher.defaultRouteName не подхватывается новым роутером.
Пока не нашел в чем проблема, пробовал указывать restorationScopeId не помогло.

Так же в примере runApp вызывается до остановки первого кадра.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment