refactor(full refactor): Рефакторинг стартера (#8)

This commit is contained in:
Yuri Petrov
2025-04-27 17:08:34 +03:00
committed by GitHub
parent 18eb7b1fe1
commit 5d7d29ecf8
206 changed files with 1065 additions and 20102 deletions

View File

@@ -5,6 +5,7 @@ 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/l10n/gen/app_localizations.dart';

17
lib/app/app_box.dart Normal file
View File

@@ -0,0 +1,17 @@
import 'package:flutter/widgets.dart';
/// {@template h_box}
/// HBox виджет для вертикального отступа (Надстройка над SizedBox)
/// {@endtemplate}
class HBox extends SizedBox {
/// {@macro h_box}
const HBox(double height, {super.key}) : super(height: height);
}
/// {@template w_box}
/// WBox виджет для вертикального отступа (Надстройка над SizedBox)
/// {@endtemplate}
class WBox extends SizedBox {
/// {@macro w_box}
const WBox(double width, {super.key}) : super(width: width);
}

View File

@@ -2,6 +2,7 @@ import 'package:envied/envied.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart';
part 'app_config.g.dart';
/// Класс для реализации конфигурации с моковыми данными

View File

@@ -1,9 +1,12 @@
import 'package:dio/dio.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
/// Класс для реализации HTTP-клиента для управления запросами
/// {@template app_http_client}
/// Класс для реализации HTTP-клиента для управления запросами
/// {@endtemplate}
final class AppHttpClient implements IHttpClient {
/// Создает HTTP клиент
///
@@ -26,6 +29,7 @@ final class AppHttpClient implements IHttpClient {
'Content-Type': 'application/json',
};
debugService.log('HTTP client created');
_httpClient.interceptors.add(debugService.dioLogger);
}
/// Конфигурация приложения

View File

@@ -10,7 +10,8 @@ import 'package:friflex_starter/features/debug/i_debug_service.dart';
/// {@template dependencies_container}
/// Контейнер для зависимостей
/// {@macro composition_process}
/// [env] - окружение приложения
/// [debugService] - сервис для отладки
/// {@endtemplate}
final class DiContainer {
/// {@macro dependencies_container}

View File

@@ -11,9 +11,6 @@ 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_repository.dart';
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
import 'package:friflex_starter/features/profile_scope/data/repository/profile_scope_mock_repository.dart';
import 'package:friflex_starter/features/profile_scope/data/repository/profile_scope_repository.dart';
import 'package:friflex_starter/features/profile_scope/domain/repository/i_profile_scope_repository.dart';
/// Список названий моковых репозиториев, которые должны быть подменены
/// для использования в сборке stage окружения
@@ -43,8 +40,6 @@ final class DiRepositories {
/// Интерфейс для работы с репозиторием профиля
late final IProfileRepository profileRepository;
/// Интерфейс для работы с репозиторием профиля scope
late final IProfileScopeRepository profileScopeRepository;
/// Метод для инициализации репозиториев в приложении
///
@@ -121,29 +116,7 @@ final class DiRepositories {
stackTrace,
);
}
try {
// Инициализация репозитория профиля scope
profileScopeRepository = _lazyInitRepo<IProfileScopeRepository>(
mockFactory: ProfileScopeMockRepository.new,
mainFactory: () => ProfileScopeRepository(
httpClient: diContainer.httpClientFactory(
diContainer.debugService,
diContainer.appConfig,
),
),
onProgress: onProgress,
environment: diContainer.env,
);
onProgress(mainRepository.name);
} on Object catch (error, stackTrace) {
onError(
'Ошибка инициализации репозитория IProfileScopeRepository',
error,
stackTrace,
);
}
onProgress(
'Инициализация репозиториев завершена! Было подменено репозиториев - ${_mockReposToSwitch.length} (${_mockReposToSwitch.join(', ')})',
);

View File

@@ -23,7 +23,7 @@ final class DiServices {
required DiContainer diContainer,
}) {
try {
pathProvider = AppPathProvider();
pathProvider = const AppPathProvider();
onProgress(AppPathProvider.name);
} on Object catch (error, stackTrace) {
onError(

View File

@@ -1,7 +1,7 @@
import '../../domain/repository/i_auth_repository.dart';
/// {@template AuthMockRepository}
///
/// Mock реализация репозитория авторизации
/// {@endtemplate}
final class AuthMockRepository implements IAuthRepository {
@override

View File

@@ -3,7 +3,7 @@ import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_auth_repository.dart';
/// {@template AuthRepository}
///
/// Реализация репозитория авторизации
/// {@endtemplate}
final class AuthRepository implements IAuthRepository {
final IHttpClient httpClient;

View File

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

View File

@@ -1,31 +1,66 @@
import 'package:flutter/widgets.dart';
import 'package:friflex_starter/features/debug/debug_screen.dart';
import 'package:friflex_starter/features/debug/screens/debug_screen.dart';
import 'package:friflex_starter/features/debug/screens/icons_screen.dart';
import 'package:friflex_starter/features/debug/screens/lang_screen.dart';
import 'package:friflex_starter/features/debug/screens/theme_screen.dart';
import 'package:friflex_starter/features/debug/screens/tokens_screen.dart';
import 'package:friflex_starter/features/debug/screens/ui_kit_screen.dart';
import 'package:go_router/go_router.dart';
/// {@template debug_routes}
/// Роуты для отладки приложения
/// [buildRoutes] - метод для создания роутов
/// {@endtemplate}
abstract final class DebugRoutes {
/// Название роута страницы профиля пользователя
/// Название экранов
static const String debugScreenName = 'debug_screen';
static const String tokensScreenName = 'tokens_screen';
static const String uiKitScreenName = 'ui_kit_screen';
static const String iconsScreenName = 'icons_screen';
static const String themeScreenName = 'theme_screen';
static const String langScreenName = 'lang_screen';
/// Путь роута страницы профиля пользователя
static const String _debugScreenPath = '/debug';
/// Пути к экранам
static const String debugScreenPath = '/debug';
static const String tokensScreenPath = 'debug/tokens';
static const String uiKitScreenPath = 'debug/ui_kit';
static const String iconsScreenPath = 'debug/icons';
static const String themeScreenPath = 'debug/theme';
static const String langScreenPath = 'debug/lang';
/// Метод для построения ветки роутов по фиче профиля пользователя
/// Метод для создания роутов для отладки
///
/// Принимает:
/// - [routes] - вложенные роуты
static StatefulShellBranch buildShellBranch({
List<RouteBase> routes = const [],
List<NavigatorObserver>? observers,
}) =>
StatefulShellBranch(
initialLocation: _debugScreenPath,
observers: observers,
static GoRoute buildRoutes({List<RouteBase> routes = const []}) => GoRoute(
path: debugScreenPath,
name: debugScreenName,
builder: (context, state) => const DebugScreen(),
routes: [
...routes,
GoRoute(
path: _debugScreenPath,
name: debugScreenName,
builder: (context, state) => const DebugScreen(),
routes: routes,
path: tokensScreenPath,
name: tokensScreenName,
builder: (context, state) => const TokensScreen(),
),
GoRoute(
path: uiKitScreenPath,
name: uiKitScreenName,
builder: (context, state) => const UiKitScreen(),
),
GoRoute(
path: iconsScreenPath,
name: iconsScreenName,
builder: (context, state) => const IconsScreen(),
),
GoRoute(
path: themeScreenPath,
name: themeScreenName,
builder: (context, state) => const ThemeScreen(),
),
GoRoute(
path: langScreenPath,
name: langScreenName,
builder: (context, state) => const LangScreen(),
),
],
);

View File

@@ -1,118 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
import 'package:friflex_starter/gen/assets.gen.dart';
import 'package:friflex_starter/gen/fonts.gen.dart';
class DebugScreen extends StatelessWidget {
const DebugScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Debug Screen')),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Реализация SecureStorage: ${context.di.services.secureStorage.nameImpl}',
),
const SizedBox(height: 16),
Text(
'Окружение: ${context.di.appConfig.env.name}',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.theme.changeTheme();
},
child: const Text('Сменить тему'),
),
const SizedBox(height: 16),
ColoredBox(
color: context.colors.testColor,
child: const SizedBox(height: 100, width: 100),
),
const SizedBox(height: 16),
Text(
'Текущая тема: ${context.theme.themeMode}',
),
const SizedBox(height: 16),
Text(
'Текущий репозиторий: ${context.di.repositories.authRepository.name}',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('ru', 'RU'),
);
},
child: const Text('Сменить язык на Rусский'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('en', 'EN'),
);
},
child: const Text('Сменить язык на Английский'),
),
const SizedBox(height: 16),
Text(
'Тестовое слово montserrat bold: ${context.l10n.helloWorld}',
style: TextStyle(
color: context.colors.testColor,
fontFamily: Assets.fonts.montserratBold,
),
),
const SizedBox(height: 16),
Text(
'Тестовое слово montserrat medium: ${context.l10n.helloWorld}',
style: TextStyle(
color: context.colors.testColor,
fontFamily: FontFamily.montserrat,
),
),
const SizedBox(height: 16),
Text(
'Текущий язык: ${context.l10n.localeName}',
),
const SizedBox(height: 16),
const Text('Тестовая иконка из assets'),
const SizedBox(height: 16),
Assets.icons.home.svg(),
ElevatedButton(
onPressed: () {
throw Exception(
'Тестовая ошибка Exception для отладки FlutterError',
);
},
child: const Text('Вызывать ошибку FlutterError'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await _callError();
},
child: const Text('Вызывать ошибку PlatformDispatcher'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
await context.di.debugService.openDebugScreen(context);
},
child: const Text('Вызывать Экран отладки'),
),
],
),
),
);
}
Future<void> _callError() async {
throw Exception('Тестовая ошибка Exception для отладки PlatformDispatcher');
}
}

