mirror of
https://github.com/smmarty/friflex_flutter_starter.git
synced 2025-12-22 01:20:46 +00:00
feat(update): добавить модуль управления Hard & Soft обновлений (#30)
1. Реализован интерфейс и репозитории для проверки обновлений. 2. Добавлены состояния и кубит для управления процессом обновления. 3. Созданы UI-компоненты для отображения информации об обновлениях. 4. Обновлен README.md с описанием нового модуля и его интеграции
This commit is contained in:
20
.vscode/dart.prime_snippets.code-snippets
vendored
20
.vscode/dart.prime_snippets.code-snippets
vendored
@@ -14,5 +14,23 @@
|
|||||||
"/// {@macro ${TM_FILENAME_BASE}}"
|
"/// {@macro ${TM_FILENAME_BASE}}"
|
||||||
],
|
],
|
||||||
"description": "DartDoc короткая запись macro с именем файла"
|
"description": "DartDoc короткая запись macro с именем файла"
|
||||||
}
|
},
|
||||||
|
"TODO": {
|
||||||
|
"prefix": "todo",
|
||||||
|
"body": [
|
||||||
|
"// TODO($1): $2"
|
||||||
|
],
|
||||||
|
"description": "Create todo"
|
||||||
|
},
|
||||||
|
"TRYCATCH": {
|
||||||
|
"prefix": "tryc",
|
||||||
|
"body": [
|
||||||
|
"try {",
|
||||||
|
"$1",
|
||||||
|
"} on Object catch (error,stackTrace) {",
|
||||||
|
"",
|
||||||
|
"}",
|
||||||
|
],
|
||||||
|
"description": "Create trycatch"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
0
CHANGELOG.md
Normal file
0
CHANGELOG.md
Normal file
104
lib/app/app.dart
104
lib/app/app.dart
@@ -1,13 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
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_context_ext.dart';
|
||||||
import 'package:friflex_starter/app/app_providers.dart';
|
import 'package:friflex_starter/app/app_providers.dart';
|
||||||
import 'package:friflex_starter/app/depends_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/app_theme.dart';
|
||||||
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
import 'package:friflex_starter/app/theme/theme_notifier.dart';
|
||||||
import 'package:friflex_starter/di/di_container.dart';
|
import 'package:friflex_starter/di/di_container.dart';
|
||||||
|
|
||||||
import 'package:friflex_starter/features/error/error_screen.dart';
|
import 'package:friflex_starter/features/error/error_screen.dart';
|
||||||
import 'package:friflex_starter/features/splash/splash_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_type.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_routes.dart';
|
||||||
import 'package:friflex_starter/l10n/gen/app_localizations.dart';
|
import 'package:friflex_starter/l10n/gen/app_localizations.dart';
|
||||||
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
import 'package:friflex_starter/l10n/localization_notifier.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -67,44 +70,22 @@ class _AppState extends State<App> {
|
|||||||
builder: () => FutureBuilder<DiContainer>(
|
builder: () => FutureBuilder<DiContainer>(
|
||||||
future: _initFuture,
|
future: _initFuture,
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
switch (snapshot.connectionState) {
|
return switch (snapshot.connectionState) {
|
||||||
case ConnectionState.none:
|
// Если состояние не определено, ожидается или активно, то отображаем экран загрузки
|
||||||
case ConnectionState.waiting:
|
ConnectionState.none ||
|
||||||
case ConnectionState.active:
|
ConnectionState.waiting ||
|
||||||
// Пока инициализация показываем Splash
|
ConnectionState.active => const SplashScreen(),
|
||||||
return const SplashScreen();
|
ConnectionState.done =>
|
||||||
case ConnectionState.done:
|
// Если данные получены и не равны null, то отображаем внутренний виджет приложения
|
||||||
if (snapshot.hasError) {
|
// Иначе отображаем экран ошибки
|
||||||
return ErrorScreen(
|
(snapshot.hasData && snapshot.data != null)
|
||||||
error: snapshot.error,
|
? _App(router: widget.router, diContainer: snapshot.data!)
|
||||||
stackTrace: snapshot.stackTrace,
|
: ErrorScreen(
|
||||||
onRetry: _retryInit,
|
error: snapshot.error,
|
||||||
);
|
stackTrace: snapshot.stackTrace,
|
||||||
}
|
onRetry: _retryInit,
|
||||||
|
|
||||||
final diContainer = snapshot.data;
|
|
||||||
if (diContainer == null) {
|
|
||||||
return ErrorScreen(
|
|
||||||
error:
|
|
||||||
'Ошибка инициализации зависимостей, diContainer = null',
|
|
||||||
stackTrace: null,
|
|
||||||
onRetry: _retryInit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return DependsProviders(
|
|
||||||
diContainer: diContainer,
|
|
||||||
child: ThemeConsumer(
|
|
||||||
builder: () => MediaQuery(
|
|
||||||
key: ValueKey('prevent_rebuild'),
|
|
||||||
data: MediaQuery.of(context).copyWith(
|
|
||||||
textScaler: TextScaler.noScaling,
|
|
||||||
boldText: false,
|
|
||||||
),
|
),
|
||||||
child: _App(router: widget.router),
|
};
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -128,21 +109,50 @@ class _AppState extends State<App> {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class _App extends StatelessWidget {
|
class _App extends StatelessWidget {
|
||||||
/// {@macro app_internal}
|
/// {@macro app_internal}
|
||||||
const _App({required this.router});
|
const _App({required this.router, required this.diContainer});
|
||||||
|
|
||||||
/// Роутер приложения для навигации
|
/// Роутер приложения для навигации
|
||||||
final GoRouter router;
|
final GoRouter router;
|
||||||
|
|
||||||
|
/// Контейнер зависимостей
|
||||||
|
final DiContainer diContainer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp.router(
|
return DependsProviders(
|
||||||
routerConfig: router,
|
diContainer: diContainer,
|
||||||
darkTheme: AppTheme.dark,
|
child: BlocConsumer<UpdateCubit, UpdateState>(
|
||||||
theme: AppTheme.light,
|
listener: (context, state) {
|
||||||
themeMode: context.theme.themeMode,
|
if (state is UpdateSuccessState &&
|
||||||
locale: context.localization.locale,
|
state.updateInfo.updateType == UpdateType.hard &&
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
context.mounted) {
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
router.goNamed(UpdateRoutes.hardUpdateScreenName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
// Если состояние загрузки, то отображаем экран загрузки
|
||||||
|
if (state is UpdateLoadingState) {
|
||||||
|
return const SplashScreen();
|
||||||
|
}
|
||||||
|
return ThemeConsumer(
|
||||||
|
builder: () => MediaQuery(
|
||||||
|
key: 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:friflex_starter/di/di_container.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';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// Класс для внедрения глобальных зависимостей
|
/// Класс для внедрения глобальных зависимостей
|
||||||
@@ -19,6 +21,14 @@ final class DependsProviders extends StatelessWidget {
|
|||||||
providers: [
|
providers: [
|
||||||
// Сюда добавляем глобальные блоки, inherited и т.д.
|
// Сюда добавляем глобальные блоки, inherited и т.д.
|
||||||
Provider.value(value: diContainer), // Передаем контейнер зависимостей
|
Provider.value(value: diContainer), // Передаем контейнер зависимостей
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) => UpdateCubit(diContainer.repositories.updatesRepository)
|
||||||
|
..checkForUpdates(
|
||||||
|
versionCode:
|
||||||
|
'1.0.0', // TODO(yura): заменить на получение из diContainer
|
||||||
|
platform: 'android',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import 'package:friflex_starter/features/main/domain/repository/i_main_repositor
|
|||||||
import 'package:friflex_starter/features/profile/data/repository/profile_mock_repository.dart';
|
import 'package:friflex_starter/features/profile/data/repository/profile_mock_repository.dart';
|
||||||
import 'package:friflex_starter/features/profile/data/repository/profile_repository.dart';
|
import 'package:friflex_starter/features/profile/data/repository/profile_repository.dart';
|
||||||
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/update/data/repository/update_mock_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/update/data/repository/update_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
||||||
|
|
||||||
/// Список названий моковых репозиториев, которые должны быть подменены
|
/// Список названий моковых репозиториев, которые должны быть подменены
|
||||||
/// для использования в сборке stage окружения.
|
/// для использования в сборке stage окружения.
|
||||||
@@ -23,7 +26,7 @@ import 'package:friflex_starter/features/profile/domain/repository/i_profile_rep
|
|||||||
/// ```
|
/// ```
|
||||||
/// [ AuthCheckRepositoryMock().name, ]
|
/// [ AuthCheckRepositoryMock().name, ]
|
||||||
/// ```
|
/// ```
|
||||||
final List<String> _mockReposToSwitch = [];
|
final List<String> _mockReposToSwitch = [UpdateMockRepository().name];
|
||||||
|
|
||||||
/// {@template di_repositories}
|
/// {@template di_repositories}
|
||||||
/// Класс для инициализации и управления репозиториями приложения.
|
/// Класс для инициализации и управления репозиториями приложения.
|
||||||
@@ -52,6 +55,9 @@ final class DiRepositories {
|
|||||||
/// Интерфейс для работы с репозиторием профиля
|
/// Интерфейс для работы с репозиторием профиля
|
||||||
late final IProfileRepository profileRepository;
|
late final IProfileRepository profileRepository;
|
||||||
|
|
||||||
|
/// Интерфейс для работы с репозиторием обновлений
|
||||||
|
late final IUpdateRepository updatesRepository;
|
||||||
|
|
||||||
/// Метод для инициализации репозиториев в приложении.
|
/// Метод для инициализации репозиториев в приложении.
|
||||||
///
|
///
|
||||||
/// Принимает:
|
/// Принимает:
|
||||||
@@ -68,6 +74,24 @@ final class DiRepositories {
|
|||||||
required OnError onError,
|
required OnError onError,
|
||||||
required DiContainer diContainer,
|
required DiContainer diContainer,
|
||||||
}) {
|
}) {
|
||||||
|
onProgress('Начинаем инициализацию репозиториев...');
|
||||||
|
try {
|
||||||
|
// Инициализация репозитория обновлений
|
||||||
|
updatesRepository = _lazyInitRepo<IUpdateRepository>(
|
||||||
|
mockFactory: UpdateMockRepository.new,
|
||||||
|
mainFactory: UpdateRepository.new,
|
||||||
|
onProgress: onProgress,
|
||||||
|
environment: diContainer.env,
|
||||||
|
);
|
||||||
|
onProgress(updatesRepository.name);
|
||||||
|
} on Object catch (error, stackTrace) {
|
||||||
|
onError(
|
||||||
|
'Ошибка инициализации репозитория IUpdateRepository',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Инициализация репозитория авторизации
|
// Инициализация репозитория авторизации
|
||||||
authRepository = _lazyInitRepo<IAuthRepository>(
|
authRepository = _lazyInitRepo<IAuthRepository>(
|
||||||
@@ -156,6 +180,7 @@ final class DiRepositories {
|
|||||||
required T Function() mockFactory,
|
required T Function() mockFactory,
|
||||||
required OnProgress onProgress,
|
required OnProgress onProgress,
|
||||||
}) {
|
}) {
|
||||||
|
// TODO(yura): https://github.com/smmarty/friflex_flutter_starter/issues/31 - добавить onError
|
||||||
final mockRepo = mockFactory();
|
final mockRepo = mockFactory();
|
||||||
final mainRepo = mainFactory();
|
final mainRepo = mainFactory();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||||
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
|
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
||||||
|
import 'package:friflex_starter/features/update/presentation/components/soft_modal_sheet.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_routes.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_type.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// {@template components_screen}
|
/// {@template components_screen}
|
||||||
/// Экран для демонстрации и тестирования компонентов приложения.
|
/// Экран для демонстрации и тестирования компонентов приложения.
|
||||||
@@ -66,6 +72,33 @@ class _ComponentsScreenState extends State<ComponentsScreen> {
|
|||||||
},
|
},
|
||||||
child: const Text('Показать снекбар с информацией'),
|
child: const Text('Показать снекбар с информацией'),
|
||||||
),
|
),
|
||||||
|
const HBox(16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
final updateCubitState = context.read<UpdateCubit>().state;
|
||||||
|
if (updateCubitState is UpdateSuccessState &&
|
||||||
|
updateCubitState.updateInfo.updateType == UpdateType.soft) {
|
||||||
|
SoftUpdateModal.show(
|
||||||
|
context,
|
||||||
|
updateEntity: updateCubitState.updateInfo,
|
||||||
|
onUpdate: () {
|
||||||
|
AppSnackBar.showSuccess(
|
||||||
|
context: context,
|
||||||
|
message: 'Начато обновление приложения',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Показать модальное окно обновления'),
|
||||||
|
),
|
||||||
|
const HBox(16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pushNamed(UpdateRoutes.hardUpdateScreenName);
|
||||||
|
},
|
||||||
|
child: const Text('Переход на экран Hard Update обновления'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
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_context_ext.dart';
|
||||||
import 'package:friflex_starter/app/app_env.dart';
|
import 'package:friflex_starter/app/app_env.dart';
|
||||||
import 'package:friflex_starter/features/debug/debug_routes.dart';
|
import 'package:friflex_starter/features/debug/debug_routes.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
||||||
|
import 'package:friflex_starter/features/update/presentation/components/soft_modal_sheet.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_type.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
/// {@template root_screen}
|
/// {@template root_screen}
|
||||||
@@ -13,7 +17,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
/// - Отображение кнопки отладки в не-продакшн окружениях
|
/// - Отображение кнопки отладки в не-продакшн окружениях
|
||||||
/// - Интеграцию с GoRouter для навигации
|
/// - Интеграцию с GoRouter для навигации
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class RootScreen extends StatelessWidget {
|
class RootScreen extends StatefulWidget {
|
||||||
/// {@macro root_screen}
|
/// {@macro root_screen}
|
||||||
const RootScreen({required this.navigationShell, super.key});
|
const RootScreen({required this.navigationShell, super.key});
|
||||||
|
|
||||||
@@ -21,6 +25,38 @@ class RootScreen extends StatelessWidget {
|
|||||||
/// Содержит информацию о текущем состоянии навигации
|
/// Содержит информацию о текущем состоянии навигации
|
||||||
final StatefulNavigationShell navigationShell;
|
final StatefulNavigationShell navigationShell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RootScreen> createState() => _RootScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RootScreenState extends State<RootScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// После построения виджета, проверяем состояние кубита обновлений
|
||||||
|
// и если есть обновление, то показываем модальное окно
|
||||||
|
_checkSoftUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверяет состояние кубита обновлений и показывает модальное окно при наличии мягкого обновления
|
||||||
|
void _checkSoftUpdate() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final updateState = context.read<UpdateCubit>().state;
|
||||||
|
|
||||||
|
// Проверяем только состояние успеха с доступной информацией об обновлении
|
||||||
|
if (updateState is UpdateSuccessState &&
|
||||||
|
updateState.updateInfo.updateType == UpdateType.soft) {
|
||||||
|
SoftUpdateModal.show(
|
||||||
|
context,
|
||||||
|
updateEntity: updateState.updateInfo,
|
||||||
|
onUpdate: () {
|
||||||
|
// TODO(yura): реализовать логику обновления приложения
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -32,14 +68,14 @@ class RootScreen extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: navigationShell,
|
body: widget.navigationShell,
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
items: const <BottomNavigationBarItem>[
|
items: const <BottomNavigationBarItem>[
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Главная'),
|
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Главная'),
|
||||||
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'),
|
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'),
|
||||||
],
|
],
|
||||||
currentIndex: navigationShell.currentIndex,
|
currentIndex: widget.navigationShell.currentIndex,
|
||||||
onTap: navigationShell.goBranch,
|
onTap: widget.navigationShell.goBranch,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
88
lib/features/update/README.md
Normal file
88
lib/features/update/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Модуль Hard/Soft Updates
|
||||||
|
|
||||||
|
Модуль для управления обновлениями приложения. Поддерживает мягкие (soft) и обязательные (hard) обновления.
|
||||||
|
|
||||||
|
## Ключевые сущности и состояния
|
||||||
|
|
||||||
|
- **`UpdateEntity`**: доменная сущность с данными об обновлении
|
||||||
|
- `availableVersion`: доступная версия
|
||||||
|
- `updateUrl`: ссылка на обновление
|
||||||
|
- `updateType`: тип (`soft` | `hard`), см. `UpdateType`
|
||||||
|
- `whatIsNew`: описание изменений
|
||||||
|
|
||||||
|
- **`UpdateType`**: перечисление типов обновления
|
||||||
|
- `UpdateType.soft`
|
||||||
|
- `UpdateType.hard`
|
||||||
|
- `UpdateType.none`
|
||||||
|
|
||||||
|
- **`UpdateCubit`**: управление состоянием проверки обновлений
|
||||||
|
- Состояния: `UpdateInitialState`, `UpdateLoadingState`, `UpdateSuccessState(UpdateEntity?)`, `UpdateErrorState(message)`
|
||||||
|
- Метод: `checkForUpdates({required String versionCode, required String platform})`
|
||||||
|
|
||||||
|
## Репозитории
|
||||||
|
|
||||||
|
- **`IUpdateRepository`**: Интерфейс, описывающий методы для проверки обновлений.
|
||||||
|
- Возвращает `Future<UpdateEntity>` (не может быть `null`)
|
||||||
|
|
||||||
|
- **`UpdateRepository`**: заготовка для реальной интеграции (бэкенд/стор)
|
||||||
|
- Реализуйте логику в `checkForUpdates`
|
||||||
|
|
||||||
|
- **`UpdateMockRepository`**: мок-реализация для разработки/демо
|
||||||
|
- Возвращает фиктивное обновление (по умолчанию soft)
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
- **Soft update** — `SoftUpdateModal`
|
||||||
|
- BottomSheet с заголовком, списком изменений и кнопками: «Отложить» и «Обновить»
|
||||||
|
- Статический метод `show` безопасно не откроет модалку, если `updateEntity == null`
|
||||||
|
|
||||||
|
Пример показа модального окна:
|
||||||
|
```dart
|
||||||
|
await SoftUpdateModal.show(
|
||||||
|
context,
|
||||||
|
updateEntity: updateEntity, // экземпляр UpdateEntity
|
||||||
|
onUpdate: () {
|
||||||
|
// TODO: переход в стор/браузер по updateEntity.updateUrl
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Hard update** — `HardUpdateScreen`
|
||||||
|
- Блокирующий экран, информирует и не даёт продолжить без обновления
|
||||||
|
|
||||||
|
## Роуты
|
||||||
|
|
||||||
|
- `UpdateRoutes.buildRoutes()` — регистрирует экран hard-обновления по пути `/update`
|
||||||
|
|
||||||
|
|
||||||
|
## Структура модуля
|
||||||
|
|
||||||
|
```
|
||||||
|
features/update/
|
||||||
|
├── data/
|
||||||
|
│ └── repository/
|
||||||
|
│ ├── update_repository.dart # реализация для интеграции
|
||||||
|
│ └── update_mock_repository.dart # мок-репозиторий
|
||||||
|
├── domain/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ └── update_entity.dart # доменная сущность
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ └── i_update_repository.dart # контракт репозитория
|
||||||
|
│ └── state/
|
||||||
|
│ └── cubit/
|
||||||
|
│ ├── update_cubit.dart # кубит и логика
|
||||||
|
│ └── update_state.dart # состояния
|
||||||
|
├── presentation/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── soft_modal_sheet.dart # модалка для soft-обновления
|
||||||
|
│ └── screens/
|
||||||
|
│ └── hard_update_screen.dart # экран для hard-обновления
|
||||||
|
├── update_type.dart # константы типов обновления
|
||||||
|
└── update_routes.dart # роут на hard-экран
|
||||||
|
```
|
||||||
|
|
||||||
|
## Заметки по реализации
|
||||||
|
|
||||||
|
- Для продакшена реализуйте переход в магазин или браузер по `updateUrl`
|
||||||
|
- Для soft-обновления не блокируйте UX; для hard — перенаправляйте на блокирующий экран
|
||||||
|
- Возвращайте `null` из репозитория, если обновлений нет
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_type.dart';
|
||||||
|
|
||||||
|
/// Мок обновления обязательное, можно использовать для тестирования
|
||||||
|
const mockHardUpdateEntity = UpdateEntity(
|
||||||
|
availableVersion: '2.0.0',
|
||||||
|
updateUrl: 'https://example.com/update',
|
||||||
|
updateType: UpdateType.hard,
|
||||||
|
whatIsNew: 'Добавлены новые функции и исправлены ошибки.',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Мок обновления мягкое, можно использовать для тестирования
|
||||||
|
const mockSoftUpdateEntity = UpdateEntity(
|
||||||
|
availableVersion: '2.0.0',
|
||||||
|
updateUrl: 'https://example.com/update',
|
||||||
|
updateType: UpdateType.soft,
|
||||||
|
whatIsNew: 'Добавлены новые функции и исправлены ошибки.',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// {@template UpdateMockRepository}
|
||||||
|
/// Репозиторий для моковой реализации проверки обновлений
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateMockRepository implements IUpdateRepository {
|
||||||
|
/// {@macro UpdateMockRepository}
|
||||||
|
const UpdateMockRepository();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UpdateEntity> checkForUpdates({
|
||||||
|
required String versionCode,
|
||||||
|
required String platform,
|
||||||
|
}) async {
|
||||||
|
// Имитация задержки для асинхронной операции
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// Возвращаем фиктивные данные об обновлении
|
||||||
|
// Можно возвращать [_mockHardUpdateEntity] или [_mockSoftUpdateEntity]
|
||||||
|
return mockSoftUpdateEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'UpdateMockRepository';
|
||||||
|
}
|
||||||
23
lib/features/update/data/repository/update_repository.dart
Normal file
23
lib/features/update/data/repository/update_repository.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
||||||
|
|
||||||
|
/// {@template UpdateRepository}
|
||||||
|
/// Репозиторий для реализации проверки обновлений
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateRepository implements IUpdateRepository {
|
||||||
|
/// {@macro UpdateRepository}
|
||||||
|
const UpdateRepository();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UpdateEntity> checkForUpdates({
|
||||||
|
required String versionCode,
|
||||||
|
required String platform,
|
||||||
|
}) {
|
||||||
|
// TODO: Реализовать реальную логику проверки обновлений
|
||||||
|
// Если обновления нет, возвращаем null
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'UpdateRepository';
|
||||||
|
}
|
||||||
35
lib/features/update/domain/entity/update_entity.dart
Normal file
35
lib/features/update/domain/entity/update_entity.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:friflex_starter/features/update/update_type.dart';
|
||||||
|
|
||||||
|
/// {@template UpdateEntity}
|
||||||
|
/// Сущность для представления информации об обновлении
|
||||||
|
/// {@endtemplate}
|
||||||
|
class UpdateEntity extends Equatable {
|
||||||
|
/// {@macro UpdateEntity}
|
||||||
|
const UpdateEntity({
|
||||||
|
required this.availableVersion,
|
||||||
|
required this.updateUrl,
|
||||||
|
required this.updateType,
|
||||||
|
required this.whatIsNew,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Доступная версия обновления
|
||||||
|
final String availableVersion;
|
||||||
|
|
||||||
|
/// URL для загрузки обновления
|
||||||
|
final String updateUrl;
|
||||||
|
|
||||||
|
/// Тип обновления (например, 'hard' или 'soft', или не требуется)
|
||||||
|
final UpdateType updateType;
|
||||||
|
|
||||||
|
/// Описание изменений в обновлении
|
||||||
|
final String whatIsNew;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
availableVersion,
|
||||||
|
updateUrl,
|
||||||
|
updateType,
|
||||||
|
whatIsNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:friflex_starter/di/di_base_repo.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
|
|
||||||
|
/// {@template IUpdateRepository}
|
||||||
|
/// Интерфейс репозитория для Hard&Soft обновлений
|
||||||
|
/// {@endtemplate}
|
||||||
|
abstract interface class IUpdateRepository with DiBaseRepo {
|
||||||
|
/// Проверяет наличие обновлений
|
||||||
|
/// [versionCode] - текущий код версии приложения
|
||||||
|
/// [platform] - платформа (например, 'android' или 'ios')
|
||||||
|
/// Возвращает [UpdateEntity] с информацией об обновлении
|
||||||
|
Future<UpdateEntity> checkForUpdates({
|
||||||
|
required String versionCode,
|
||||||
|
required String platform,
|
||||||
|
});
|
||||||
|
}
|
||||||
38
lib/features/update/domain/state/cubit/update_cubit.dart
Normal file
38
lib/features/update/domain/state/cubit/update_cubit.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/repository/i_update_repository.dart';
|
||||||
|
|
||||||
|
part 'update_state.dart';
|
||||||
|
|
||||||
|
/// {@template UpdateCubit}
|
||||||
|
/// Кубит для управления состояниями обновления приложения
|
||||||
|
/// {@endtemplate}
|
||||||
|
class UpdateCubit extends Cubit<UpdateState> {
|
||||||
|
/// {@macro UpdateCubit}
|
||||||
|
UpdateCubit(this._updatesRepository) : super(UpdateInitialState());
|
||||||
|
|
||||||
|
/// Репозиторий для проверки обновлений
|
||||||
|
final IUpdateRepository _updatesRepository;
|
||||||
|
|
||||||
|
/// Метод для проверки доступности обновлений
|
||||||
|
/// [versionCode] - текущий код версии приложения
|
||||||
|
/// [platform] - платформа (например, 'android' или 'ios')
|
||||||
|
Future<void> checkForUpdates({
|
||||||
|
required String versionCode,
|
||||||
|
required String platform,
|
||||||
|
}) async {
|
||||||
|
if (state is UpdateLoadingState) return;
|
||||||
|
emit(UpdateLoadingState());
|
||||||
|
try {
|
||||||
|
final updateInfo = await _updatesRepository.checkForUpdates(
|
||||||
|
versionCode: versionCode,
|
||||||
|
platform: platform,
|
||||||
|
);
|
||||||
|
emit(UpdateSuccessState(updateInfo));
|
||||||
|
} on Object catch (e, st) {
|
||||||
|
emit(UpdateErrorState(e.toString()));
|
||||||
|
addError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/features/update/domain/state/cubit/update_state.dart
Normal file
56
lib/features/update/domain/state/cubit/update_state.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
part of 'update_cubit.dart';
|
||||||
|
|
||||||
|
/// {@template UpdateState}
|
||||||
|
/// Состояния для управления процессом обновления приложения
|
||||||
|
/// {@endtemplate}
|
||||||
|
sealed class UpdateState extends Equatable {
|
||||||
|
/// {@macro UpdateState}
|
||||||
|
const UpdateState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template UpdateInitialState}
|
||||||
|
/// Состояние начальной инициализации
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateInitialState extends UpdateState {
|
||||||
|
/// {@macro UpdateInitialState}
|
||||||
|
const UpdateInitialState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template UpdateLoadingState}
|
||||||
|
/// Состояние загрузки информации об обновлении
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateLoadingState extends UpdateState {
|
||||||
|
/// {@macro UpdateLoadingState}
|
||||||
|
const UpdateLoadingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template UpdateSuccessState}
|
||||||
|
/// Состояние успешного получения информации об обновлении
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateSuccessState extends UpdateState {
|
||||||
|
/// {@macro UpdateSuccessState}
|
||||||
|
const UpdateSuccessState(this.updateInfo);
|
||||||
|
|
||||||
|
/// Информация об обновлении
|
||||||
|
final UpdateEntity updateInfo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [updateInfo];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template UpdateErrorState}
|
||||||
|
/// Состояние ошибки при получении информации об обновлении
|
||||||
|
/// {@endtemplate}
|
||||||
|
final class UpdateErrorState extends UpdateState {
|
||||||
|
/// {@macro UpdateErrorState}
|
||||||
|
const UpdateErrorState(this.message);
|
||||||
|
|
||||||
|
/// Сообщение об ошибке в UI
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [message];
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/entity/update_entity.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// {@template soft_update_modal}
|
||||||
|
/// Модальное окно для уведомления о доступности новой версии приложения.
|
||||||
|
///
|
||||||
|
/// Отвечает за:
|
||||||
|
/// - Отображение информации о новой версии приложения
|
||||||
|
/// - Предоставление возможности обновления или отложения
|
||||||
|
/// - Показ описания изменений в новой версии
|
||||||
|
/// - Мягкое уведомление пользователя без принуждения к обновлению
|
||||||
|
/// {@endtemplate}
|
||||||
|
class SoftUpdateModal extends StatelessWidget {
|
||||||
|
/// {@macro soft_update_modal}
|
||||||
|
const SoftUpdateModal({required this.updateEntity, this.onUpdate, super.key});
|
||||||
|
|
||||||
|
/// Информация об обновлении
|
||||||
|
final UpdateEntity updateEntity;
|
||||||
|
|
||||||
|
/// Обратный вызов при нажатии "Обновить"
|
||||||
|
final VoidCallback? onUpdate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Заголовок
|
||||||
|
Text(
|
||||||
|
'Доступна новая версия: ${updateEntity.availableVersion} ',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
|
||||||
|
const HBox(16),
|
||||||
|
|
||||||
|
// Описание изменений
|
||||||
|
Text(
|
||||||
|
'Что нового:',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const HBox(8),
|
||||||
|
Text(
|
||||||
|
updateEntity.whatIsNew,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
|
||||||
|
const HBox(24),
|
||||||
|
|
||||||
|
// Кнопки действий
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: const Text('Отложить'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const WBox(12),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
onUpdate?.call();
|
||||||
|
},
|
||||||
|
child: const Text('Обновить'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Показать модальное окно обновления
|
||||||
|
///
|
||||||
|
/// [context] - контекст для отображения модального окна
|
||||||
|
/// [updateEntity] - информация об обновлении
|
||||||
|
/// [onUpdate] - функция при нажатии "Обновить"
|
||||||
|
static Future<void> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required UpdateEntity updateEntity,
|
||||||
|
VoidCallback? onUpdate,
|
||||||
|
}) {
|
||||||
|
return showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) =>
|
||||||
|
SoftUpdateModal(updateEntity: updateEntity, onUpdate: onUpdate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||||
|
import 'package:friflex_starter/features/update/domain/state/cubit/update_cubit.dart';
|
||||||
|
|
||||||
|
/// Блокирующий экран для обязательного обновления приложения
|
||||||
|
class HardUpdateScreen extends StatelessWidget {
|
||||||
|
const HardUpdateScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Hard Обновление')),
|
||||||
|
body: Center(
|
||||||
|
child: BlocBuilder<UpdateCubit, UpdateState>(
|
||||||
|
builder: (context, updateCubitState) {
|
||||||
|
final updateEntity = updateCubitState is UpdateSuccessState
|
||||||
|
? updateCubitState.updateInfo
|
||||||
|
: null;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Доступна новая версия приложения. Пожалуйста, обновите его.',
|
||||||
|
),
|
||||||
|
const HBox(16),
|
||||||
|
Text(
|
||||||
|
'Доступная версия: ${updateEntity?.availableVersion ?? ''}',
|
||||||
|
),
|
||||||
|
const HBox(8),
|
||||||
|
Text('Что нового: ${updateEntity?.whatIsNew ?? ''}'),
|
||||||
|
const HBox(8),
|
||||||
|
Text('Тип обновления: ${updateEntity?.updateType ?? ''}'),
|
||||||
|
const HBox(8),
|
||||||
|
Text('URL для обновления: ${updateEntity?.updateUrl ?? ''}'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/features/update/update_routes.dart
Normal file
20
lib/features/update/update_routes.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:friflex_starter/features/update/presentation/screens/hard_update_screen.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
abstract final class UpdateRoutes {
|
||||||
|
/// Название роута главной страницы
|
||||||
|
static const String hardUpdateScreenName = 'update_screen';
|
||||||
|
|
||||||
|
/// Путь роута экрана обновления
|
||||||
|
static const String _hardUpdateScreenPath = '/update';
|
||||||
|
|
||||||
|
/// Метод для построения роутов по Update
|
||||||
|
///
|
||||||
|
/// Принимает:
|
||||||
|
/// - [routes] - вложенные роуты
|
||||||
|
static GoRoute buildRoutes({List<RouteBase> routes = const []}) => GoRoute(
|
||||||
|
path: _hardUpdateScreenPath,
|
||||||
|
name: hardUpdateScreenName,
|
||||||
|
builder: (context, state) => const HardUpdateScreen(),
|
||||||
|
);
|
||||||
|
}
|
||||||
13
lib/features/update/update_type.dart
Normal file
13
lib/features/update/update_type.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// {@template UpdateType}
|
||||||
|
/// Тип обновления
|
||||||
|
/// {@endtemplate}
|
||||||
|
enum UpdateType {
|
||||||
|
/// Обязательное обновление
|
||||||
|
hard,
|
||||||
|
|
||||||
|
/// Мягкое обновление
|
||||||
|
soft,
|
||||||
|
|
||||||
|
/// Не требуется обновление
|
||||||
|
none,
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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/profile/presentation/profile_routes.dart';
|
||||||
import 'package:friflex_starter/features/root/root_screen.dart';
|
import 'package:friflex_starter/features/root/root_screen.dart';
|
||||||
import 'package:friflex_starter/features/splash/splash_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';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
/// {@template app_router}
|
/// {@template app_router}
|
||||||
@@ -42,6 +43,7 @@ class AppRouter {
|
|||||||
path: '/splash',
|
path: '/splash',
|
||||||
builder: (context, state) => const SplashScreen(),
|
builder: (context, state) => const SplashScreen(),
|
||||||
),
|
),
|
||||||
|
UpdateRoutes.buildRoutes(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,9 +125,6 @@ class AppRunner {
|
|||||||
required AppEnv env,
|
required AppEnv env,
|
||||||
required TimerRunner timerRunner,
|
required TimerRunner timerRunner,
|
||||||
}) async {
|
}) async {
|
||||||
// Имитация задержки инициализации
|
|
||||||
// TODO(yura): Удалить после проверки
|
|
||||||
await Future.delayed(const Duration(seconds: 3));
|
|
||||||
debugService.log(() => 'Тип сборки: ${env.name}');
|
debugService.log(() => 'Тип сборки: ${env.name}');
|
||||||
final diContainer = DiContainer(env: env, dService: debugService);
|
final diContainer = DiContainer(env: env, dService: debugService);
|
||||||
await diContainer.init(
|
await diContainer.init(
|
||||||
|
|||||||
Reference in New Issue
Block a user