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