Flutter艺术探索-Repository模式:数据层抽象与复用

Flutter 数据层架构:Repository 模式详解与应用实战

引言

当我们开发的 Flutter 应用从小工具演变为真正的项目时,数据管理往往会变成一个"重灾区"。你是不是也见过这样的场景:网络请求和数据库操作直接混在业务逻辑里,改一处而动全身;想写个单元测试,却因为一堆外部依赖而无从下手。

Repository 模式就是为解决这类问题而生的。它的核心思路很简单:在业务逻辑和数据源之间建立一个抽象层。无论数据来自云端 API、本地 SQLite,还是内存缓存,上层业务都通过同一个清晰的接口进行交互。这让你的代码更干净,也更好测试和维护。

在这篇文章里,我将带你彻底搞懂 Repository 模式。我们会从一个实际例子出发,分析它如何解决常见痛点,并一步步构建一个完整、健壮的数据层架构。你还会看到缓存策略、错误处理等实战技巧。

一、我们为什么需要 Repository 模式?

1.1 传统做法带来的麻烦

在没有引入 Repository 模式之前,我们的代码常常长这样(是不是很眼熟?):

dart 复制代码
class ProductListViewModel extends ChangeNotifier {
  List<Product> products = [];
  
  Future<void> fetchProducts() async {
    try {
      // 网络请求直接写在这里
      final response = await http.get(Uri.parse('https://api.example.com/products'));
      final data = jsonDecode(response.body);
      products = (data as List).map((json) => Product.fromJson(json)).toList();
      
      // 可能还要顺手存个数据库
      await LocalDatabase.instance.saveProducts(products);
    } catch (e) {
      // 错误处理也混在一起
      print('Error: $e');
    }
    notifyListeners();
  }
}

这种写法短期内似乎很方便,但项目一复杂,问题就全暴露出来了:

  • 高度耦合ViewModel 牢牢绑定了具体的 http 客户端和数据库。哪天要换一个网络库,或者加个缓存,你得把业务逻辑翻个遍。
  • 测试困难 :想为这个 fetchProducts 写个单元测试?你得先准备好真实的网络环境和数据库,这根本不是单元测试,成了集成测试。
  • 重复代码 :同样的数据获取逻辑,可能在 ProductDetailViewModel 或别的 Bloc 里又被复制了一遍。
  • 错误处理混乱 :网络异常、JSON 解析失败、数据库错误全挤在一个 catch 块里,难以区分和处理。
  • 没有统一的缓存策略:缓存代码东一块西一块,想实现"优先内存,其次本地,最后网络"的智能缓存?工作量巨大。

1.2 Repository 模式如何破局

Repository 模式的核心思想是引入一个"中介"。这个中介(Repository)向上对业务层提供统一的数据 API,向下则管理着所有复杂的数据源细节。

它的优势很明显:

  1. 单一职责:Repository 就专心干一件事------获取和管理数据。业务层(如 ViewModel)则专注于界面逻辑和状态管理。
  2. 依赖倒置 :业务层只依赖于 ProductRepository 这个抽象接口,而不是具体的 http.Clientsqflite。这使得底层实现的替换变得轻而易举。
  3. 极强的可测试性 :测试业务逻辑时,你只需要模拟一个 MockProductRepository,让它返回预定数据或抛出特定异常即可,完全隔离了外部依赖。
  4. 数据源对业务透明 :业务层不需要知道数据是刚从网络拉取的,还是从本地缓存读取的。它只管调用 getProducts(), Repository 会自己决定最优的获取路径。
  5. 统一的错误门户:所有来自网络、数据库的底层异常,都可以在 Repository 层被捕获、转换,并向上抛出业务层能理解的、统一的异常类型。

二、动手实现 Flutter Repository 层

理论说完了,我们来看看具体怎么搭。这是一个典型的项目结构,我们会自底向上构建。

2.1 准备依赖项

