Merge branch 'main' into feat/перенести-роутер-ближе-к-месту-инициализации

This commit is contained in:
Artem
2025-12-30 12:03:18 +07:00
34 changed files with 930 additions and 741 deletions

View File

@@ -1,180 +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:friflex_starter/router/app_router.dart';
import 'package:go_router/go_router.dart';
/// {@template app}
/// Главный виджет приложения, управляющий инициализацией зависимостей
/// и отображением основного интерфейса приложения.
///
/// Отвечает за:
/// - Инициализацию зависимостей приложения
/// - Отображение экрана загрузки во время инициализации
/// - Обработку ошибок инициализации
/// - Настройку провайдеров для темы и локализации
/// {@endtemplate}
class App extends StatefulWidget {
/// {@macro app}
const App({required this.initDependencies, super.key});
/// Функция для инициализации зависимостей приложения
/// Возвращает Future с контейнером зависимостей
final Future<DiContainer> Function() initDependencies;
@override
State<App> createState() => _AppState();
}
/// {@template app_state}
/// Состояние главного виджета приложения.
///
/// Управляет процессом инициализации зависимостей и отображением
/// соответствующих экранов в зависимости от состояния инициализации.
/// {@endtemplate}
class _AppState extends State<App> {
/// {@macro app_state}
_AppState();
/// Мутабельная Future для инициализации зависимостей
/// Позволяет перезапускать инициализацию при ошибках
late Future<DiContainer> _initFuture;
@override
void initState() {
super.initState();
_initFuture = widget.initDependencies();
}
@override
Widget build(BuildContext context) {
return AppProviders(
// Consumer для локализации добавляем выше чем DependsProviders
// чтобы при изменении локализации перестраивался весь виджет
// Но, это не обязательно, можно добавить в DependsProviders
child: LocalizationConsumer(
builder: () => FutureBuilder<DiContainer>(
future: _initFuture,
builder: (_, snapshot) {
return switch (snapshot.connectionState) {
// Если состояние не определено, ожидается или активно, то отображаем экран загрузки
ConnectionState.none ||
ConnectionState.waiting ||
ConnectionState.active => const SplashScreen(),
ConnectionState.done =>
// Если данные получены и не равны null, то отображаем внутренний виджет приложения
// Иначе отображаем экран ошибки
(snapshot.hasData && snapshot.data != null)
? _AppInternal(diContainer: snapshot.data!)
: ErrorScreen(
error: snapshot.error,
stackTrace: snapshot.stackTrace,
onRetry: _retryInit,
),
};
},
),
),
);
}
/// Метод для перезапуска инициализации зависимостей
/// Вызывается при ошибках инициализации для повторной попытки
void _retryInit() {
setState(() {
_initFuture = widget.initDependencies();
});
}
}
/// {@template app_internal}
/// Внутренний виджет приложения, отображающий основной интерфейс
/// после успешной инициализации зависимостей.
///
/// Настраивает MaterialApp с роутером, темами и локализацией.
/// {@endtemplate}
class _AppInternal extends StatefulWidget {
/// {@macro app_internal}
const _AppInternal({
required this.diContainer,
@visibleForTesting this.mockRouter, // ignore: unused_element_parameter
});
/// Контейнер зависимостей
final DiContainer diContainer;
/// Роутер приложения для навигации для тестирования
final GoRouter? mockRouter;
@override
State<_AppInternal> createState() => _AppInternalState();
}
class _AppInternalState extends State<_AppInternal> {
/// Роутер приложения для навигации
late final GoRouter router;
@override
void initState() {
super.initState();
router =
widget.mockRouter ??
AppRouter.createRouter(widget.diContainer.debugService);
}
@override
void dispose() {
router.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DependsProviders(
diContainer: widget.diContainer,
child: BlocConsumer<UpdateCubit, UpdateState>(
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,
),
),
);
},
),
);
}
}

View File

@@ -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,
);

54
lib/app/app_root.dart Normal file
View File

