init
This commit is contained in:
53
lib/app/app.dart
Normal file
53
lib/app/app.dart
Normal 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
29
lib/app/bootstrap.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
48
lib/app/config/app_config.dart
Normal file
48
lib/app/config/app_config.dart
Normal 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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
189
lib/app/router/app_navigator.dart
Normal file
189
lib/app/router/app_navigator.dart
Normal 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);
|
||||
}
|
||||
80
lib/app/theme/app_theme.dart
Normal file
80
lib/app/theme/app_theme.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user