先在 pubspec.yaml 里引入我们需要的库:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0          # 用于网络请求
  sqflite: ^2.3.0       # 本地 SQLite 数据库
  path: ^1.8.3
  provider: ^6.0.5      # 状态管理(也可用 riverpod/bloc 等)
  connectivity_plus: ^5.0.2 # 检查网络状态
  get_it: ^7.6.4        # 依赖注入(可选但推荐)

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.3.2       # 创建 Mock 对象,方便测试
  build_runner: ^2.3.3

2.2 定义领域模型

数据层的基础是模型。我们定义一个 Product 类,并实现序列化方法。

dart 复制代码
// lib/domain/models/product.dart
class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final String imageUrl;
  final DateTime createdAt;
  
  Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.imageUrl,
    required this.createdAt,
  });
  
  // 从 JSON 映射
  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'] ?? '',
      name: json['name'] ?? '',
      description: json['description'] ?? '',
      price: (json['price'] as num?)?.toDouble() ?? 0.0,
      imageUrl: json['imageUrl'] ?? '',
      createdAt: DateTime.parse(json['createdAt'] ?? DateTime.now().toIso8601String()),
    );
  }
  
  // 转换为 JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'description': description,
      'price': price,
      'imageUrl': imageUrl,
      'createdAt': createdAt.toIso8601String(),
    };
  }
  
  // 方便创建更新后的副本
  Product copyWith({
    String? id,
    String? name,
    String? description,
    double? price,
    String? imageUrl,
    DateTime? createdAt,
  }) {
    return Product(
      id: id ?? this.id,
      name: name ?? this.name,
      description: description ?? this.description,
      price: price ?? this.price,
      imageUrl: imageUrl ?? this.imageUrl,
      createdAt: createdAt ?? this.createdAt,
    );
  }
}

2.3 实现具体的数据源

数据源是真正干活的地方。我们把它们抽象出来,方便 Repository 调用和替换。

2.3.1 远程数据源(网络 API)
dart 复制代码
// lib/data/datasources/remote/product_remote_data_source.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

// 抽象类,定义远程数据源契约
abstract class ProductRemoteDataSource {
  Future<List<Product>> getProducts();
  Future<Product> getProductById(String id);
  Future<Product> createProduct(Product product);
}

// 具体实现
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
  final http.Client client;
  final String baseUrl;
  
  ProductRemoteDataSourceImpl({
    required this.client,
    this.baseUrl = 'https://api.example.com',
  });
  
  @override
  Future<List<Product>> getProducts() async {
    try {
      final response = await client.get(
        Uri.parse('$baseUrl/products'),
        headers: {'Content-Type': 'application/json'},
      );
      
      if (response.statusCode == 200) {
        final List<dynamic> data = json.decode(response.body);
        return data.map((json) => Product.fromJson(json)).toList();
      } else {
        // 抛出统一的 API 异常
        throw ApiException(
          message: 'Failed to load products',
          statusCode: response.statusCode,
        );
      }
    } on http.ClientException catch (e) {
      // 网络连接问题
      throw NetworkException(message: e.message);
    } on FormatException catch (e) {
      // 数据解析问题
      throw DataParsingException(message: e.message);
    }
  }
  
  // 其他方法(getProductById, createProduct)实现思路类似,此处省略...
}
2.3.2 本地数据源(SQLite 缓存)
dart 复制代码
// lib/data/datasources/local/product_local_data_source.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

// 抽象类,定义本地缓存操作
abstract class ProductLocalDataSource {
  Future<List<Product>> getCachedProducts();
  Future<void> cacheProducts(List<Product> products);
  Future<void> clearCache();
}

// 具体实现,使用 SQLite
class ProductLocalDataSourceImpl implements ProductLocalDataSource {
  static const String _tableName = 'cached_products';
  static Database? _database;
  
