This commit is contained in:
2026-06-03 14:07:10 +08:00
parent 3bdece45c3
commit 9eb8d1cc37
118 changed files with 5689 additions and 2 deletions

53
lib/app/app.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/app/router/app_navigator.dart';
import 'package:flutter_template/app/theme/app_theme.dart';
import 'package:flutter_template/features/demo/demo_page.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class FlutterTemplateApp extends StatelessWidget {
const FlutterTemplateApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return MaterialApp(
title: AppConfig.appName,
navigatorKey: AppNavigator.navigatorKey,
navigatorObservers: [RouteTracker()],
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
builder: EasyLoading.init(
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: const TextScaler.linear(1)),
child: child ?? const SizedBox.shrink(),
);
},
),
home: RefreshConfiguration(
enableLoadingWhenNoData: false,
headerTriggerDistance: 80,
child: const DemoPage(),
),
);
},
);
}
}

29
lib/app/bootstrap.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/app.dart';
import 'package:flutter_template/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart';
import 'package:flutter_template/core/logging/app_logger.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AppBootstrapper {
AppBootstrapper._();
static Future<void> bootstrap({
AppEnvironment environment = AppEnvironment.dev,
}) async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppStorage.init();
final packageInfo = await PackageInfo.fromPlatform();
AppConfig.configure(environment: environment, packageInfo: packageInfo);
AppLogger.debug('App started in ${AppConfig.current.environment.name}');
runApp(const ProviderScope(child: FlutterTemplateApp()));
}
}

View File

@@ -0,0 +1,48 @@
import 'package:package_info_plus/package_info_plus.dart';
enum AppEnvironment { dev, staging, prod }
class EnvironmentValues {
const EnvironmentValues({
required this.environment,
required this.baseUrl,
required this.enableNetworkLog,
});
final AppEnvironment environment;
final String baseUrl;
final bool enableNetworkLog;
}
class AppConfig {
AppConfig._();
static late EnvironmentValues current;
static PackageInfo? packageInfo;
static const appName = 'Flutter Template';
static void configure({
required AppEnvironment environment,
PackageInfo? packageInfo,
}) {
AppConfig.packageInfo = packageInfo;
current = switch (environment) {
AppEnvironment.dev => const EnvironmentValues(
environment: AppEnvironment.dev,
baseUrl: 'https://example.com/api',
enableNetworkLog: true,
),
AppEnvironment.staging => const EnvironmentValues(
environment: AppEnvironment.staging,
baseUrl: 'https://staging.example.com/api',
enableNetworkLog: true,
),
AppEnvironment.prod => const EnvironmentValues(
environment: AppEnvironment.prod,
baseUrl: 'https://api.example.com',
enableNetworkLog: false,
),
};
}
}

View File

@@ -0,0 +1,189 @@
import 'dart:io';
import 'package:flutter/material.dart';
class AppNavigator {
AppNavigator._();
static final navigatorKey = GlobalKey<NavigatorState>();
static final Set<String> _pushingRoutes = <String>{};
static BuildContext? get context => navigatorKey.currentContext;
static Future<T?> push<T>(
Widget page, {
BuildContext? context,
Object? arguments,
String? name,
bool preventDuplicate = true,
}) async {
final routeName = name ?? page.runtimeType.toString();
if (preventDuplicate && RouteTracker.contains(routeName)) {
return null;
}
if (_pushingRoutes.contains(routeName)) {
return null;
}
_pushingRoutes.add(routeName);
final nav = Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
);
try {
return await nav.push<T>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: routeName, arguments: arguments),
),
);
} finally {
WidgetsBinding.instance.addPostFrameCallback((_) {
_pushingRoutes.remove(routeName);
});
}
}
static Future<T?> pushReplacement<T, TO>(
Widget page, {
BuildContext? context,
Object? arguments,
String? name,
}) {
return Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pushReplacement<T, TO>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: name, arguments: arguments),
),
);
}
static Future<T?> pushAndRemoveUntil<T>(
Widget page, {
BuildContext? context,
RoutePredicate? predicate,
Object? arguments,
String? name,
}) {
return Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pushAndRemoveUntil<T>(
SlidePageRoute(
builder: (_) => page,
settings: RouteSettings(name: name, arguments: arguments),
),
predicate ?? (_) => false,
);
}
static Future<T?> pushTransparent<T>(
Widget page, {
BuildContext? context,
Color barrierColor = Colors.black54,
Duration duration = const Duration(milliseconds: 200),
bool dismissible = true,
}) {
return Navigator.of(context ?? AppNavigator.context!).push<T>(
PageRouteBuilder<T>(
opaque: false,
barrierColor: barrierColor,
barrierDismissible: dismissible,
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(opacity: animation, child: child);
},
),
);
}
static void pop<T extends Object?>({BuildContext? context, T? result}) {
Navigator.of(
context ?? AppNavigator.context!,
rootNavigator: true,
).pop<T>(result);
}
static void popTimes({BuildContext? context, int count = 1}) {
var popped = 0;
Navigator.of(context ?? AppNavigator.context!).popUntil((route) {
if (popped < count) {
popped++;
return false;
}
return true;
});
}
}
class SlidePageRoute<T> extends MaterialPageRoute<T> {
SlidePageRoute({required super.builder, super.settings});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (Platform.isAndroid) {
final tween = Tween(
begin: const Offset(1, 0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOutCubic));
return SlideTransition(position: animation.drive(tween), child: child);
}
return Theme.of(context).pageTransitionsTheme.buildTransitions<T>(
this,
context,
animation,
secondaryAnimation,
child,
);
}
}
class RouteTracker extends NavigatorObserver {
static final List<String?> pageStack = [];
bool _isPage(Route<dynamic> route) => route is PageRoute;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route)) {
pageStack.add(route.settings.name);
}
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route) && pageStack.isNotEmpty) {
pageStack.removeLast();
}
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (_isPage(route)) {
pageStack.remove(route.settings.name);
}
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
if (oldRoute != null && _isPage(oldRoute) && pageStack.isNotEmpty) {
pageStack.removeLast();
}
if (newRoute != null && _isPage(newRoute)) {
pageStack.add(newRoute.settings.name);
}
}
static bool contains(String name) => pageStack.contains(name);
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
class AppTheme {
AppTheme._();
static const seedColor = Color(0xFF2563EB);
static const background = Color(0xFFF6F7FB);
static const surface = Colors.white;
static const textPrimary = Color(0xFF111827);
static const textSecondary = Color(0xFF6B7280);
static const border = Color(0xFFE5E7EB);
static const success = Color(0xFF16A34A);
static const warning = Color(0xFFF59E0B);
static const danger = Color(0xFFDC2626);
static ThemeData get light {
final scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
surface: surface,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: background,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: surface,
foregroundColor: textPrimary,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(88, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 44),
side: const BorderSide(color: border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
}
static ThemeData get dark {
final scheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 0,
),
);
}
}
class AppSpacing {
AppSpacing._();
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 24;
static const double xxl = 32;
}