diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fee43c8..4e63334 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,7 +9,7 @@ # Правила проведения Code Review ## Основные правила проведения Code Review -- Комментарии и описание Pull Request должны быть на русском языке. +**ВАЖНО:** - Комментарии, обзоры и описание Pull Request при проведении code review должны быть на РУССКОМ языке. # Стиль кода diff --git a/analysis_options.yaml b/analysis_options.yaml index 4708b49..a75c6ed 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -49,6 +49,7 @@ linter: # === Обработка ошибок и типобезопасность === - avoid_catching_errors # Не перехватывать Error (только Exception) — ошибки системные + - avoid_catches_without_on_clauses # Требует явного указания типа исключения в catch (on Type catch) - await_only_futures # await только для Future (ловит ошибки типизации) - control_flow_in_finally # Запрет return/break/continue в finally блоках - empty_catches # Предупреждение о пустых catch (скрытые баги) diff --git a/lib/app/app.dart b/lib/app/app.dart deleted file mode 100644 index 3bb931f..0000000 --- a/lib/app/app.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:friflex_starter/app/app_context_ext.dart'; -import 'package:friflex_starter/app/app_providers.dart'; -import 'package:friflex_starter/app/depends_providers.dart'; -import 'package:friflex_starter/app/theme/app_theme.dart'; -import 'package:friflex_starter/app/theme/theme_notifier.dart'; -import 'package:friflex_starter/di/di_container.dart'; -import 'package:friflex_starter/features/error/error_screen.dart'; -import 'package:friflex_starter/features/splash/splash_screen.dart'; -import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart'; -import 'package:friflex_starter/features/update/update_routes.dart'; -import 'package:friflex_starter/l10n/gen/app_localizations.dart'; -import 'package:friflex_starter/l10n/localization_notifier.dart'; -import 'package:go_router/go_router.dart'; - -/// {@template app} -/// Главный виджет приложения, управляющий инициализацией зависимостей -/// и отображением основного интерфейса приложения. -/// -/// Отвечает за: -/// - Инициализацию зависимостей приложения -/// - Отображение экрана загрузки во время инициализации -/// - Обработку ошибок инициализации -/// - Настройку провайдеров для темы и локализации -/// {@endtemplate} -class App extends StatefulWidget { - /// {@macro app} - const App({required this.router, required this.initDependencies, super.key}); - - /// Роутер приложения для навигации между экранами - final GoRouter router; - - /// Функция для инициализации зависимостей приложения - /// Возвращает Future с контейнером зависимостей - final Future Function() initDependencies; - - @override - State createState() => _AppState(); -} - -/// {@template app_state} -/// Состояние главного виджета приложения. -/// -/// Управляет процессом инициализации зависимостей и отображением -/// соответствующих экранов в зависимости от состояния инициализации. -/// {@endtemplate} -class _AppState extends State { - /// {@macro app_state} - _AppState(); - - /// Мутабельная Future для инициализации зависимостей - /// Позволяет перезапускать инициализацию при ошибках - late Future _initFuture; - - @override - void initState() { - super.initState(); - _initFuture = widget.initDependencies(); - } - - @override - Widget build(BuildContext context) { - return AppProviders( - // Consumer для локализации добавляем выше чем DependsProviders - // чтобы при изменении локализации перестраивался весь виджет - // Но, это не обязательно, можно добавить в DependsProviders - child: LocalizationConsumer( - builder: () => FutureBuilder( - future: _initFuture, - builder: (_, snapshot) { - return switch (snapshot.connectionState) { - // Если состояние не определено, ожидается или активно, то отображаем экран загрузки - ConnectionState.none || - ConnectionState.waiting || - ConnectionState.active => const SplashScreen(), - ConnectionState.done => - // Если данные получены и не равны null, то отображаем внутренний виджет приложения - // Иначе отображаем экран ошибки - (snapshot.hasData && snapshot.data != null) - ? _App(router: widget.router, diContainer: snapshot.data!) - : ErrorScreen( - error: snapshot.error, - stackTrace: snapshot.stackTrace, - onRetry: _retryInit, - ), - }; - }, - ), - ), - ); - } - - /// Метод для перезапуска инициализации зависимостей - /// Вызывается при ошибках инициализации для повторной попытки - void _retryInit() { - setState(() { - _initFuture = widget.initDependencies(); - }); - } -} - -/// {@template app_internal} -/// Внутренний виджет приложения, отображающий основной интерфейс -/// после успешной инициализации зависимостей. -/// -/// Настраивает MaterialApp с роутером, темами и локализацией. -/// {@endtemplate} -class _App extends StatelessWidget { - /// {@macro app_internal} - const _App({required this.router, required this.diContainer}); - - /// Роутер приложения для навигации - final GoRouter router; - - /// Контейнер зависимостей - final DiContainer diContainer; - - @override - Widget build(BuildContext context) { - return DependsProviders( - diContainer: diContainer, - child: BlocConsumer( - listener: (context, state) { - if (state is UpdateSuccessState && - state.updateInfo.updateType == .hard && - context.mounted) { - router.goNamed(UpdateRoutes.hardUpdateScreenName); - } - }, - builder: (context, state) { - // Если состояние загрузки, то отображаем экран загрузки - if (state is UpdateLoadingState) { - return const SplashScreen(); - } - return ThemeConsumer( - builder: () => MediaQuery( - key: const ValueKey('prevent_rebuild'), - data: MediaQuery.of( - context, - ).copyWith(textScaler: TextScaler.noScaling, boldText: false), - child: MaterialApp.router( - routerConfig: router, - darkTheme: AppTheme.dark, - theme: AppTheme.light, - themeMode: context.theme.themeMode, - locale: context.localization.locale, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - ), - ), - ); - }, - ), - ); - } -} diff --git a/lib/app/app_providers.dart b/lib/app/app_providers.dart index fede8c4..6f30ce0 100644 --- a/lib/app/app_providers.dart +++ b/lib/app/app_providers.dart @@ -1,14 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:friflex_starter/app/theme/theme_notifier.dart'; +import 'package:friflex_starter/di/di_container.dart'; +import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart'; import 'package:friflex_starter/l10n/localization_notifier.dart'; import 'package:provider/provider.dart'; -/// Класс для добавления провайдеров темы и локализации +/// {@template app_providers} +/// Класс для добавления зависимостей приложения +/// {@endtemplate} final class AppProviders extends StatelessWidget { - const AppProviders({required this.child, super.key}); + /// {@macro app_providers} + const AppProviders({ + required this.child, + required this.diContainer, + super.key, + }); + /// Виджет, который будет отображаться внутри провайдеров final Widget child; + /// Контейнер зависимостей + final DiContainer diContainer; + @override Widget build(BuildContext context) { return MultiProvider( @@ -19,6 +33,10 @@ final class AppProviders extends StatelessWidget { ChangeNotifierProvider( create: (_) => LocalizationNotifier(), ), // Провайдер для локализации + Provider.value(value: diContainer), // Передаем контейнер зависимостей + BlocProvider( + create: (_) => UpdateCubit(diContainer.repositories.updateRepository), + ), ], child: child, ); diff --git a/lib/app/app_root.dart b/lib/app/app_root.dart new file mode 100644 index 0000000..f2ca9c5 --- /dev/null +++ b/lib/app/app_root.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:friflex_starter/app/app_context_ext.dart'; +import 'package:friflex_starter/app/app_providers.dart'; +import 'package:friflex_starter/app/theme/app_theme.dart'; +import 'package:friflex_starter/app/theme/theme_notifier.dart'; +import 'package:friflex_starter/di/di_container.dart'; +import 'package:friflex_starter/l10n/gen/app_localizations.dart'; +import 'package:friflex_starter/l10n/localization_notifier.dart'; +import 'package:go_router/go_router.dart'; + +/// {@template app} +/// Главный виджет приложения, отображающий основной интерфейс приложения +/// +/// Отвечает за: +/// - Настройку провайдеров для темы и локализации +/// {@endtemplate} +class AppRoot extends StatelessWidget { + /// {@macro app_root} + const AppRoot({required this.diContainer, required this.router, super.key}); + + /// Контейнер зависимостей + final DiContainer diContainer; + + /// Роутер приложения + final GoRouter router; + + @override + Widget build(BuildContext context) { + return AppProviders( + diContainer: diContainer, + child: LocalizationConsumer( + builder: (localizationContext) { + return ThemeConsumer( + builder: (themeContext) => MediaQuery( + key: const ValueKey('prevent_rebuild'), + data: MediaQuery.of( + themeContext, + ).copyWith(textScaler: TextScaler.noScaling, boldText: false), + child: MaterialApp.router( + darkTheme: AppTheme.dark, + theme: AppTheme.light, + themeMode: themeContext.theme.themeMode, + locale: localizationContext.localization.locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: router, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/app/depends_providers.dart b/lib/app/depends_providers.dart deleted file mode 100644 index 7ec1352..0000000 --- a/lib/app/depends_providers.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:friflex_starter/di/di_container.dart'; -import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart'; -import 'package:provider/provider.dart'; - -/// Класс для внедрения глобальных зависимостей -final class DependsProviders extends StatelessWidget { - const DependsProviders({ - required this.child, - required this.diContainer, - super.key, - }); - - final Widget child; - final DiContainer diContainer; - - @override - Widget build(BuildContext context) { - return MultiProvider( - providers: [ - // Сюда добавляем глобальные блоки, inherited и т.д. - Provider.value(value: diContainer), // Передаем контейнер зависимостей - BlocProvider( - create: (_) { - final updateCubit = UpdateCubit( - diContainer.repositories.updatesRepository, - ); - unawaited( - updateCubit.checkForUpdates( - versionCode: - '1.0.0', // TODO(yura): заменить на получение из diContainer - platform: 'android', - ), - ); - return updateCubit; - }, - ), - ], - child: child, - ); - } -} diff --git a/lib/app/theme/theme_notifier.dart b/lib/app/theme/theme_notifier.dart index fdc6f19..c74964f 100644 --- a/lib/app/theme/theme_notifier.dart +++ b/lib/app/theme/theme_notifier.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; /// Тип функции для построения виджета с учетом темы -typedef ThemeBuilder = Widget Function(); +typedef ThemeBuilder = Widget Function(BuildContext context); /// {@template theme_consumer} /// Виджет для подписки на изменения темы приложения. @@ -20,8 +20,8 @@ class ThemeConsumer extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( - builder: (_, _, _) { - return builder(); + builder: (context, _, _) { + return builder(context); }, ); } diff --git a/lib/di/di_repositories.dart b/lib/di/di_repositories.dart index d03a52b..50016f4 100644 --- a/lib/di/di_repositories.dart +++ b/lib/di/di_repositories.dart @@ -49,7 +49,7 @@ final class DiRepositories { late final IProfileRepository profileRepository; /// Интерфейс для работы с репозиторием обновлений - late final IUpdateRepository updatesRepository; + late final IUpdateRepository updateRepository; /// Метод для инициализации репозиториев в приложении. /// @@ -70,7 +70,7 @@ final class DiRepositories { onProgress('Начинаем инициализацию репозиториев...'); // Инициализация репозитория обновлений - updatesRepository = _lazyInitRepo( + updateRepository = _lazyInitRepo( mockFactory: () => const UpdateMockRepository(), mainFactory: () => UpdateRepository(httpClient: diContainer.httpClient), onProgress: onProgress, diff --git a/lib/features/splash/splash_screen.dart b/lib/features/splash/splash_screen.dart deleted file mode 100644 index 71eebc1..0000000 --- a/lib/features/splash/splash_screen.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:friflex_starter/gen/assets.gen.dart'; - -/// {@template SplashScreen} -/// Экран загрузки приложения. -/// {@endtemplate} -class SplashScreen extends StatelessWidget { - /// {@macro SplashScreen} - const SplashScreen({super.key}); - - @override - Widget build(BuildContext context) { - return Center(child: Assets.lottie.splash.lottie()); - } -} diff --git a/lib/l10n/localization_notifier.dart b/lib/l10n/localization_notifier.dart index bd83fa7..f559eb7 100644 --- a/lib/l10n/localization_notifier.dart +++ b/lib/l10n/localization_notifier.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; /// Тип функции для построения виджета с учетом локализации -typedef LocalizationBuilder = Widget Function(); +typedef LocalizationBuilder = Widget Function(BuildContext context); /// {@template localization_consumer} /// Виджет для подписки на изменения локализации приложения. @@ -20,8 +20,8 @@ class LocalizationConsumer extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( - builder: (_, _, _) { - return builder(); + builder: (context, _, _) { + return builder(context); }, ); } diff --git a/lib/main.dart b/lib/main.dart index 07f2cff..7a4e24b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,3 @@ -import 'package:friflex_starter/runner/app_runner.dart'; +import 'package:friflex_starter/targets/prod.dart' as prod; -void main() => AppRunner(.prod).run(); +void main(List arguments) => prod.main(arguments); diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index eefd037..1be0562 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -4,7 +4,6 @@ import 'package:friflex_starter/features/debug/i_debug_service.dart'; import 'package:friflex_starter/features/main/presentation/main_routes.dart'; import 'package:friflex_starter/features/profile/presentation/profile_routes.dart'; import 'package:friflex_starter/features/root/root_screen.dart'; -import 'package:friflex_starter/features/splash/splash_screen.dart'; import 'package:friflex_starter/features/update/update_routes.dart'; import 'package:go_router/go_router.dart'; @@ -39,10 +38,6 @@ class AppRouter { ], ), DebugRoutes.buildRoutes(), - GoRoute( - path: '/splash', - builder: (context, state) => const SplashScreen(), - ), UpdateRoutes.buildRoutes(), ], ); diff --git a/lib/runner/app_runner.dart b/lib/runner/app_runner.dart index c481474..4e70ef0 100644 --- a/lib/runner/app_runner.dart +++ b/lib/runner/app_runner.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:friflex_starter/app/app.dart'; import 'package:friflex_starter/app/app_env.dart'; +import 'package:friflex_starter/app/app_root.dart'; import 'package:friflex_starter/di/di_container.dart'; import 'package:friflex_starter/features/debug/debug_service.dart'; import 'package:friflex_starter/features/debug/i_debug_service.dart'; @@ -16,11 +16,6 @@ import 'package:go_router/go_router.dart'; part 'errors_handlers.dart'; -/// Время ожидания инициализации зависимостей -/// Если время превышено, то будет показан экран ошибки -/// В дальнейшем нужно убрать в env -const _initTimeout = Duration(seconds: 7); - /// Класс, реализующий раннер для конфигурирования приложения при запуске /// /// Порядок инициализации: @@ -48,7 +43,7 @@ class AppRunner { late TimerRunner _timerRunner; /// Метод для запуска приложения - Future run() async { + Future run(List arguments) async { try { WidgetsFlutterBinding.ensureInitialized(); // Инициализация сервиса отладки @@ -62,42 +57,31 @@ class AppRunner { // Инициализация приложения await _initApp(); - // Инициализация метода обработки ошибок - _initErrorHandlers(_debugService); - // Инициализация роутера router = AppRouter.createRouter(_debugService); - // throw Exception('Test error'); - - runApp( - App( - router: router, - initDependencies: () { - return _initDependencies( - debugService: _debugService, - env: env, - timerRunner: _timerRunner, - ).timeout( - _initTimeout, - onTimeout: () { - return Future.error( - TimeoutException( - 'Превышено время ожидания инициализации зависимостей', - ), - ); - }, - ); - }, - ), + final diContainer = await _initDependencies( + debugService: _debugService, + env: env, + timerRunner: _timerRunner, ); + // Инициализация метода обработки ошибок + _initErrorHandlers(_debugService); + runApp(AppRoot(diContainer: diContainer, router: router)); await _onAppLoaded(); } on Object catch (e, stackTrace) { await _onAppLoaded(); + _timerRunner.stop(); /// Если произошла ошибка при инициализации приложения, /// то запускаем экран ошибки - runApp(ErrorScreen(error: e, stackTrace: stackTrace, onRetry: run)); + runApp( + ErrorScreen( + error: e, + stackTrace: stackTrace, + onRetry: () => run(arguments), + ), + ); } } @@ -127,20 +111,32 @@ class AppRunner { }) async { debugService.log(() => 'Тип сборки: ${env.name}'); final diContainer = DiContainer(env: env, dService: debugService); - await diContainer.init( - onProgress: (name) => timerRunner.logOnProgress(name), - onComplete: (name) { - timerRunner - ..logOnComplete(name) - ..stop(); - }, - onError: (message, error, [stackTrace]) { - timerRunner.stop(); - _debugService.logError(message, error: error, stackTrace: stackTrace); - throw Exception('Ошибка инициализации зависимостей: $message'); - }, - ); - //throw Exception('Test error'); + await diContainer + .init( + onProgress: (name) => timerRunner.logOnProgress(name), + onComplete: (name) { + timerRunner + ..logOnComplete(name) + ..stop(); + }, + onError: (message, error, [stackTrace]) { + timerRunner.stop(); + _debugService.logError( + message, + error: error, + stackTrace: stackTrace, + ); + throw Exception('Ошибка инициализации зависимостей: $message'); + }, + ) + .timeout( + const Duration(seconds: 7), + onTimeout: () { + throw Exception( + 'Превышено время ожидания инициализации зависимостей', + ); + }, + ); return diContainer; } } diff --git a/lib/runner/errors_handlers.dart b/lib/runner/errors_handlers.dart index 02f060d..3f0fe66 100644 --- a/lib/runner/errors_handlers.dart +++ b/lib/runner/errors_handlers.dart @@ -4,7 +4,6 @@ part of 'app_runner.dart'; void _initErrorHandlers(IDebugService debugService) { // Обработка ошибок в приложении FlutterError.onError = (details) { - _showErrorScreen(details.exception, details.stack); debugService.logError( () => 'FlutterError.onError: ${details.exceptionAsString()}', error: details.exception, @@ -13,7 +12,6 @@ void _initErrorHandlers(IDebugService debugService) { }; // Обработка асинхронных ошибок в приложении PlatformDispatcher.instance.onError = (error, stack) { - _showErrorScreen(error, stack); debugService.logError( () => 'PlatformDispatcher.instance.onError', error: error, @@ -22,14 +20,3 @@ void _initErrorHandlers(IDebugService debugService) { return true; }; } - -/// Метод для показа экрана ошибки -void _showErrorScreen(Object error, StackTrace? stackTrace) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await AppRouter.rootNavigatorKey.currentState?.push( - MaterialPageRoute( - builder: (_) => ErrorScreen(error: error, stackTrace: stackTrace), - ), - ); - }); -} diff --git a/lib/runner/timer_runner.dart b/lib/runner/timer_runner.dart index be54608..473fa3f 100644 --- a/lib/runner/timer_runner.dart +++ b/lib/runner/timer_runner.dart @@ -24,6 +24,11 @@ class TimerRunner { ); } + /// Метод для сброса секундомера + void reset() { + _stopwatch.reset(); + } + /// Метод для обработки прогресса инициализации зависимостей void logOnProgress(String name) { _debugService.log( diff --git a/lib/targets/dev.dart b/lib/targets/dev.dart index 708320e..9334991 100644 --- a/lib/targets/dev.dart +++ b/lib/targets/dev.dart @@ -1,3 +1,3 @@ import 'package:friflex_starter/runner/app_runner.dart'; -void main() => AppRunner(.dev).run(); +void main(List arguments) => AppRunner(.dev).run(arguments); diff --git a/lib/targets/prod.dart b/lib/targets/prod.dart index 07f2cff..9653962 100644 --- a/lib/targets/prod.dart +++ b/lib/targets/prod.dart @@ -1,3 +1,3 @@ import 'package:friflex_starter/runner/app_runner.dart'; -void main() => AppRunner(.prod).run(); +void main(List arguments) => AppRunner(.prod).run(arguments); diff --git a/lib/targets/stage.dart b/lib/targets/stage.dart index 72eddca..0dc8590 100644 --- a/lib/targets/stage.dart +++ b/lib/targets/stage.dart @@ -1,3 +1,3 @@ import 'package:friflex_starter/runner/app_runner.dart'; -void main() => AppRunner(.stage).run(); +void main(List arguments) => AppRunner(.stage).run(arguments);