  // 数据库单例获取
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }
  
  Future<Database> _initDatabase() async {
    final path = join(await getDatabasesPath(), 'app_database.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE $_tableName(
            id TEXT PRIMARY KEY,
            name TEXT,
            description TEXT,
            price REAL,
            imageUrl TEXT,
            createdAt TEXT,
            cachedAt TEXT
          )
        ''');
      },
    );
  }
  
  @override
  Future<List<Product>> getCachedProducts() async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query(_tableName);
    
    return List.generate(maps.length, (i) {
      // 注意:缓存时我们可能加了字段(如 cachedAt),反序列化时只需模型字段
      return Product.fromJson({
        'id': maps[i]['id'],
        'name': maps[i]['name'],
        'description': maps[i]['description'],
        'price': maps[i]['price'],
        'imageUrl': maps[i]['imageUrl'],
        'createdAt': maps[i]['createdAt'],
      });
    });
  }
  
  @override
  Future<void> cacheProducts(List<Product> products) async {
    final db = await database;
    await db.transaction((txn) async {
      await txn.delete(_tableName); // 简单策略:先清空
      final batch = txn.batch();
      final now = DateTime.now().toIso8601String();
      
      for (final product in products) {
        batch.insert(_tableName, {
          ...product.toJson(),
          'cachedAt': now, // 额外记录缓存时间
        });
      }
      await batch.commit();
    });
  }
}

2.4 核心:Repository 实现

这里是粘合一切的大脑。Repository 决定数据从哪里来,并管理缓存策略。

dart 复制代码
// lib/domain/repositories/product_repository.dart
// 首先,定义业务层依赖的抽象接口
abstract class ProductRepository {
  Future<List<Product>> getProducts({bool forceRefresh = false});
  Future<Product> getProductById(String id);
  Future<Product> createProduct(Product product);
  Future<void> clearCache();
}

// lib/data/repositories/product_repository_impl.dart
import 'package:connectivity_plus/connectivity_plus.dart';

class ProductRepositoryImpl implements ProductRepository {
  final ProductRemoteDataSource remoteDataSource;
  final ProductLocalDataSource localDataSource;
  final Connectivity connectivity;
  
  // 简单的内存缓存
  final Map<String, Product> _memoryCache = {};
  List<Product>? _cachedProductList;
  DateTime? _lastCacheTime;
  static const Duration cacheDuration = Duration(minutes: 5);
  
  ProductRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.connectivity,
  });
  
  @override
  Future<List<Product>> getProducts({bool forceRefresh = false}) async {
    // 1. 检查内存缓存(如果未强制刷新且未过期)
    if (!forceRefresh && 
        _cachedProductList != null && 
        _lastCacheTime != null &&
        DateTime.now().difference(_lastCacheTime!) < cacheDuration) {
      return _cachedProductList!;
    }
    
    // 2. 检查网络状态
    final connectivityResult = await connectivity.checkConnectivity();
    final hasInternet = connectivityResult != ConnectivityResult.none;
    
    if (hasInternet) {
      try {
        // 3. 有网:尝试从网络获取
        final remoteProducts = await remoteDataSource.getProducts();
        
        // 4. 更新各级缓存
        _cachedProductList = remoteProducts;
        _lastCacheTime = DateTime.now();
        _updateLocalCache(remoteProducts); // 异步更新本地,不阻塞
        
        return remoteProducts;
      } catch (e) {
        // 5. 网络失败,降级到本地缓存
        print('网络请求失败,尝试使用本地缓存: $e');
        return _getProductsFromLocal();
      }
    } else {
      // 6. 无网络,直接使用本地缓存
      print('无网络连接,使用本地缓存');
      return _getProductsFromLocal();
    }
  }
  
  // 从本地数据源获取的辅助方法,包含错误处理
  Future<List<Product>> _getProductsFromLocal() async {
    try {
      final localProducts = await localDataSource.getCachedProducts();
      if (localProducts.isNotEmpty) {
        // 顺便更新一下内存缓存
        _cachedProductList = localProducts;
        _lastCacheTime = DateTime.now();
        return localProducts;
      } else {
        throw CacheException(message: '本地暂无缓存数据');
      }
    } catch (e) {
      throw CacheException(message: '从缓存加载失败: $e');
    }
  }
  
  // 异步更新本地缓存,失败也不影响主流程
  Future<void> _updateLocalCache(List<Product> products) async {
    try {
      await localDataSource.cacheProducts(products);
    } catch (e) {
      print('更新本地缓存失败: $e');
    }
  }
  
  // 其他方法(getProductById, createProduct)实现...
  // 思路类似:优先查内存,再查网络,并做好缓存更新
}

2.5 构建清晰的错误处理体系

统一的异常让上层处理起来更方便。

dart 复制代码
// lib/core/errors/exceptions.dart
// 应用异常基类
abstract class AppException implements Exception {
  final String message;
  final StackTrace? stackTrace;
  
  AppException({required this.message, this.stackTrace});
  
  @override
  String toString() => '$runtimeType: $message';
}

// 网络异常
class NetworkException extends AppException {
  NetworkException({required super.message, super.stackTrace});
}

// API 异常(可包含状态码)
class ApiException extends AppException {
  final int? statusCode;
  ApiException({required super.message, this.statusCode, super.stackTrace});
}

// 数据解析异常
class DataParsingException extends AppException {
  DataParsingException({required super.message, super.stackTrace});
}

// 缓存异常
class CacheException extends AppException {
  CacheException({required super.message, super.stackTrace});
}

// 业务逻辑异常,如未找到商品
class ProductNotFoundException extends AppException {
  final String id;
  ProductNotFoundException({required this.id})
      : super(message: '未找到ID为 $id 的商品');
}

2.6 ViewModel:连接数据与界面

ViewModel 变得非常清爽,它只关心业务状态。

dart 复制代码
// lib/presentation/viewmodels/product_list_viewmodel.dart
import 'package:flutter/foundation.dart';

class ProductListViewModel extends ChangeNotifier {
  final ProductRepository productRepository;
  
  ProductListViewModel({required this.productRepository});
  
  List<Product> _products = [];
  List<Product> get products => _products;
  
  bool _isLoading = false;
  bool get isLoading => _isLoading;
  
  String? _errorMessage;
  String? get errorMessage => _errorMessage;
  
  Future<void> loadProducts({bool forceRefresh = false}) async {
    if (_isLoading) return; // 防止重复加载
    
    _isLoading = true;
    _errorMessage = null;
    notifyListeners(); // 通知 UI 更新加载状态
    
    try {
      _products = await productRepository.getProducts(
        forceRefresh: forceRefresh,
      );
    } on AppException catch (e) {
      // 捕获已知异常,转换为用户友好信息
      _errorMessage = _getUserFriendlyErrorMessage(e);
    } catch (e) {
      // 捕获未知异常
      _errorMessage = '发生未知错误';
      if (kDebugMode) print('意外错误: $e');
    } finally {
      _isLoading = false;
      notifyListeners(); // 无论成功失败,最终都要更新状态
    }
  }
  
  String _getUserFriendlyErrorMessage(AppException exception) {
    if (exception is NetworkException) {
      return '网络连接异常,请检查后重试';
    } else if (exception is ApiException) {
      return '服务器错误 (${exception.statusCode}),请稍后再试';
    } else if (exception is CacheException) {
      return '暂无缓存数据,请连接网络后刷新';
    } else if (exception is ProductNotFoundException) {
      return '商品不存在';
    } else {
      return '加载商品列表失败,请重试';
    }
  }
  
  Future<void> refreshProducts() async {
    await loadProducts(forceRefresh: true);
  }
}

2.7 依赖注入:优雅地组合对象

使用 get_it 这样的服务定位器,可以避免手动传递依赖的麻烦。

dart 复制代码
// lib/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:connectivity_plus/connectivity_plus.dart';

final GetIt sl = GetIt.instance; // Service Locator

Future<void> init() async {
  // 外部依赖(第三方库对象)
  sl.registerLazySingleton(() => http.Client());
  sl.registerLazySingleton(() => Connectivity());
  
  // 数据源
  sl.registerLazySingleton<ProductRemoteDataSource>(
    () => ProductRemoteDataSourceImpl(client: sl(), baseUrl: 'https://api.example.com'),
  );
  sl.registerLazySingleton<ProductLocalDataSource>(
    () => ProductLocalDataSourceImpl(),
  );
  
  // 仓库(核心)
  sl.registerLazySingleton<ProductRepository>(
    () => ProductRepositoryImpl(
      remoteDataSource: sl(),
      localDataSource: sl(),
      connectivity: sl(),
    ),
  );
  
  // ViewModel(每次需要新实例,用 Factory)
  sl.registerFactory<ProductListViewModel>(
    () => ProductListViewModel(productRepository: sl()),
  );
}

// lib/main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await init(); // 初始化所有依赖
  
  runApp(
    MultiProvider(
      providers: [
        // 提供 Repository 给 widget 树
        Provider<ProductRepository>(create: (_) => sl<ProductRepository>()),
      ],
      child: const MyApp(),
    ),
  );
}

三、优化与实践建议

上面是一个完整的实现。在实际项目中,你还可以进一步优化:

1. 更智能的缓存策略 可以引入缓存优先级(内存 > SQLite > 网络)和过期策略。对于列表数据,可以记录最后更新时间,并在下次请求时携带 If-Modified-Since 头,以减少不必要的数据传输。

2. 响应式更新 结合 Stream,让 Repository 在数据更新时(例如,后台同步了新数据)能主动通知 ViewModel,而不是只能靠下拉刷新。

3. 请求合并与去重 在短时间内发生的多个相同请求,可以合并成一个,避免重复的网络消耗。

4. 离线队列 对于 createProduct 这类写操作,在网络断开时可以先存入本地队列,等网络恢复后自动重试。

结语

Repository 模式并不是一个复杂的概念,但它能为你的 Flutter 应用数据层带来结构上的清晰和长期的维护性。通过将数据源细节隐藏在一个统一的接口之后,你的业务逻辑会变得更纯粹、更易测试。

刚开始搭建可能会觉得多写了一些"模板代码",但随着项目发展,尤其是当需要切换数据源、添加缓存或进行深入测试时,你会发现这些前期的投入是非常值得的。

希望这篇详细的指南能帮助你构建出更健壮、更灵活的 Flutter 应用架构。

相关推荐
爱吃大芒果2 小时前
Flutter for OpenHarmony 实战: mango_shop 资源文件管理与鸿蒙适配
javascript·flutter·harmonyos
hhhjhl2 小时前
flutter_for_openharmony逆向思维训练app实战+学习日历实现
学习·flutter
AC赳赳老秦3 小时前
外文文献精读:DeepSeek翻译并解析顶会论文核心技术要点
前端·flutter·zookeeper·自动化·rabbitmq·prometheus·deepseek
爱吃大芒果3 小时前
Flutter for OpenHarmony 实战: mango_shop 购物车模块的状态同步与本地缓存处理
flutter·缓存·dart
2601_949543013 小时前
Flutter for OpenHarmony垃圾分类指南App实战:意见反馈实现
android·flutter
子春一3 小时前
Flutter for OpenHarmony:构建一个 Flutter 天气卡片组件,深入解析动态 UI、响应式布局与语义化设计
javascript·flutter·ui
雨季6663 小时前
Flutter 三端应用实战:OpenHarmony “极简文本行数统计器”
开发语言·前端·flutter·ui·交互
爱吃大芒果3 小时前
Flutter for OpenHarmony 适配:mango_shop 页面布局的鸿蒙多设备屏幕适配方案
flutter·华为·harmonyos
雨季6664 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态字体大小调节器”交互模式深度解析
开发语言·flutter·ui·交互·dart