企业级 Flutter 项目框架搭建实战:从 0 到 1 构建可维护的大型应用
本文以一个完整的电商 Flutter 项目为例,详细介绍如何搭建企业级 Flutter 应用框架,涵盖架构设计、状态管理、网络封装、本地存储、路由管理等核心模块。
目录
一、项目概述
1.1 项目背景
在企业级 Flutter 应用开发中,我们面临以下挑战:
- 代码规模庞大:数万行代码,多人协作开发
- 业务逻辑复杂:用户、商品、订单、支付等多个模块
- 需求频繁变更:需要快速响应业务变化
- 长期维护:代码可读性、可测试性要求高
1.2 技术选型
| 技术栈 | 选型 | 说明 |
|---|---|---|
| 状态管理 | BLoC (flutter_bloc) | 可预测、易测试、社区活跃 |
| 依赖注入 | get_it + injectable | 编译期生成,类型安全 |
| 网络请求 | Dio | 功能强大,拦截器完善 |
| 本地数据库 | Drift (SQLite) | 类型安全,支持流式查询 |
| 路由管理 | 自定义封装 | 灵活可控,支持深链接 |
| 代码生成 | build_runner | 减少样板代码 |
二、整体架构设计
2.1 Clean Architecture 分层
lib/
├── main.dart # 入口文件
├── injection.dart # 依赖注入初始化
├── config/ # 配置层
│ ├── routes.dart # 路由配置
│ ├── theme.dart # 主题配置
│ └── constants.dart # 常量定义
├── core/ # 核心层
│ ├── di/ # 依赖注入
│ ├── network/ # 网络封装
│ ├── error/ # 错误处理
│ └── utils/ # 工具函数
├── data/ # 数据层
│ ├── datasources/ # 数据源
│ │ ├── local/ # 本地数据
│ │ └── remote/ # 远程数据
│ ├── models/ # 数据模型
│ └── repositories/ # 仓库实现
├── domain/ # 领域层
│ ├── entities/ # 实体
│ ├── repositories/ # 仓库接口
│ └── usecases/ # 用例
└── presentation/ # 表现层
├── blocs/ # BLoC 状态管理
├── pages/ # 页面
└── widgets/ # 组件
2.2 数据流向
UI (Widget)
↓ (事件)
BLoC (业务逻辑)
↓ (调用)
UseCase (用例)
↓ (调用)
Repository (仓库接口)
↓ (实现)
DataSource (数据源)
↓ (操作)
Local DB / Remote API
2.3 依赖关系
// 核心原则:依赖指向内侧
// UI → BLoC → UseCase → Repository → DataSource
// 外层依赖内层接口,不依赖实现
// 通过依赖注入解耦
三、核心模块详解
3.1 依赖注入(DI)
使用 get_it + injectable 实现编译期依赖注入:
// core/di/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final getIt = GetIt.instance;
@InjectableInit(
initializerName: r'$initGetIt',
preferRelativeImports: true,
asExtension: false,
)
void configureDependencies() => $initGetIt(getIt);
// core/di/modules/network_module.dart
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
@module
abstract class NetworkModule {
@singleton
Dio get dio => Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
));
}
// 使用示例
@injectable
class ProductRepositoryImpl implements ProductRepository {
final Dio _dio;
final DriftDatabase _database;
ProductRepositoryImpl(this._dio, this._database);
// ... 实现方法
}
优势:
- 编译期生成,类型安全
- 自动单例管理
- 易于测试(可 mock)
3.2 错误处理
统一错误处理机制:
// core/error/failures.dart
abstract class Failure {
final String message;
final int? code;
const Failure(this.message, {this.code});
}
class ServerFailure extends Failure {
const ServerFailure(String message, {int? code}) : super(message, code: code);
}
class CacheFailure extends Failure {
const CacheFailure(String message) : super(message);
}
class NetworkFailure extends Failure {
const NetworkFailure(String message) : super(message);
}
// core/error/exceptions.dart
class ServerException implements Exception {
final String message;
final int? code;
ServerException(this.message, {this.code});
}
// core/utils/result.dart
import 'package:dartz/dartz.dart';
type Result<T> = Either<Failure, T>;
四、状态管理方案
4.1 BLoC 架构
// presentation/blocs/base_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class BaseBloc<Event, State> extends Bloc<Event, State> {
BaseBloc(super.initialState);
@override
void onError(Object error, StackTrace stackTrace) {
// 统一错误上报
super.onError(error, stackTrace);
}
}
4.2 完整 BLoC 示例
// presentation/blocs/product_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
// Events
abstract class ProductEvent extends Equatable {
const ProductEvent();
@override
List<Object?> get props => [];
}
class LoadProducts extends ProductEvent {
final int page;
final int limit;
const LoadProducts({this.page = 1, this.limit = 20});
@override
List<Object?> get props => [page, limit];
}
class LoadProductDetail extends ProductEvent {
final String productId;
const LoadProductDetail(this.productId);
@override
List<Object?> get props => [productId];
}
// States
abstract class ProductState extends Equatable {
const ProductState();
@override
List<Object?> get props => [];
}
class ProductInitial extends ProductState {}
class ProductLoading extends ProductState {}
class ProductsLoaded extends ProductState {
final List<Product> products;
final bool hasReachedMax;
const ProductsLoaded(this.products, {this.hasReachedMax = false});
@override
List<Object?> get props => [products, hasReachedMax];
}
class ProductDetailLoaded extends ProductState {
final Product product;
const ProductDetailLoaded(this.product);
@override
List<Object?> get props => [product];
}
class ProductError extends ProductState {
final String message;
const ProductError(this.message);
@override
List<Object?> get props => [message];
}
// BLoC
class ProductBloc extends BaseBloc<ProductEvent, ProductState> {
final GetProductsUseCase _getProducts;
final GetProductDetailUseCase _getProductDetail;
ProductBloc(
this._getProducts,
this._getProductDetail,
) : super(ProductInitial()) {
on<LoadProducts>(_onLoadProducts);
on<LoadProductDetail>(_onLoadProductDetail);
}
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductState> emit,
) async {
emit(ProductLoading());
final result = await _getProducts(
PageParams(page: event.page, limit: event.limit),
);
result.fold(
(failure) => emit(ProductError(failure.message)),
(products) => emit(ProductsLoaded(
products,
hasReachedMax: products.length < event.limit,
)),
);
}
Future<void> _onLoadProductDetail(
LoadProductDetail event,
Emitter<ProductState> emit,
) async {
emit(ProductLoading());
final result = await _getProductDetail(event.productId);
result.fold(
(failure) => emit(ProductError(failure.message)),
(product) => emit(ProductDetailLoaded(product)),
);
}
}
4.3 UI 层使用
// presentation/pages/product_list_page.dart
class ProductListPage extends StatelessWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<ProductBloc>()..add(const LoadProducts()),
child: Scaffold(
appBar: AppBar(title: const Text('商品列表')),
body: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is ProductsLoaded) {
return ProductListView(products: state.products);
}
if (state is ProductError) {
return Center(child: Text('错误: ${state.message}'));
}
return const SizedBox.shrink();
},
),
),
);
}
}
五、网络层封装
5.1 Dio 配置
// core/network/network_manager.dart
import 'package:dio/dio.dart';
class NetworkManager {
final Dio _dio;
NetworkManager(this._dio) {
_setupInterceptors();
}
void _setupInterceptors() {
_dio.interceptors.addAll([
// 日志拦截器
LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
),
// 认证拦截器
AuthInterceptor(),
// 错误处理拦截器
ErrorInterceptor(),
// 缓存拦截器
CacheInterceptor(),
]);
}
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
);
}
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
}) async {
return _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
}
}
5.2 拦截器实现
// core/network/dio_interceptors.dart
import 'package:dio/dio.dart';
// 认证拦截器
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = getIt<AuthLocalDataSource>().getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401) {
// Token 过期,刷新或跳转登录
getIt<AuthBloc>().add(const TokenExpired());
}
handler.next(err);
}
}
// 错误处理拦截器
class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
throw NetworkException('连接超时,请检查网络');
case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
final message = err.response?.data['message'] ?? '服务器错误';
throw ServerException(message, code: statusCode);
default:
throw NetworkException('网络错误,请重试');
}
}
}
// 缓存拦截器
class CacheInterceptor extends Interceptor {
final CacheManager _cacheManager;
CacheInterceptor(this._cacheManager);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (options.extra['cache'] == true) {
final cached = _cacheManager.get(options.uri.toString());
if (cached != null) {
return handler.resolve(cached);
}
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.extra['cache'] == true) {
_cacheManager.set(
response.requestOptions.uri.toString(),
response,
ttl: response.requestOptions.extra['cacheTTL'] ?? const Duration(minutes: 5),
);
}
handler.next(response);
}
}
六、本地数据存储
6.1 Drift 数据库配置
// data/datasources/local/drift_database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
part 'drift_database.g.dart';
// 表定义
class Products extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
RealColumn get price => real()();
TextColumn get imageUrl => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
class CartItems extends Table {
TextColumn get id => text()();
TextColumn get productId => text().references(Products, #id)();
IntegerColumn get quantity => integer()();
DateTimeColumn get addedAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}
// 数据库类
@DriftDatabase(tables: [Products, CartItems])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'ecommerce_db');
}
// 商品相关查询
Future<List<Product>> getAllProducts() => select(products).get();
Stream<List<Product>> watchAllProducts() => select(products).watch();
Future<Product?> getProductById(String id) =>
(select(products)..where((p) => p.id.equals(id))).getSingleOrNull();
Future<int> insertProduct(ProductsCompanion product) =>
into(products).insert(product, mode: InsertMode.insertOrReplace);
// 购物车相关查询
Stream<List<CartItemWithProduct>> watchCartItems() {
final query = select(cartItems).join([
innerJoin(products, products.id.equalsExp(cartItems.productId)),
]);
return query.watch().map((rows) => rows.map((row) {
return CartItemWithProduct(
cartItem: row.readTable(cartItems),
product: row.readTable(products),
);
}).toList());
}
}
// 数据类
class CartItemWithProduct {
final CartItem cartItem;
final Product product;
CartItemWithProduct({required this.cartItem, required this.product});
}
6.2 SharedPreferences 封装
// core/storage/shared_prefs.dart
import 'package:shared_preferences/shared_preferences.dart';
class SharedPrefs {
static SharedPreferences? _instance;
static Future<void> init() async {
_instance = await SharedPreferences.getInstance();
}
// Token
static Future<void> setToken(String token) async {
await _instance?.setString('token', token);
}
static String? getToken() {
return _instance?.getString('token');
}
static Future<void> removeToken() async {
await _instance?.remove('token');
}
// 用户信息
static Future<void> setUser(String userJson) async {
await _instance?.setString('user', userJson);
}
static String? getUser() {
return _instance?.getString('user');
}
// 主题设置
static Future<void> setDarkMode(bool isDark) async {
await _instance?.setBool('isDarkMode', isDark);
}
static bool getDarkMode() {
return _instance?.getBool('isDarkMode') ?? false;
}
// 语言设置
static Future<void> setLocale(String locale) async {
await _instance?.setString('locale', locale);
}
static String getLocale() {
return _instance?.getString('locale') ?? 'zh_CN';
}
}
七、路由与导航
7.1 路由配置
// config/routes.dart
import 'package:flutter/material.dart';
class AppRoutes {
// 路由名称
static const String splash = '/';
static const String login = '/login';
static const String register = '/register';
static const String home = '/home';
static const String productList = '/products';
static const String productDetail = '/product/:id';
static const String cart = '/cart';
static const String checkout = '/checkout';
static const String orderList = '/orders';
static const String orderDetail = '/order/:id';
static const String profile = '/profile';
static const String settings = '/settings';
// 路由配置
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
final uri = Uri.parse(settings.name ?? '');
final path = uri.path;
final queryParams = uri.queryParameters;
switch (path) {
case splash:
return MaterialPageRoute(builder: (_) => const SplashPage());
case login:
return MaterialPageRoute(builder: (_) => const LoginPage());
case home:
return MaterialPageRoute(builder: (_) => const HomePage());
case productList:
return MaterialPageRoute(
builder: (_) => ProductListPage(
categoryId: queryParams['categoryId'],
),
);
case productDetail:
final productId = _extractParam(path, ':id');
return MaterialPageRoute(
builder: (_) => ProductDetailPage(productId: productId!),
);
case cart:
return MaterialPageRoute(builder: (_) => const CartPage());
case checkout:
return MaterialPageRoute(builder: (_) => const CheckoutPage());
default:
return MaterialPageRoute(builder: (_) => const NotFoundPage());
}
}
static String? _extractParam(String path, String param) {
final segments = path.split('/');
final index = segments.indexWhere((s) => s.startsWith(':'));
if (index != -1 && index < segments.length) {
return segments[index].replaceFirst(':', '');
}
return null;
}
}
// 导航扩展
extension NavigationExtension on BuildContext {
void push(String route, {Object? arguments}) {
Navigator.pushNamed(this, route, arguments: arguments);
}
void pushReplacement(String route, {Object? arguments}) {
Navigator.pushReplacementNamed(this, route, arguments: arguments);
}
void pop<T>([T? result]) {
Navigator.pop(this, result);
}
void popUntil(String route) {
Navigator.popUntil(this, ModalRoute.withName(route));
}
}
7.2 深链接支持
// 在 AndroidManifest.xml 和 Info.plist 中配置
// 然后处理深链接
class DeepLinkHandler {
static void handle(Uri uri) {
final path = uri.path;
final params = uri.queryParameters;
if (path.startsWith('/product/')) {
final productId = path.split('/').last;
navigatorKey.currentState?.pushNamed(
AppRoutes.productDetail,
arguments: {'id': productId},
);
} else if (path == '/cart') {
navigatorKey.currentState?.pushNamed(AppRoutes.cart);
}
}
}
八、UI 组件化
8.1 基础组件库
// presentation/widgets/app_button.dart
import 'package:flutter/material.dart';
enum AppButtonType { primary, secondary, outline, danger }
class AppButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final AppButtonType type;
final bool isLoading;
final bool isFullWidth;
final IconData? icon;
const AppButton({
super.key,
required this.text,
this.onPressed,
this.type = AppButtonType.primary,
this.isLoading = false,
this.isFullWidth = false,
this.icon,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget buttonChild = isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 18),
const SizedBox(width: 8),
],
Text(text),
],
);
ButtonStyle style;
switch (type) {
case AppButtonType.primary:
style = ElevatedButton.styleFrom(
backgroundColor: theme.primaryColor,
foregroundColor: Colors.white,
);
break;
case AppButtonType.secondary:
style = ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.secondary,
foregroundColor: Colors.white,
);
break;
case AppButtonType.outline:
style = OutlinedButton.styleFrom(
foregroundColor: theme.primaryColor,
);
break;
case AppButtonType.danger:
style = ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
);
break;
}
Widget button = type == AppButtonType.outline
? OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: style,
child: buttonChild,
)
: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: style,
child: buttonChild,
);
if (isFullWidth) {
button = SizedBox(width: double.infinity, child: button);
}
return button;
}
}
8.2 主题配置
// config/theme.dart
import 'package:flutter/material.dart';
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primaryColor: const Color(0xFF2196F3),
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
brightness: Brightness.light,
),
scaffoldBackgroundColor: const Color(0xFFF5F5F5),
appBarTheme: const AppBarTheme(
elevation: 0,
centerTitle: true,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF2196F3)),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
primaryColor: const Color(0xFF2196F3),
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF121212),
);
}
}
九、测试策略
9.1 单元测试
// test/domain/usecases/get_products_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'get_products_test.mocks.dart';
@GenerateMocks([ProductRepository])
void main() {
late GetProductsUseCase useCase;
late MockProductRepository mockRepository;
setUp(() {
mockRepository = MockProductRepository();
useCase = GetProductsUseCase(mockRepository);
});
final tProducts = [
Product(id: '1', name: '商品1', price: 100),
Product(id: '2', name: '商品2', price: 200),
];
test('should get products from repository', () async {
// arrange
when(mockRepository.getProducts(any))
.thenAnswer((_) async => Right(tProducts));
// act
final result = await useCase(const PageParams(page: 1, limit: 20));
// assert
expect(result, Right(tProducts));
verify(mockRepository.getProducts(any));
verifyNoMoreInteractions(mockRepository);
});
}
9.2 Widget 测试
// test/presentation/pages/product_list_page_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
class MockProductBloc extends MockBloc<ProductEvent, ProductState>
implements ProductBloc {}
void main() {
late MockProductBloc mockBloc;
setUp(() {
mockBloc = MockProductBloc();
});
testWidgets('should display products when loaded', (tester) async {
// arrange
final products = [
Product(id: '1', name: '商品1', price: 100),
];
when(() => mockBloc.state).thenReturn(ProductsLoaded(products));
// act
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<ProductBloc>.value(
value: mockBloc,
child: const ProductListPage(),
),
),
);
// assert
expect(find.text('商品1'), findsOneWidget);
expect(find.text('¥100'), findsOneWidget);
});
}
9.3 集成测试
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_ecommerce/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('完整购物流程', (tester) async {
// 启动应用
app.main();
await tester.pumpAndSettle();
// 登录
await tester.enterText(find.byKey(const Key('emailField')), 'test@test.com');
await tester.enterText(find.byKey(const Key('passwordField')), 'password');
await tester.tap(find.byKey(const Key('loginButton')));
await tester.pumpAndSettle();
// 浏览商品
await tester.tap(find.text('商品列表'));
await tester.pumpAndSettle();
// 添加购物车
await tester.tap(find.byIcon(Icons.add_shopping_cart).first);
await tester.pumpAndSettle();
// 验证购物车有商品
expect(find.text('购物车 (1)'), findsOneWidget);
});
}
十、性能优化
10.1 列表优化
// 使用 ListView.builder 替代 ListView
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(product: products[index]);
},
)
// 大数据量使用 CustomScrollView + Sliver
CustomScrollView(
slivers: [
SliverAppBar(...),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ProductCard(product: products[index]),
childCount: products.length,
),
),
],
)
10.2 图片优化
// 使用 cached_network_image
CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
memCacheWidth: 300, // 限制内存缓存大小
)
10.3 状态优化
// 使用 const 构造函数
const ProductCard({required this.product});
// 使用 ValueNotifier 替代 setState 局部刷新
ValueNotifier<int> counter = ValueNotifier(0);
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) {
return Text('$value');
},
)
总结
本文介绍了一个完整的企业级 Flutter 项目框架,核心要点:
- Clean Architecture:分层清晰,依赖向内
- BLoC 状态管理:可预测、易测试
- 依赖注入:编译期生成,类型安全
- 网络封装:拦截器统一处理
- 本地存储:Drift + SharedPreferences
- 组件化:可复用、易维护
项目结构模板:
flutter_enterprise/
├── lib/
│ ├── config/ # 配置
│ ├── core/ # 核心
│ ├── data/ # 数据层
│ ├── domain/ # 领域层
│ └── presentation/ # 表现层
├── test/ # 测试
└── integration_test/ # 集成测试
技术交流:欢迎交流 Flutter 企业级开发经验!
相关阅读
- Flutter Clean Architecture 最佳实践
- BLoC 模式深入解析
- Flutter 性能优化指南
互动话题
你在 Flutter 企业级开发中遇到过哪些架构问题?欢迎在评论区分享!