View File

@@ -1,19 +1,57 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart';
import 'package:talker_bloc_logger/talker_bloc_logger_observer.dart';
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
import 'package:talker_flutter/talker_flutter.dart';
/// Класс реализации интерфейса Debug сервиса
/// {@template debug_service}
/// Реализация сервиса отладки, с помощью Talker
/// {@endtemplate}
class DebugService implements IDebugService {
/// {@macro debug_service}
DebugService() {
_talker = TalkerFlutter.init();
_talkerDioLogger = TalkerDioLogger(talker: _talker);
_talkerRouteObserver = TalkerRouteObserver(_talker);
_talkerBlocObserver = TalkerBlocObserver(talker: _talker);
}
/// Наименование сервиса
static const name = 'DebugService';
@override
void logDebug(Object message, {Object? logLevel, Map<String, dynamic>? args}) {
if (kDebugMode) {
print('Message: $message');
}
/// Реализация Talker
late final Talker _talker;
/// Реализация логики
/// Реализация TalkerDioLogger
late final TalkerDioLogger _talkerDioLogger;
/// Реализация TalkerRouteObserver
late final TalkerRouteObserver _talkerRouteObserver;
/// Реализация TalkerBlocLoggerObserver
late final TalkerBlocObserver _talkerBlocObserver;
/// Получает TalkerDioLoggerInterceptor
@override
TalkerDioLogger get dioLogger => _talkerDioLogger;
/// Получает TalkerRouteObserver\
@override
TalkerRouteObserver get routeObserver => _talkerRouteObserver;
/// Получает TalkerBlocObserver
@override
TalkerBlocObserver get blocObserver => _talkerBlocObserver;
@override
void logDebug(
Object message, {
Object? logLevel,
Map<String, dynamic>? args,
}) {
_talker.debug(
message is Function ? Function.apply(message, []) as Object : message,
);
}
@override
@@ -24,43 +62,34 @@ class DebugService implements IDebugService {
Map<String, dynamic>? args,
StackTrace? stackTrace,
}) {
final logMessage = message is Function ? Function.apply(message, []) as Object : message;
if (kDebugMode) {
print('Message: $logMessage');
print('Error: $error');
print('StackTrace: $stackTrace');
}
/// Реализация логики
final logMessage =
message is Function ? Function.apply(message, []) as Object : message;
_talker.error(logMessage, error, stackTrace);
}
@override
void log(Object message, {Object? logLevel, Map<String, dynamic>? args}) {
final logMessage = message is Function ? Function.apply(message, []) as Object : message;
if (kDebugMode) {
print('Message: $logMessage');
}
/// Реализация логики
final logMessage =
message is Function ? Function.apply(message, []) as Object : message;
_talker.log(logMessage);
}
@override
void logWarning(Object message, {Object? logLevel, Map<String, dynamic>? args}) {
final logMessage = message is Function ? Function.apply(message, []) as Object : message;
if (kDebugMode) {
print('Message: $logMessage');
}
/// Реализация логики
void logWarning(
Object message, {
Object? logLevel,
Map<String, dynamic>? args,
}) {
final logMessage =
message is Function ? Function.apply(message, []) as Object : message;
_talker.warning(logMessage);
}
@override
Future<T?> openDebugScreen<T>(BuildContext context, {bool useRootNavigator = false}) {
if (kDebugMode) {
print('Переход на страницу отладки');
}
/// Реализация логики
return Future.value();
Future<void> openDebugScreen(BuildContext context,
{bool useRootNavigator = false,}) async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TalkerScreen(talker: _talker)),
);
}
}

