Flutter项目搭建最佳实践
1. 项目结构规范
lib/
├── core/ # 核心层
│ ├── base/ # 基类封装
│ ├── constants/ # 常量定义
│ ├── theme/ # 主题配置
│ ├── localization/ # 国际化
│ └── utils/ # 工具类
├── data/ # 数据层
│ ├── models/ # 数据模型
│ ├── repositories/ # 数据仓库
│ ├── datasources/ # 数据源
│ └── network/ # 网络请求
├── domain/ # 业务层
│ ├── entities/ # 业务实体
│ ├── repositories/ # 抽象仓库
│ └── usecases/ # 用例
├── presentation/ # 表现层
│ ├── blocs/ # BLoC状态管理
│ ├── pages/ # 页面
│ ├── widgets/ # 公共组件
│ ├── dialogs/ # 对话框
│ └── animations/ # 动画
└── main.dart
2. Base基类封装
基础Widget封装
// core/base/base_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';abstract class BaseStatefulWidget extends StatefulWidget {const BaseStatefulWidget({Key? key}) : super(key: key);
}abstract class BaseState<T extends BaseStatefulWidget> extends State<T> {bool _isLoading = false;void initState() {super.initState();initBase();}void initBase() {// 基础初始化逻辑}void showLoading() {if (!_isLoading) {_isLoading = true;// 显示加载对话框}}void hideLoading() {if (_isLoading) {_isLoading = false;// 隐藏加载对话框}}void showError(String message) {// 错误提示}void showSuccess(String message) {// 成功提示}
}
BLoC基类封装
// core/base/base_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';abstract class BaseBloc<Event, State> extends Bloc<Event, State> {BaseBloc(State initialState) : super(initialState);void onEvent(Event event) {super.onEvent(event);// 统一事件处理}void onChange(Change<State> change) {super.onChange(change);// 统一状态变更处理}void onError(Object error, StackTrace stackTrace) {// 统一错误处理super.onError(error, stackTrace);}
}
3. 日志类封装
// core/utils/logger.dart
import 'package:logger/logger.dart';class AppLogger {static final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0,errorMethodCount: 8,lineLength: 120,colors: true,printEmojis: true,printTime: true,),);static void v(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.v(message, error, stackTrace);}static void d(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.d(message, error, stackTrace);}static void i(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.i(message, error, stackTrace);}static void w(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.w(message, error, stackTrace);}static void e(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.e(message, error, stackTrace);}static void wtf(dynamic message, [dynamic error, StackTrace? stackTrace]) {_logger.wtf(message, error, stackTrace);}
}
4. 主题封装
// core/theme/app_theme.dart
import 'package:flutter/material.dart';class AppTheme {static const Color primaryColor = Color(0xFF6200EE);static const Color secondaryColor = Color(0xFF03DAC6);static const Color errorColor = Color(0xFFB00020);static const Color backgroundColor = Color(0xFFF5F5F5);static ThemeData get lightTheme {return ThemeData(brightness: Brightness.light,primaryColor: primaryColor,colorScheme: const ColorScheme.light().copyWith(primary: primaryColor,secondary: secondaryColor,error: errorColor,),scaffoldBackgroundColor: backgroundColor,appBarTheme: const AppBarTheme(backgroundColor: primaryColor,foregroundColor: Colors.white,elevation: 0,),textTheme: const TextTheme(headlineLarge: TextStyle(fontSize: 32,fontWeight: FontWeight.bold,color: Colors.black,),bodyLarge: TextStyle(fontSize: 16,color: Colors.black87,),),);}static ThemeData get darkTheme {return ThemeData(brightness: Brightness.dark,primaryColor: primaryColor,colorScheme: const ColorScheme.dark().copyWith(primary: primaryColor,secondary: secondaryColor,error: errorColor,),);}
}
5. 国际化语言处理
// core/localization/app_localizations.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';class AppLocalizations {final Locale locale;AppLocalizations(this.locale);static AppLocalizations? of(BuildContext context) {return Localizations.of<AppLocalizations>(context, AppLocalizations);}static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();static final Map<String, Map<String, String>> _localizedValues = {'en': {'title': 'App Title','welcome': 'Welcome','error': 'An error occurred',},'zh': {'title': '应用标题','welcome': '欢迎','error': '发生了一个错误',},};String? get title {return _localizedValues[locale.languageCode]?['title'];}String? get welcome {return _localizedValues[locale.languageCode]?['welcome'];}String? get error {return _localizedValues[locale.languageCode]?['error'];}
}class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {const _AppLocalizationsDelegate();bool isSupported(Locale locale) {return ['en', 'zh'].contains(locale.languageCode);}Future<AppLocalizations> load(Locale locale) async {return AppLocalizations(locale);}bool shouldReload(_AppLocalizationsDelegate old) => false;
}
6. 字符串工具类封装
// core/utils/string_utils.dart
class StringUtils {static bool isEmpty(String? str) {return str == null || str.isEmpty;}static bool isNotEmpty(String? str) {return !isEmpty(str);}static String safeString(String? str, {String defaultValue = ''}) {return str ?? defaultValue;}static String formatCurrency(double amount, {String symbol = '¥'}) {return '$symbol${amount.toStringAsFixed(2)}';}static String truncateWithEllipsis(String str, int maxLength) {if (str.length <= maxLength) return str;return '${str.substring(0, maxLength)}...';}static bool isEmail(String email) {final RegExp emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');return emailRegex.hasMatch(email);}static bool isPhone(String phone) {final RegExp phoneRegex = RegExp(r'^1[3-9]\d{9}$');return phoneRegex.hasMatch(phone);}
}
7. 动画工具类封装
// core/utils/animation_utils.dart
import 'package:flutter/material.dart';class AnimationUtils {static const Duration fastDuration = Duration(milliseconds: 200);static const Duration mediumDuration = Duration(milliseconds: 300);static const Duration slowDuration = Duration(milliseconds: 500);static Curve get easeInOut => Curves.easeInOut;static Curve get bounceOut => Curves.bounceOut;static Curve get elasticOut => Curves.elasticOut;static Widget fadeIn(Widget child, Animation<double> animation) {return FadeTransition(opacity: animation,child: child,);}static Widget slideInFromBottom(Widget child, Animation<double> animation) {return SlideTransition(position: Tween<Offset>(begin: const Offset(0, 1),end: Offset.zero,).animate(animation),child: child,);}static Widget scaleIn(Widget child, Animation<double> animation) {return ScaleTransition(scale: animation,child: child,);}static Future<void> delayed(VoidCallback callback,{Duration duration = const Duration(milliseconds: 300)}) {return Future.delayed(duration, callback);}
}
8. 路由封装
// core/utils/route_utils.dart
import 'package:flutter/material.dart';class RouteUtils {static Future<T?> push<T>(BuildContext context, Widget page) {return Navigator.push<T>(context,MaterialPageRoute(builder: (context) => page),);}static Future<T?> pushReplacement<T>(BuildContext context, Widget page) {return Navigator.pushReplacement<T, T>(context,MaterialPageRoute(builder: (context) => page),);}static void pop<T>(BuildContext context, [T? result]) {Navigator.pop(context, result);}static Future<T?> pushNamed<T>(BuildContext context, String routeName,{Object? arguments}) {return Navigator.pushNamed<T>(context,routeName,arguments: arguments,);}static void popUntil(BuildContext context, String routeName) {Navigator.popUntil(context, ModalRoute.withName(routeName));}static bool canPop(BuildContext context) {return Navigator.canPop(context);}
}class AppRoutes {static const String home = '/home';static const String login = '/login';static const String profile = '/profile';static const String settings = '/settings';static Route<dynamic>? generateRoute(RouteSettings settings) {switch (settings.name) {case home:// return MaterialPageRoute(builder: (_) => HomePage());case login:// return MaterialPageRoute(builder: (_) => LoginPage());default:return null;}}
}
9. BLoC状态管理封装
BLoC基类增强
// presentation/blocs/base_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';abstract class BaseBloc<Event, State> extends Bloc<Event, State> {BaseBloc(State initialState) : super(initialState);void onEvent(Event event) {AppLogger.d('BLoC Event: $event');super.onEvent(event);}void onChange(Change<State> change) {AppLogger.d('BLoC State Change: ${change.currentState} -> ${change.nextState}');super.onChange(change);}
}
示例:计数器BLoC
// presentation/blocs/counter/counter_bloc.dart
part of 'counter_event.dart';
part of 'counter_state.dart';class CounterBloc extends BaseBloc<CounterEvent, CounterState> {CounterBloc() : super(const CounterState()) {on<CounterIncremented>(_onIncremented);on<CounterDecremented>(_onDecremented);on<CounterReset>(_onReset);}void _onIncremented(CounterIncremented event,Emitter<CounterState> emit,) {emit(state.copyWith(count: state.count + 1));}void _onDecremented(CounterDecremented event,Emitter<CounterState> emit,) {emit(state.copyWith(count: state.count - 1));}void _onReset(CounterReset event,Emitter<CounterState> emit,) {emit(state.copyWith(count: 0));}
}// presentation/blocs/counter/counter_event.dart
part of 'counter_bloc.dart';abstract class CounterEvent extends Equatable {const CounterEvent();List<Object> get props => [];
}class CounterIncremented extends CounterEvent {const CounterIncremented();
}class CounterDecremented extends CounterEvent {const CounterDecremented();
}class CounterReset extends CounterEvent {const CounterReset();
}// presentation/blocs/counter/counter_state.dart
part of 'counter_bloc.dart';class CounterState extends Equatable {final int count;const CounterState({this.count = 0});CounterState copyWith({int? count,}) {return CounterState(count: count ?? this.count,);}List<Object> get props => [count];
}
10. Retrofit风格网络请求封装
Dio配置
// data/network/dio_client.dart
import 'package:dio/dio.dart';
import 'package:logger/logger.dart';class DioClient {static final DioClient _instance = DioClient._internal();factory DioClient() => _instance;late Dio _dio;DioClient._internal() {_dio = Dio(BaseOptions(baseUrl: 'https://api.example.com',connectTimeout: const Duration(seconds: 30),receiveTimeout: const Duration(seconds: 30),sendTimeout: const Duration(seconds: 30),headers: {'Content-Type': 'application/json',},),);// 添加拦截器_dio.interceptors.add(LogInterceptor(request: true,requestBody: true,responseBody: true,logPrint: (object) => Logger().d(object),));_dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {// 添加认证token// options.headers['Authorization'] = 'Bearer $token';return handler.next(options);},onError: (DioException error, handler) {// 统一错误处理return handler.next(error);},));}Dio get dio => _dio;
}
API服务封装
// data/network/api_service.dart
import 'package:dio/dio.dart';
import 'dio_client.dart';class ApiService {final Dio _dio = DioClient().dio;Future<Response<T>> get<T>(String path, {Map<String, dynamic>? queryParameters,Options? options,CancelToken? cancelToken,ProgressCallback? onReceiveProgress,}) async {try {final response = await _dio.get<T>(path,queryParameters: queryParameters,options: options,cancelToken: cancelToken,onReceiveProgress: onReceiveProgress,);return response;} on DioException catch (e) {throw _handleError(e);}}Future<Response<T>> post<T>(String path, {dynamic data,Map<String, dynamic>? queryParameters,Options? options,CancelToken? cancelToken,ProgressCallback? onSendProgress,ProgressCallback? onReceiveProgress,}) async {try {final response = await _dio.post<T>(path,data: data,queryParameters: queryParameters,options: options,cancelToken: cancelToken,onSendProgress: onSendProgress,onReceiveProgress: onReceiveProgress,);return response;} on DioException catch (e) {throw _handleError(e);}}dynamic _handleError(DioException error) {switch (error.type) {case DioExceptionType.connectionTimeout:throw '连接超时';case DioExceptionType.sendTimeout:throw '发送超时';case DioExceptionType.receiveTimeout:throw '接收超时';case DioExceptionType.badCertificate:throw '证书错误';case DioExceptionType.badResponse:throw '服务器错误: ${error.response?.statusCode}';case DioExceptionType.cancel:throw '请求取消';case DioExceptionType.connectionError:throw '连接错误';case DioExceptionType.unknown:throw '网络错误';}}
}
Retrofit风格API接口
// data/network/apis/user_api.dart
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';part 'user_api.g.dart';(baseUrl: "https://api.example.com/")
abstract class UserApi {factory UserApi(Dio dio, {String baseUrl}) = _UserApi;("/users/{id}")Future<HttpResponse<User>> getUser(("id") String id);("/users")Future<HttpResponse<User>> createUser(() User user);("/users/{id}")Future<HttpResponse<User>> updateUser(("id") String id, () User user);("/users/{id}")Future<HttpResponse<void>> deleteUser(("id") String id);
}
11. Dialog封装
// presentation/dialogs/app_dialog.dart
import 'package:flutter/material.dart';class AppDialog {static Future<void> showLoading(BuildContext context, {String? message}) {return showDialog(context: context,barrierDismissible: false,builder: (context) => AlertDialog(content: Row(children: [const CircularProgressIndicator(),const SizedBox(width: 16),Text(message ?? '加载中...'),],),),);}static void hideLoading(BuildContext context) {Navigator.of(context, rootNavigator: true).pop();}static Future<bool?> showConfirmDialog(BuildContext context, {required String title,required String content,String confirmText = '确认',String cancelText = '取消',}) {return showDialog<bool>(context: context,builder: (context) => AlertDialog(title: Text(title),content: Text(content),actions: [TextButton(onPressed: () => Navigator.of(context).pop(false),child: Text(cancelText),),TextButton(onPressed: () => Navigator.of(context).pop(true),child: Text(confirmText),),],),);}static Future<void> showErrorDialog(BuildContext context, {required String message,String title = '错误',}) {return showDialog(context: context,builder: (context) => AlertDialog(title: Text(title),content: Text(message),actions: [TextButton(onPressed: () => Navigator.of(context).pop(),child: const Text('确定'),),],),);}static Future<String?> showInputDialog(BuildContext context, {required String title,String hintText = '请输入',String confirmText = '确定',String cancelText = '取消',}) {final TextEditingController controller = TextEditingController();return showDialog<String>(context: context,builder: (context) => AlertDialog(title: Text(title),content: TextField(controller: controller,decoration: InputDecoration(hintText: hintText),),actions: [TextButton(onPressed: () => Navigator.of(context).pop(),child: Text(cancelText),),TextButton(onPressed: () => Navigator.of(context).pop(controller.text),child: Text(confirmText),),],),);}
}
12. 底部弹窗封装
// presentation/dialogs/bottom_sheets.dart
import 'package:flutter/material.dart';class AppBottomSheets {static Future<T?> showCustomBottomSheet<T>({required BuildContext context,required Widget child,bool isScrollControlled = false,Color? backgroundColor,double? elevation,ShapeBorder? shape,Clip? clipBehavior,BoxConstraints? constraints,Color? barrierColor,bool enableDrag = true,bool isDismissible = true,bool useRootNavigator = false,RouteSettings? routeSettings,}) {return showModalBottomSheet<T>(context: context,isScrollControlled: isScrollControlled,backgroundColor: backgroundColor,elevation: elevation,shape: shape ?? const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16)),),clipBehavior: clipBehavior,constraints: constraints,barrierColor: barrierColor,enableDrag: enableDrag,isDismissible: isDismissible,useRootNavigator: useRootNavigator,routeSettings: routeSettings,builder: (context) => child,);}static Future<int?> showActionSheet({required BuildContext context,required List<String> options,String? title,String? cancelText = '取消',}) {return showCustomBottomSheet<int>(context: context,isScrollControlled: true,child: Container(padding: const EdgeInsets.all(16),child: Column(mainAxisSize: MainAxisSize.min,children: [if (title != null) ...[Text(title,style: Theme.of(context).textTheme.titleMedium,),const SizedBox(height: 16),],...List.generate(options.length, (index) {return Column(children: [ListTile(title: Text(options[index]),onTap: () => Navigator.of(context).pop(index),),if (index < options.length - 1)const Divider(height: 1),],);}),const SizedBox(height: 8),Container(width: double.infinity,child: ElevatedButton(onPressed: () => Navigator.of(context).pop(),style: ElevatedButton.styleFrom(backgroundColor: Theme.of(context).cardColor,foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,),child: Text(cancelText!),),),],),),);}
}
13. 常用工具类封装
// core/utils/common_utils.dart
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';class CommonUtils {static Future<bool> checkNetworkConnection() async {final connectivityResult = await Connectivity().checkConnectivity();return connectivityResult != ConnectivityResult.none;}static Future<String> getAppVersion() async {final PackageInfo packageInfo = await PackageInfo.fromPlatform();return '${packageInfo.version} (${packageInfo.buildNumber})';}static String formatFileSize(int bytes) {if (bytes <= 0) return "0 B";const suffixes = ["B", "KB", "MB", "GB", "TB"];final i = (log(bytes) / log(1024)).floor();return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${suffixes[i]}';}static String getTimeAgo(DateTime date) {final now = DateTime.now();final difference = now.difference(date);if (difference.inDays > 365) {return '${(difference.inDays / 365).floor()}年前';} else if (difference.inDays > 30) {return '${(difference.inDays / 30).floor()}月前';} else if (difference.inDays > 0) {return '${difference.inDays}天前';} else if (difference.inHours > 0) {return '${difference.inHours}小时前';} else if (difference.inMinutes > 0) {return '${difference.inMinutes}分钟前';} else {return '刚刚';}}
}// core/utils/device_utils.dart
import 'package:device_info_plus/device_info_plus.dart';class DeviceUtils {static Future<String> getDeviceInfo() async {final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();if (Platform.isAndroid) {final androidInfo = await deviceInfo.androidInfo;return '${androidInfo.manufacturer} ${androidInfo.model}';} else if (Platform.isIOS) {final iosInfo = await deviceInfo.iosInfo;return '${iosInfo.name} ${iosInfo.model}';}return 'Unknown Device';}
}
14. 打包配置
pubspec.yaml配置
name: flutter_best_practice
description: A Flutter Best Practice Projectpublish_to: 'none'version: 1.0.0+1environment:sdk: '>=3.0.0 <4.0.0'dependencies:flutter:sdk: fluttercupertino_icons: ^1.0.2bloc: ^8.1.2flutter_bloc: ^8.1.3equatable: ^2.0.5dio: ^5.3.2retrofit: ^4.0.1logger: ^1.1.0connectivity_plus: ^5.0.1package_info_plus: ^4.2.0device_info_plus: ^9.0.3intl: ^0.18.1dev_dependencies:flutter_test:sdk: flutterflutter_lints: ^2.0.0build_runner: ^2.4.4retrofit_generator: ^4.2.0flutter:uses-material-design: truegenerate: trueassets:- assets/images/- assets/icons/- assets/translations/
环境配置
// core/constants/app_constants.dart
class AppConstants {static const String appName = 'Flutter Best Practice';static const String apiBaseUrl = String.fromEnvironment('API_BASE_URL',defaultValue: 'https://api.example.com',);static const bool isDebug = bool.fromEnvironment('DEBUG', defaultValue: false);
}// flutter run --dart-define=API_BASE_URL=https://dev.api.example.com --dart-define=DEBUG=true
// flutter build apk --dart-define=API_BASE_URL=https://prod.api.example.com --dart-define=DEBUG=false
15. 其他重要补充
依赖注入配置
// core/di/injector.dart
import 'package:get_it/get_it.dart';final GetIt injector = GetIt.instance;class DependencyInjection {static Future<void> init() async {// 网络相关injector.registerLazySingleton(() => DioClient());injector.registerLazySingleton(() => ApiService());// 数据仓库// injector.registerLazySingleton<UserRepository>(() => UserRepositoryImpl());// BLoC// injector.registerFactory(() => CounterBloc());}
}
错误处理全局配置
// core/error/error_handler.dart
import 'package:flutter/foundation.dart';class ErrorHandler {static void setup() {FlutterError.onError = (FlutterErrorDetails details) {if (kDebugMode) {FlutterError.dumpErrorToConsole(details);} else {// 生产环境错误上报_reportError(details.exception, details.stack);}};PlatformDispatcher.instance.onError = (error, stack) {_reportError(error, stack);return true;};}static void _reportError(Object error, StackTrace? stack) {// 错误上报到服务器AppLogger.e('Global Error: $error', error, stack);}
}
性能监控
// core/utils/performance_utils.dart
import 'package:flutter/foundation.dart';class PerformanceUtils {static Future<T> measurePerformance<T>(String taskName,Future<T> Function() task,) async {final Stopwatch stopwatch = Stopwatch()..start();final T result = await task();stopwatch.stop();AppLogger.i('$taskName took ${stopwatch.elapsedMilliseconds}ms');return result;}static T measureSyncPerformance<T>(String taskName,T Function() task,) {final Stopwatch stopwatch = Stopwatch()..start();final T result = task();stopwatch.stop();AppLogger.i('$taskName took ${stopwatch.elapsedMilliseconds}ms');return result;}
}
这个最佳实践涵盖了Flutter项目开发的核心方面,提供了完整的项目结构和代码示例。可以根据自身项目需求和自己的理解进一步封装或者重构
