作者
发布于 2026-03-08 / 1 阅读
0

企业级 Flutter 项目框架搭建实战

企业级 Flutter 项目框架搭建实战:从 0 到 1 构建可维护的大型应用

本文以一个完整的电商 Flutter 项目为例,详细介绍如何搭建企业级 Flutter 应用框架,涵盖架构设计、状态管理、网络封装、本地存储、路由管理等核心模块。


目录

  1. 项目概述
  2. 整体架构设计
  3. 核心模块详解
  4. 状态管理方案
  5. 网络层封装
  6. 本地数据存储
  7. 路由与导航
  8. UI 组件化
  9. 测试策略
  10. 性能优化

一、项目概述

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 项目框架,核心要点:

  1. Clean Architecture:分层清晰,依赖向内
  2. BLoC 状态管理:可预测、易测试
  3. 依赖注入:编译期生成,类型安全
  4. 网络封装:拦截器统一处理
  5. 本地存储:Drift + SharedPreferences
  6. 组件化:可复用、易维护

项目结构模板

flutter_enterprise/
├── lib/
│   ├── config/          # 配置
│   ├── core/            # 核心
│   ├── data/            # 数据层
│   ├── domain/          # 领域层
│   └── presentation/    # 表现层
├── test/                # 测试
└── integration_test/    # 集成测试

技术交流:欢迎交流 Flutter 企业级开发经验!

相关阅读

  • Flutter Clean Architecture 最佳实践
  • BLoC 模式深入解析
  • Flutter 性能优化指南

互动话题
你在 Flutter 企业级开发中遇到过哪些架构问题?欢迎在评论区分享!