View File

@@ -4,6 +4,15 @@ import 'package:flutter/material.dart';
abstract interface class IDebugService {
static const name = 'IDebugService';
/// Наблюдение за dio
dynamic get dioLogger;
/// Наблюдение за роутами
dynamic get routeObserver;
/// Наблюдение за BLoC
dynamic get blocObserver;
/// Метод для логирования сообщений
void log(
Object message, {
@@ -39,7 +48,7 @@ abstract interface class IDebugService {
/// Принимает:
/// - [context] - для определения навигатора по нему
/// - [useRootNavigator] - при true, открывает окно в корневом навигаторе
Future<T?> openDebugScreen<T>(
Future<void> openDebugScreen(
BuildContext context, {
bool useRootNavigator = false,
});

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_box.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart';
import 'package:go_router/go_router.dart';
class DebugScreen extends StatelessWidget {
const DebugScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Debug Screen')),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Окружение: ${context.di.appConfig.env.name}',
),
const HBox(22),
Text(
'Реализация AppServices: ${context.di.services.secureStorage.nameImpl}',
),
const HBox(22),
ElevatedButton(
onPressed: () async {
await context.di.debugService.openDebugScreen(context);
},
child: const Text('Вызывать Экран отладки'),
),
const HBox(22),
const Text('Экраны для отладки:'),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.iconsScreenName);
},
child: const Text('Экран с иконками'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.themeScreenName);
},
child: const Text('Экран настроек темы'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.tokensScreenName);
},
child: const Text('Экран с токенами'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.uiKitScreenName);
},
child: const Text('Экран UI Kit'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.langScreenName);
},
child: const Text('Экран локализации'),
),
const HBox(22),
const Text('Имитирование ошибок:'),
const HBox(16),
ElevatedButton(
onPressed: () {
throw Exception(
'Тестовая ошибка Exception для отладки FlutterError',
);
},
child: const Text('Вызывать ошибку FlutterError'),
),
const HBox(16),
ElevatedButton(
onPressed: () async {
await _callError();
},
child: const Text('Вызывать ошибку PlatformDispatcher'),
),
],
),
),
);
}
Future<void> _callError() async {
throw Exception('Тестовая ошибка Exception для отладки PlatformDispatcher');
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_box.dart';
import 'package:friflex_starter/gen/assets.gen.dart';
/// {@template IconsScreen}
/// Экран для отрисовки иконок
/// {@endtemplate}
class IconsScreen extends StatelessWidget {
/// {@macro IconsScreen}
const IconsScreen({super.key});
@override
Widget build(BuildContext context) {
final iconList = Assets.icons.values
.map(
(icon) => _ItemIcon(icon: icon.svg(), name: icon.path),
)
.toList();
return Scaffold(
appBar: AppBar(title: const Text('Иконки')),
body: Center(
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return iconList[index];
},
separatorBuilder: (context, index) => const Divider(),
itemCount: iconList.length,
),
),
);
}
}
// Приватный класс для реализации элемента списка иконок
class _ItemIcon extends StatelessWidget {
/// Создает экземпляр элемента списка иконок
///
/// Принимает:
/// - [icon] - иконка
/// - [name] - название иконки
const _ItemIcon({required this.icon, required this.name});
/// Иконка
final Widget icon;
/// Название иконки
final String name;
@override
Widget build(BuildContext context) {
return Row(children: [icon, const WBox(16), Text(name)]);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
import 'package:friflex_starter/gen/assets.gen.dart';
import 'package:friflex_starter/gen/fonts.gen.dart';
/// {@template LangScreen}
/// Экран для отладки языков приложения
/// {@endtemplate}
class LangScreen extends StatelessWidget {
/// {@macro LangScreen}
const LangScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lang')),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('ru', 'RU'),
);
},
child: const Text('Сменить язык на Rусский'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.localization.changeLocal(
const Locale('en', 'EN'),
);
},
child: const Text('Сменить язык на Английский'),
),
const SizedBox(height: 16),
Text(
'Тестовое слово bold: ${context.l10n.helloWorld}',
style: TextStyle(
color: context.colors.testColor,
fontFamily: Assets.fonts.montserratBold,
),
),
const SizedBox(height: 16),
Text(
'Тестовое слово medium: ${context.l10n.helloWorld}',
style: TextStyle(
color: context.colors.testColor,
fontFamily: FontFamily.montserrat,
),
),
const SizedBox(height: 16),
Text(
'Текущий язык: ${context.l10n.localeName}',
),
],
),
),
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
/// {@template ThemeScreen}
/// Экран для отладки темы приложения
/// {@endtemplate}
class ThemeScreen extends StatelessWidget {
/// {@macro ThemeScreen}
const ThemeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Theme')),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
ElevatedButton(
onPressed: () {
context.theme.changeTheme();
},
child: const Text('Сменить тему'),
),
const SizedBox(height: 16),
ColoredBox(
color: context.colors.testColor,
child: const SizedBox(height: 100, width: 100),
),
const SizedBox(height: 16),
Text(
'Текущая тема: ${context.theme.themeMode}',
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
/// {@template TokensScreen}
/// Экран для отображения токенов
/// {@endtemplate}
class TokensScreen extends StatelessWidget {
/// {@macro TokensScreen}
const TokensScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Tokens')),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
Text(
'Access Token: ',
),
SizedBox(height: 16),
Text(
'Refresh Token: ',
),
SizedBox(height: 16),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
/// {@template UiKitScreen}
/// Экран для отрисовки UI Kit
/// и тестирования его компонентов.
/// {@endtemplate}
class UiKitScreen extends StatelessWidget {
/// {@macro UiKitScreen}
const UiKitScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('UI Kit Screen'),
),
body: Center(
child: ListView(
padding: const EdgeInsets.all(16),
children: const [
Text(
'UI Kit Screen',
),
SizedBox(height: 16),
// Здесь можно добавить другие компоненты UI Kit
],
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:friflex_starter/features/main/presentation/screens/main_detail_screen.dart';
import 'package:friflex_starter/features/main/presentation/screens/main_screen.dart';
import 'package:go_router/go_router.dart';
@@ -6,9 +7,15 @@ abstract final class MainRoutes {
/// Название роута главной страницы
static const String mainScreenName = 'main_screen';
/// Название роута экрана с деталями
static const String mainDetailScreenName = 'main_detail_screen';
/// Путь роута страницы профиля пользователя
static const String _mainScreenPath = '/main';
/// Путь роута экрана с деталями
static const String _mainDetailScreenPath = '/main/detail';
/// Метод для построения ветки роутов по фиче профиля пользователя
///
/// Принимает:
@@ -21,11 +28,19 @@ abstract final class MainRoutes {
initialLocation: _mainScreenPath,
observers: observers,
routes: [
...routes,
GoRoute(
path: _mainScreenPath,
name: mainScreenName,
builder: (context, state) => const MainScreen(),
routes: routes,
routes: [
// Пример вложенного роута для главного экрана
GoRoute(
path: _mainDetailScreenPath,
name: mainDetailScreenName,
builder: (context, state) => const MainDetailScreen(),
),
],
),
],
);

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
/// {@template MainDetailScreen}
/// Вложенный экран для главного экрана приложения
/// {@endtemplate}
class MainDetailScreen extends StatelessWidget {
/// {@macro MainDetailScreen}
const MainDetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main Detail Screen'),
),
body: const Center(
child: Text('Вложенный экран'),
),
);
}
}

View File

@@ -1,7 +1,13 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_box.dart';
import 'package:friflex_starter/features/main/presentation/main_routes.dart';
import 'package:go_router/go_router.dart';
/// {@template MainScreen}
/// Главный экран приложения
/// {@endtemplate}
class MainScreen extends StatelessWidget {
/// {@macro MainScreen}
const MainScreen({super.key});
@override
@@ -12,20 +18,16 @@ class MainScreen extends StatelessWidget {
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Главный экран приложения'),
const HBox(16),
ElevatedButton(
onPressed: () {
context.push('/profile');
// Переход на экран с деталями
context.pushNamed(MainRoutes.mainDetailScreenName);
},
child: const Text('Открыть профиль'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.push('/profile_scope');
},
child: const Text('Открыть профиль с областью видимости'),
child: const Text('Переход на экран с деталями'),
),
],
),

View File

@@ -0,0 +1,32 @@
import 'package:flutter/widgets.dart';
import 'package:friflex_starter/features/profile/presentation/screens/profile_screen.dart';
import 'package:go_router/go_router.dart';
abstract final class ProfileRoutes {
/// Название роута главной страницы
static const String profileScreenName = 'profile_screen';
/// Путь роута страницы профиля пользователя
static const String _profileScreenPath = '/profile';
/// Метод для построения ветки роутов по фиче профиля пользователя
///
/// Принимает:
/// - [routes] - вложенные роуты
static StatefulShellBranch buildShellBranch({
List<RouteBase> routes = const [],
List<NavigatorObserver>? observers,
}) =>
StatefulShellBranch(
initialLocation: _profileScreenPath,
observers: observers,
routes: [
GoRoute(
path: _profileScreenPath,
name: profileScreenName,
builder: (context, state) => const ProfileScreen(),
routes: routes,
),
],
);
}

View File

@@ -1,14 +0,0 @@
import '../../domain/repository/i_profile_scope_repository.dart';
/// {@template ProfileScopeMockRepository}
///
/// {@endtemplate}
final class ProfileScopeMockRepository implements IProfileScopeRepository {
@override
String get name => 'ProfileScopeMockRepository';
@override
Future<String> fetchUserProfile(String id) async {
return 'MOCK Yura Petrov';
}
}

View File

@@ -1,24 +0,0 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_profile_scope_repository.dart';
/// {@template ProfileScopeRepository}
///
/// {@endtemplate}
final class ProfileScopeRepository implements IProfileScopeRepository {
final IHttpClient httpClient;
ProfileScopeRepository({required this.httpClient});
@override
String get name => 'ProfileScopeRepository';
@override
Future<String> fetchUserProfile(String id) async {
// Какой-то запрос к серверу
await Future.delayed(const Duration(seconds: 1));
// httpClient.get('https://example.com/profile/$id');
return 'Yura Petrov';
}
}

View File

@@ -1,41 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/features/profile_scope/domain/repository/i_profile_scope_repository.dart';
part 'profile_scope_event.dart';
part 'profile_scope_state.dart';
class ProfileScopeBloc extends Bloc<ProfileScopeEvent, ProfileScopeState> {
ProfileScopeBloc({required IProfileScopeRepository profileRepository})
: _profileRepository = profileRepository,
super(ProfileScopeInitialState()) {
// Вам необходимо добавлять только
// один обработчик событий в конструкторе
on<ProfileScopeEvent>((event, emit) async {
return switch (event) {
ProfileScopeFetchProfileEvent() => await _fetchProfile(event, emit),
};
});
}
final IProfileScopeRepository _profileRepository;
Future<void> _fetchProfile(
ProfileScopeFetchProfileEvent event,
Emitter<ProfileScopeState> emit,
) async {
try {
emit(ProfileScopeWaitingState());
final data = await _profileRepository.fetchUserProfile(event.id);
emit(ProfileScopeSuccessState(data: data));
} on Object catch (error, stackTrace) {
emit(
ProfileScopeErrorState(
message: 'Ошибка при загрузке профиля',
error: error,
stackTrace: stackTrace,
),
);
}
}
}

View File

@@ -1,17 +0,0 @@
part of 'profile_scope_bloc.dart';
sealed class ProfileScopeEvent extends Equatable {
const ProfileScopeEvent();
@override
List<Object> get props => [];
}
final class ProfileScopeFetchProfileEvent extends ProfileScopeEvent {
final String id;
const ProfileScopeFetchProfileEvent({required this.id});
@override
List<Object> get props => [id];
}

View File

@@ -1,36 +0,0 @@
part of 'profile_scope_bloc.dart';
sealed class ProfileScopeState extends Equatable {
const ProfileScopeState();
@override
List<Object> get props => [];
}
final class ProfileScopeInitialState extends ProfileScopeState {}
final class ProfileScopeWaitingState extends ProfileScopeState {}
final class ProfileScopeErrorState extends ProfileScopeState {
final String message;
final Object error;
final StackTrace? stackTrace;
const ProfileScopeErrorState({
required this.message,
required this.error,
this.stackTrace,
});
@override
List<Object> get props => [message, error];
}
final class ProfileScopeSuccessState extends ProfileScopeState {
final Object data;
const ProfileScopeSuccessState({required this.data});
@override
List<Object> get props => [data];
}

View File

@@ -1,8 +0,0 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IProfileScopeRepository}
///
/// {@endtemplate}
abstract interface class IProfileScopeRepository with DiBaseRepo {
Future<String> fetchUserProfile(String id);
}

View File

@@ -1,77 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/features/profile_scope/domain/bloc/profile_scope_bloc.dart';
class ProfileInheritedScope extends InheritedWidget {
const ProfileInheritedScope({
required this.profileScopeBloc,
required super.child,
super.key,
});
final ProfileScopeBloc profileScopeBloc;
@override
bool updateShouldNotify(ProfileInheritedScope oldWidget) =>
profileScopeBloc != oldWidget.profileScopeBloc;
}
class ProfileScope extends StatefulWidget {
const ProfileScope({
required this.child,
super.key,
});
final Widget child;
static ProfileInheritedScope? maybeOf(
BuildContext context, {
bool listen = false,
}) {
return listen
? context.dependOnInheritedWidgetOfExactType<ProfileInheritedScope>()
: context.getInheritedWidgetOfExactType<ProfileInheritedScope>();
}
static ProfileInheritedScope of(
BuildContext context, {
bool listen = false,
}) {
final result = maybeOf(context, listen: listen);
if (result == null) {
throw StateError(
'ProfileScope is not found above widget ${context.widget}',
);
}
return result;
}
@override
State<StatefulWidget> createState() => _ProfileScopeState();
}
class _ProfileScopeState extends State<ProfileScope> {
late final ProfileScopeBloc _profileScopeBloc;
@override
void initState() {
super.initState();
_profileScopeBloc =
ProfileScopeBloc(profileRepository: context.di.repositories.profileScopeRepository);
_profileScopeBloc.add(const ProfileScopeFetchProfileEvent(id: '1'));
}
@override
void dispose() {
_profileScopeBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ProfileInheritedScope(profileScopeBloc: _profileScopeBloc, child: widget.child);
}
}

View File

@@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/features/profile_scope/domain/bloc/profile_scope_bloc.dart';
import 'package:friflex_starter/features/profile_scope/presentation/profile_scope.dart';
// Класс экрана, где мы инициализируем ProfileScopeBloc
class ProfileScopeScreen extends StatelessWidget {
const ProfileScopeScreen({super.key});
@override
Widget build(BuildContext context) {
return const ProfileScope(
child: _ProfileScopeView(),
);
}
}
class _ProfileScopeView extends StatelessWidget {
const _ProfileScopeView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Scope')),
body: Center(
child: BlocBuilder<ProfileScopeBloc, ProfileScopeState>(
bloc: ProfileScope.of(context).profileScopeBloc,
builder: (context, state) {
return switch (state) {
ProfileScopeSuccessState() => Text('Data: ${state.props.first}'),
ProfileScopeErrorState() => Text('Error: ${state.message}'),
_ => const CircularProgressIndicator(),
};
},
),
),
);
}
}

