Refactor Flutter app architecture
Build and deploy Flutter Web / build (push) Successful in 3m7s

This commit is contained in:
Ruslan Bakiev
2026-06-12 14:25:24 +07:00
parent be41f74b33
commit 5e78a134b0
17 changed files with 821 additions and 751 deletions
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../features/mapflow/application/place_cubit.dart';
import '../features/mapflow/data/mapflow_api.dart';
import '../shared/location/current_location.dart';
import 'router/app_router.dart';
import 'theme/mapflow_theme.dart';
class MapflowApp extends StatefulWidget {
const MapflowApp({super.key});
@override
State<MapflowApp> createState() => _MapflowAppState();
}
class _MapflowAppState extends State<MapflowApp> {
late final PlaceCubit _placeCubit;
late final GoRouter _router;
@override
void initState() {
super.initState();
_placeCubit = PlaceCubit(api: MapflowApi(), location: CurrentLocation())
..load();
_router = createAppRouter();
}
@override
void dispose() {
_router.dispose();
_placeCubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final shortcuts =
Map<ShortcutActivator, Intent>.of(WidgetsApp.defaultShortcuts)
..[const SingleActivator(LogicalKeyboardKey.tab)] =
const NextFocusIntent()
..[const SingleActivator(LogicalKeyboardKey.tab, shift: true)] =
const PreviousFocusIntent();
return BlocProvider.value(
value: _placeCubit,
child: MaterialApp.router(
title: 'MapFlow',
debugShowCheckedModeBanner: false,
routerConfig: _router,
scrollBehavior: const MaterialScrollBehavior().copyWith(
scrollbars: true,
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.trackpad,
PointerDeviceKind.stylus,
},
),
theme: MapflowTheme.light(),
builder: (context, child) {
return Shortcuts(
shortcuts: shortcuts,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: child ?? const SizedBox.shrink(),
),
);
},
),
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../../features/mapflow/presentation/admin_voice_experiences_screen.dart';
import '../../features/mapflow/presentation/mapflow_shell.dart';
class AddExperienceArgs {
const AddExperienceArgs({
required this.coordinate,
required this.hasTelegramAuth,
});
final LatLng? coordinate;
final bool hasTelegramAuth;
}
GoRouter createAppRouter() {
return GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const MapflowShell(),
routes: [
GoRoute(
path: 'experience/new',
pageBuilder: (context, state) {
final args = state.extra as AddExperienceArgs;
return CustomTransitionPage<void>(
fullscreenDialog: true,
key: state.pageKey,
child: AddExperienceFlow(
coordinate: args.coordinate,
hasTelegramAuth: args.hasTelegramAuth,
),
transitionsBuilder: (context, animation, _, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: child,
);
},
);
},
),
GoRoute(
path: 'admin/reviews',
builder: (context, state) => const AdminVoiceExperiencesScreen(),
),
],
),
],
);
}
+124
View File
@@ -0,0 +1,124 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
@immutable
class MapflowThemeTokens extends ThemeExtension<MapflowThemeTokens> {
const MapflowThemeTokens({
required this.mapPanel,
required this.mapPanelBorder,
required this.mapShadow,
required this.danger,
required this.onDark,
required this.panelRadius,
});
final Color mapPanel;
final Color mapPanelBorder;
final Color mapShadow;
final Color danger;
final Color onDark;
final double panelRadius;
@override
MapflowThemeTokens copyWith({
Color? mapPanel,
Color? mapPanelBorder,
Color? mapShadow,
Color? danger,
Color? onDark,
double? panelRadius,
}) {
return MapflowThemeTokens(
mapPanel: mapPanel ?? this.mapPanel,
mapPanelBorder: mapPanelBorder ?? this.mapPanelBorder,
mapShadow: mapShadow ?? this.mapShadow,
danger: danger ?? this.danger,
onDark: onDark ?? this.onDark,
panelRadius: panelRadius ?? this.panelRadius,
);
}
@override
MapflowThemeTokens lerp(
covariant ThemeExtension<MapflowThemeTokens>? other,
double t,
) {
if (other is! MapflowThemeTokens) {
return this;
}
return MapflowThemeTokens(
mapPanel: Color.lerp(mapPanel, other.mapPanel, t)!,
mapPanelBorder: Color.lerp(mapPanelBorder, other.mapPanelBorder, t)!,
mapShadow: Color.lerp(mapShadow, other.mapShadow, t)!,
danger: Color.lerp(danger, other.danger, t)!,
onDark: Color.lerp(onDark, other.onDark, t)!,
panelRadius: lerpDouble(panelRadius, other.panelRadius, t),
);
}
}
class MapflowTheme {
const MapflowTheme._();
static const tokens = MapflowThemeTokens(
mapPanel: Color(0xFFFFFBF5),
mapPanelBorder: Color(0xFFE0D8CA),
mapShadow: Color(0x33000000),
danger: Color(0xFFE11D48),
onDark: Colors.white,
panelRadius: 8,
);
static ThemeData light() {
return FlexThemeData.light(
useMaterial3: true,
colors: const FlexSchemeColor(
primary: Color(0xFF0F766E),
primaryContainer: Color(0xFFBFE7DF),
secondary: Color(0xFFE11D48),
secondaryContainer: Color(0xFFFFD9E1),
tertiary: Color(0xFF7C3AED),
tertiaryContainer: Color(0xFFE9DDFF),
appBarColor: Color(0xFFF7F3EA),
error: Color(0xFFB3261E),
),
scaffoldBackground: const Color(0xFFF7F3EA),
surface: tokens.mapPanel,
subThemesData: const FlexSubThemesData(
defaultRadius: 8,
inputDecoratorRadius: 8,
cardRadius: 8,
chipRadius: 8,
filledButtonRadius: 8,
),
fontFamily: 'SF Pro Display',
extensions: const [tokens],
).copyWith(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFF7F3EA),
foregroundColor: Color(0xFF17211D),
surfaceTintColor: Colors.transparent,
),
cardTheme: CardThemeData(
elevation: 0,
color: tokens.mapPanel,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(tokens.panelRadius),
side: BorderSide(color: tokens.mapPanelBorder),
),
),
chipTheme: ChipThemeData(
shape: StadiumBorder(side: BorderSide(color: tokens.mapPanelBorder)),
),
);
}
}
double lerpDouble(double a, double b, double t) => a + (b - a) * t;
extension MapflowThemeContext on BuildContext {
MapflowThemeTokens get mapflowTokens {
return Theme.of(this).extension<MapflowThemeTokens>()!;
}
}