@@ -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,
),
),
);
},
),
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -1,13 +1,12 @@
import 'package:dio/dio.dart';
import 'package:friflex_starter/app/app_config/app_config.dart';
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
/// {@template app_http_client}
/// Класс для реализации HTTP-клиента для управления запросами
/// {@endtemplate}
final class AppHttpClient implements IHttpClient {
final class AppHttpClient {
/// Создает HTTP клиент
///
/// Принимает:
@@ -18,7 +17,6 @@ final class AppHttpClient implements IHttpClient {
required IAppConfig appConfig,
}) {
_httpClient = Dio();
_appConfig = appConfig;
_httpClient.options
..baseUrl = appConfig.baseUrl
@@ -30,111 +28,8 @@ final class AppHttpClient implements IHttpClient {
_httpClient.interceptors.add(debugService.dioLogger);
}
/// Конфигурация приложения
late final IAppConfig _appConfig;
/// Экземпляр HTTP клиента
late final Dio _httpClient;
@override
Future<Response> get(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.get(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> post(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> patch(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.patch(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> put(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> delete(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
@override
Future<Response> head(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
_httpClient.options.baseUrl = _appConfig.baseUrl;
return _httpClient.head(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
Dio get client => _httpClient;
}

View File

@@ -1,94 +0,0 @@
import 'package:dio/dio.dart';
/// Класс для описания интерфейса сервиса по управлению HTTP запросами
abstract interface class IHttpClient {
/// Описывает поля HTTP клиента
const IHttpClient();
/// Наименование сервиса
static const name = 'IHttpClient';
/// Метод для реализации запроса GET
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> get(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса POST
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> post(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса PATCH
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> patch(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса PUT
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> put(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса DELETE
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> delete(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
/// Метод для реализации запроса POST
///
/// Принимает:
/// - [path] - путь к ресурсу
/// - [data] - тело запроса
/// - [queryParameters] - параметры запроса
/// - [options] - конфигурация запроса
Future<Response> head(
String path, {
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
});
}

View File

@@ -55,7 +55,7 @@ class AppColors extends ThemeExtension<AppColors> with _$AppColorsTailorMixin {
);
/// Цвета тёмной темы
static const AppColors dark = AppColors(
static const AppColors dark = AppColors(
testColor: Colors.green,
errorSnackbarBackground: Color(0xFF638B8B),
successSnackbarBackground: Color(0xFF93C499),

View File

@@ -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<ThemeNotifier>(
builder: (_, _, _) {
return builder();
builder: (context, _, _) {
return builder(context);
},
);
}

View File

@@ -290,21 +290,9 @@ class _Icon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return switch (type) {
.success => const Icon(
Icons.check_circle,
color: Colors.white,
size: 32,
),
.error => const Icon(
Icons.error,
color: Colors.white,
size: 32,
),
.info => const Icon(
Icons.info,
color: Colors.white,
size: 32,
),
.success => const Icon(Icons.check_circle, color: Colors.white, size: 32),
.error => const Icon(Icons.error, color: Colors.white, size: 32),
.info => const Icon(Icons.info, color: Colors.white, size: 32),
};
}
}

View File

@@ -1,7 +1,6 @@
import 'package:friflex_starter/app/app_config/app_config.dart';
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/di/di_repositories.dart';
import 'package:friflex_starter/di/di_services.dart';
import 'package:friflex_starter/di/di_typedefs.dart';
@@ -25,7 +24,7 @@ final class DiContainer {
late final IAppConfig appConfig;
/// Сервис для работы с HTTP запросами
late final IHttpClient Function(IDebugService, IAppConfig) httpClientFactory;
late final AppHttpClient httpClient;
/// Репозитории приложения
late final DiRepositories repositories;
@@ -47,8 +46,10 @@ final class DiContainer {
};
// Инициализация HTTP клиента
httpClientFactory = (debugService, appConfig) =>
AppHttpClient(debugService: debugService, appConfig: appConfig);
httpClient = AppHttpClient(
debugService: debugService,
appConfig: appConfig,
);
// Инициализация сервисов
services = DiServices()

View File

@@ -2,9 +2,6 @@ import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/di/di_base_repo.dart';
import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/di/di_typedefs.dart';
import 'package:friflex_starter/features/auth/data/repository/auth_mock_repository.dart';
import 'package:friflex_starter/features/auth/data/repository/auth_repository.dart';
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
import 'package:friflex_starter/features/main/data/repository/main_mock_repository.dart';
import 'package:friflex_starter/features/main/data/repository/main_repository.dart';
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
@@ -45,9 +42,6 @@ final class DiRepositories {
/// {@macro di_repositories}
DiRepositories();
/// Интерфейс для работы с репозиторием авторизации
late final IAuthRepository authRepository;
/// Интерфейс для работы с репозиторием главного сервиса
late final IMainRepository mainRepository;
@@ -55,7 +49,7 @@ final class DiRepositories {
late final IProfileRepository profileRepository;
/// Интерфейс для работы с репозиторием обновлений
late final IUpdateRepository updatesRepository;
late final IUpdateRepository updateRepository;
/// Метод для инициализации репозиториев в приложении.
///
@@ -76,23 +70,9 @@ final class DiRepositories {
onProgress('Начинаем инициализацию репозиториев...');
// Инициализация репозитория обновлений
updatesRepository = _lazyInitRepo<IUpdateRepository>(
mockFactory: UpdateMockRepository.new,
mainFactory: UpdateRepository.new,
onProgress: onProgress,
onError: onError,
environment: diContainer.env,
);
// Инициализация репозитория авторизации
authRepository = _lazyInitRepo<IAuthRepository>(
mockFactory: AuthMockRepository.new,
mainFactory: () => AuthRepository(
httpClient: diContainer.httpClientFactory(
diContainer.debugService,
diContainer.appConfig,
),
),
updateRepository = _lazyInitRepo<IUpdateRepository>(
mockFactory: () => const UpdateMockRepository(),
mainFactory: () => UpdateRepository(httpClient: diContainer.httpClient),
onProgress: onProgress,
onError: onError,
environment: diContainer.env,
@@ -100,13 +80,8 @@ final class DiRepositories {
// Инициализация репозитория сервиса управления токеном доступа
mainRepository = _lazyInitRepo<IMainRepository>(
mockFactory: MainMockRepository.new,
mainFactory: () => MainRepository(
httpClient: diContainer.httpClientFactory(
diContainer.debugService,
diContainer.appConfig,
),
),
mockFactory: () => const MainMockRepository(),
mainFactory: () => MainRepository(httpClient: diContainer.httpClient),
onProgress: onProgress,
onError: onError,
environment: diContainer.env,
@@ -114,13 +89,8 @@ final class DiRepositories {
// Инициализация репозитория профиля
profileRepository = _lazyInitRepo<IProfileRepository>(
mockFactory: ProfileMockRepository.new,
mainFactory: () => ProfileRepository(
httpClient: diContainer.httpClientFactory(
diContainer.debugService,
diContainer.appConfig,
),
),
mockFactory: () => const ProfileMockRepository(),
mainFactory: () => ProfileRepository(httpClient: diContainer.httpClient),
onProgress: onProgress,
onError: onError,
environment: diContainer.env,

View File

@@ -1,9 +0,0 @@
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
/// {@template AuthMockRepository}
/// Mock реализация репозитория авторизации
/// {@endtemplate}
final class AuthMockRepository implements IAuthRepository {
@override
String get name => 'AuthMockRepository';
}

View File

@@ -1,14 +0,0 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/features/auth/domain/repository/i_auth_repository.dart';
/// {@template AuthRepository}
/// Реализация репозитория авторизации
/// {@endtemplate}
final class AuthRepository implements IAuthRepository {
AuthRepository({required this.httpClient});
final IHttpClient httpClient;
@override
String get name => 'AuthRepository';
}

View File

@@ -1,6 +0,0 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IAuthRepository}
/// Интерфейс для работы с репозиторием авторизации
/// {@endtemplate}
abstract interface class IAuthRepository with DiBaseRepo {}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
/// {@template auth_screen}
/// Экран авторизации пользователя.
///
/// Отвечает за:
/// - Отображение формы входа в приложение
/// - Обработку процесса аутентификации
/// - Навигацию после успешной авторизации
///
/// В текущей реализации является заглушкой для будущей функциональности.
/// {@endtemplate}
class AuthScreen extends StatelessWidget {
/// {@macro auth_screen}
const AuthScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AuthScreen')),
body: const Center(child: Text('AuthScreen')),
);
}
}

View File

@@ -1,9 +1,12 @@
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
/// {@template MainMockRepository}
///
/// Мок реализация репозитория главного сервиса
/// {@endtemplate}
final class MainMockRepository implements IMainRepository {
/// {@macro MainMockRepository}
const MainMockRepository();
@override
String get name => 'MainMockRepository';
}

View File

@@ -1,13 +1,14 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/features/main/domain/repository/i_main_repository.dart';
/// {@template MainRepository}
///
/// Реализация репозитория главного сервиса
/// {@endtemplate}
final class MainRepository implements IMainRepository {
MainRepository({required this.httpClient});
final IHttpClient httpClient;
/// Экземпляр HTTP клиента для взаимодействия с сервером
final AppHttpClient httpClient;
@override
String get name => 'MainRepository';

View File

@@ -1,9 +1,12 @@
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
/// {@template ProfileMockRepository}
///
/// Мок реализация репозитория профиля пользователя
/// {@endtemplate}
final class ProfileMockRepository implements IProfileRepository {
/// {@macro ProfileMockRepository}
const ProfileMockRepository();
@override
String get name => 'ProfileMockRepository';

View File

@@ -1,13 +1,15 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
/// {@template ProfileRepository}
///
/// Реализация репозитория профиля пользователя
/// {@endtemplate}
final class ProfileRepository implements IProfileRepository {
ProfileRepository({required this.httpClient});
final IHttpClient httpClient;
/// Экземпляр HTTP клиента для взаимодействия с сервером
final AppHttpClient httpClient;
@override
String get name => 'ProfileRepository';

View File

@@ -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());
}
}

View File

@@ -1,3 +1,4 @@
import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
@@ -6,7 +7,10 @@ import 'package:friflex_starter/features/update/domain/repository/i_update_repos
/// {@endtemplate}
final class UpdateRepository implements IUpdateRepository {
/// {@macro UpdateRepository}
const UpdateRepository();
UpdateRepository({required this.httpClient});
/// Экземпляр HTTP клиента для взаимодействия с сервером
final AppHttpClient httpClient;
@override
Future<UpdateEntity> checkForUpdates({

View File

@@ -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<LocalizationNotifier>(
builder: (_, _, _) {
return builder();
builder: (context, _, _) {
return builder(context);
},
);
}

View File

@@ -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<String> arguments) => prod.main(arguments);

View File

@@ -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(),
],
);

View File

@@ -4,23 +4,17 @@ 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';
import 'package:friflex_starter/features/error/error_screen.dart';
import 'package:friflex_starter/router/app_router.dart';
import 'package:friflex_starter/runner/timer_runner.dart';
import 'package:go_router/go_router.dart';
part 'errors_handlers.dart';
/// Время ожидания инициализации зависимостей
/// Если время превышено, то будет показан экран ошибки
/// В дальнейшем нужно убрать в env
const _initTimeout = Duration(seconds: 7);
/// Класс, реализующий раннер для конфигурирования приложения при запуске
///
/// Порядок инициализации:
@@ -48,7 +42,7 @@ class AppRunner {
late TimerRunner _timerRunner;
/// Метод для запуска приложения
Future<void> run() async {
Future<void> run(List<String> arguments) async {
try {
WidgetsFlutterBinding.ensureInitialized();
// Инициализация сервиса отладки
@@ -62,38 +56,28 @@ class AppRunner {
// Инициализация приложения
await _initApp();
final diContainer = await _initDependencies(
debugService: _debugService,
env: env,
timerRunner: _timerRunner,
);
// Инициализация метода обработки ошибок
_initErrorHandlers(_debugService);
// throw Exception('Test error');
runApp(
App(
initDependencies: () {
return _initDependencies(
debugService: _debugService,
env: env,
timerRunner: _timerRunner,
).timeout(
_initTimeout,
onTimeout: () {
return Future.error(
TimeoutException(
'Превышено время ожидания инициализации зависимостей',
),
);
},
);
},
),
);
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),
),
);
}
}
@@ -123,20 +107,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;
}
}

View File

@@ -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),
),
);
});
}

View File

@@ -24,6 +24,11 @@ class TimerRunner {
);
}
/// Метод для сброса секундомера
void reset() {
_stopwatch.reset();
}
/// Метод для обработки прогресса инициализации зависимостей
void logOnProgress(String name) {
_debugService.log(

View File

@@ -1,3 +1,3 @@
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(.dev).run();
void main(List<String> arguments) => AppRunner(.dev).run(arguments);

View File

@@ -1,3 +1,3 @@
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(.prod).run();
void main(List<String> arguments) => AppRunner(.prod).run(arguments);

View File

@@ -1,3 +1,3 @@
import 'package:friflex_starter/runner/app_runner.dart';
void main() => AppRunner(.stage).run();
void main(List<String> arguments) => AppRunner(.stage).run(arguments);