View File

@@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart';
import 'package:go_router/go_router.dart';
/// Класс для реализации корневой страницы приложения
@@ -18,11 +21,19 @@ class RootScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: context.di.env != AppEnv.prod
? FloatingActionButton(
child: const Icon(Icons.bug_report),
onPressed: () {
context.pushNamed(DebugRoutes.debugScreenName);
},
)
: null,
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.bug_report), label: 'Debug'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'),
],
currentIndex: navigationShell.currentIndex,
onTap: navigationShell.goBranch,

View File

@@ -2,29 +2,33 @@ import 'package:flutter/cupertino.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart';
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/screens/profile_screen.dart';
import 'package:friflex_starter/features/profile_scope/presentation/screens/profile_scope_screen.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:go_router/go_router.dart';
/// Класс, реализующий роутер приложения и все поля классов
/// {@template app_router}
/// AppRouter - класс для управления навигацией в приложении
/// [createRouter] - метод для создания экземпляра GoRouter
/// {@endtemplate}
class AppRouter {
/// Конструктор для инициализации роутера
/// {@macro app_router}
const AppRouter();
/// Ключ для доступа к корневому навигатору приложения
static final rootNavigatorKey = GlobalKey<NavigatorState>();
/// Начальный роут приложения
static String get initialLocation => '/debug';
static String get initialLocation => '/main';
/// Метод для создания экземпляра GoRouter
static GoRouter createRouter(IDebugService debugService) {
return GoRouter(
navigatorKey: rootNavigatorKey,
debugLogDiagnostics: true,
initialLocation: initialLocation,
observers: [
debugService.routeObserver,
],
routes: [
StatefulShellRoute.indexedStack(
parentNavigatorKey: rootNavigatorKey,
@@ -32,21 +36,14 @@ class AppRouter {
RootScreen(navigationShell: navigationShell),
branches: [
MainRoutes.buildShellBranch(),
DebugRoutes.buildShellBranch(),
ProfileRoutes.buildShellBranch(),
],
),
DebugRoutes.buildRoutes(),
GoRoute(
path: '/splash',
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: '/profile_scope',
builder: (context, state) => const ProfileScopeScreen(),
),
],
);
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
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/di/di_container.dart';
@@ -55,6 +56,9 @@ class AppRunner {
_timerRunner = TimerRunner(_debugService);
// Инициализация Talker для логирования Bloc
Bloc.observer = _debugService.blocObserver;
// Инициализация приложения
await _initApp();

View File

@@ -0,0 +1,21 @@
import 'package:flutter/widgets.dart';
/// {@template HBox}
/// HBox виджет для вертикального отступа (Надстройка над SizedBox)
/// [height] - высота отступа
/// [key] - ключ виджета
/// {@endtemplate}
class HBox extends SizedBox {
/// {@macro HBox}
const HBox(double height, {super.key}) : super(height: height);
}
/// {@template WBox}
/// WBox виджет для вертикального отступа (Надстройка над SizedBox)
/// [width] - ширина отступа
/// [key] - ключ виджета
/// {@endtemplate}
class WBox extends SizedBox {
/// {@macro WBox}
const WBox(double width, {super.key}) : super(width: width);
}