本文由 简悦SimpRead 转码,原文地址 codewithandrea.com
深入介绍Flutter中的资源库模式:它是什么,何时使用,以及各种im......。
设计模式是有用的模板,可以帮助我们解决软件设计中常见的问题。
而当涉及到应用架构时,结构性设计模式可以帮助我们决定应用的不同部分是如何组织的。
在这种情况下,我们可以使用存储库模式来访问来自不同来源的数据对象,例如后端API,并将它们作为类型安全的实体提供给应用程序的域层(也就是我们业务逻辑所在的地方)。
在这篇文章中,我们将详细了解资源库模式。
- 它是什么以及何时使用它
- 一些实际的例子
- 使用具体的或抽象的类的实现细节以及它们的权衡
- 如何用资源库测试代码
我还会分享一个带有完整源代码的天气应用实例。
准备好了吗?让我们开始吧!
什么是资源库设计模式?
为了理解这一点,让我们考虑下面的架构图。
使用控制器、服务和存储库的Flutter应用程序架构
在这种情况下,存储库被发现在数据层。他们的工作是。
- 将领域模型(或实体)与数据层中的数据源的实现细节隔离。
- 将数据传输对象转换为域层所能理解的有效实体。
- (可选择地)执行诸如数据缓存的操作。
上图显示的只是架构你的应用程序的许多可能方式之一。如果你遵循不同的架构,如MVC、MVVM或Clean Architecture,事情看起来会有所不同,但同样的概念也适用。
还要注意小部件是如何属于表现层的,它与业务逻辑或网络代码无关。
如果你的小部件直接使用来自REST API或远程数据库的键值对工作,你就做错了。换句话说。不要把商业逻辑和你的UI代码混在一起。这将使你的代码更难测试、调试和推理。
何时使用资源库模式?
如果你的应用程序有一个复杂的数据层,有许多不同的端点返回非结构化的数据(如JSON),而你想将其与应用程序的其他部分隔离,那么存储库模式就非常方便了。
更广泛地说,以下是我觉得资源库模式最合适的几个用例。
- 与REST APIs对话
- 与本地或远程数据库(如Sembast、Hive、Firestore等)对话。
- 与特定设备的API对话(如权限、摄像头、位置等)。
这种方法的一个很大的好处是,如果你使用的任何第三方API有突破性的变化,你只需要更新你的版本库代码。
仅此一点,就使软件库100%地值得使用。💯
所以,让我们看看如何使用它们吧 🚀
仓库模式的实践
作为一个例子,我建立了一个简单的Flutter应用程序(这里是源代码),从OpenWeatherMap API获取天气数据。
通过阅读API docs,我们可以发现如何调用API,以及一些JSON格式的响应数据的例子。
存储库模式对于抽象出所有的网络和JSON序列化代码是非常好的。
例如,这里有一个抽象类,为我们的资源库定义了接口。
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
上面的WeatherRepository只有一个方法,但可以有更多(例如,如果你想支持所有的CRUD操作)。
重要的是,这个资源库允许我们定义一个合同,如何检索一个给定城市的天气。
我们需要用一个具体的类来实现WeatherRepository,该类使用网络客户端如http或dio来进行必要的API调用。
import 'package:http/http.dart' as http;
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
// implements the method in the abstract class
Future<Weather> getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}
所有这些实现细节都是数据层关心的问题,应用程序的其他部分不应该关心或甚至不知道这些细节。
解析JSON数据
当然,我们还必须定义一个Weather模型类(或实体),以及用于解析API响应数据的JSON序列化代码。
class Weather {
// TODO: declare all the properties we need
factory Weather.fromJson(Map<String, dynamic> json) {
// TODO: parse JSON and return validated Weather object
}
}
注意,虽然JSON响应可能包含许多不同的字段,但我们只需要解析将在用户界面中使用的字段。
我们可以手工编写JSON解析代码,或者使用代码生成包,如Freezed。要了解更多关于JSON序列化的信息,请看我的关于Dart中JSON解析的基本指南。
在应用程序中初始化存储库
一旦我们定义了一个资源库,我们就需要一种方法来初始化它,并使它能被应用程序的其他部分访问。
这样做的语法会根据你选择的DI/状态管理解决方案而改变。
下面是一个使用get_it的例子。
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton<WeatherRepository>(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
下面是另一个使用Riverpod软件包的提供者。
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});
如果你喜欢flutter_bloc包,这里有一个等价物。
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider<WeatherRepository>(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
))
底线是一样的:一旦你初始化了你的资源库,你就可以在你的应用程序的任何地方访问它(widgets, blocs, controllers, etc.)。
抽象类还是具体类?
在创建资源库时,一个常见的问题是这样的。你真的需要一个抽象类吗,或者你可以直接创建一个具体的类并取消所有的仪式?
这是一个非常有道理的问题,因为在两个类中添加越来越多的方法会变得相当乏味。
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
Future<Forecast> getHourlyForecast({required String city});
Future<Forecast> getDailyForecast({required String city});
// and so on
}
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
Future<Weather> getWeather({required String city}) { ... }
Future<Forecast> getHourlyForecast({required String city}) { ... }
Future<Forecast> getDailyForecast({required String city}) { ... }
// and so on
}
正如软件设计中经常出现的情况,答案是。看情况。
因此,让我们来看看每种方法的一些优点和缺点。
使用抽象类
- 优点:可以在一个地方看到我们版本库的接口,没有所有的杂乱。
- 优点:我们可以用一个完全不同的实现来交换资源库(例如
DioWeatherRepository而不是HttpWeatherRepository),并且只需改变初始化代码的一行,因为应用程序的其余部分只知道WeatherRepository。 - 缺点。当我们 "跳转到引用 "并把我们带到抽象类中的方法定义,而不是具体类中的实现时,VSCode会有点困惑。
- 缺点: 更多的模板代码。
只使用具体类
- 优点: 更少的模板代码。
- 优点: "跳转到引用 "只是工作,因为存储库的方法只能在一个类中找到。
- 缺点: 如果我们改变资源库的名称,交换到不同的实现需要更多的改变(尽管用VSCode很容易在整个项目中重命名)。
当决定使用哪种方法时,我们也应该弄清楚如何为我们的代码编写测试。
用版本库写测试
在测试过程中,一个常见的要求是用模拟或 "假的 "来交换网络代码,以便我们的测试运行得更快、更可靠。
然而,抽象类在这里并没有给我们带来任何优势,因为在Dart中,所有的类都有一个隐式接口。
这意味着我们可以这样做。
// note: in Dart we can always implement a concrete class
class FakeWeatherRepository implements HttpWeatherRepository {
// just a fake implementation that returns a value immediately
Future<Weather> getWeather({required String city}) {
return Future.value(Weather(...));
}
}
换句话说,如果我们打算在测试中模拟我们的资源库,就没有必要创建抽象类。
事实上,像mocktail这样的包就利用了这一点,我们可以像这样使用它们。
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock implements HttpWeatherRepository {}
final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));
模拟数据源
当你写测试的时候,你可以模拟你的存储库,像我们上面那样返回预制的响应。
但还有一个选择,那就是模拟底层数据源。
让我们回顾一下HttpWeatherRepository是如何定义的。
import 'package:http/http.dart' as http;
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
// implements the method in the abstract class
Future<Weather> getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}
在这种情况下,我们可以选择模拟传递给HttpWeatherRepository构造函数的http.Client对象。下面是一个测试例子,说明你如何做到这一点。
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MockHttpClient extends Mock implements http.Client {}
void main() {
test('repository with mocked http client', () async {
// setup
final mockHttpClient = MockHttpClient();
final api = OpenWeatherMapAPI();
final weatherRepository =
HttpWeatherRepository(api: api, client: mockHttpClient);
when(() => mockHttpClient.get(api.weather('London')))
.thenAnswer((_) => Future.value(/* some valid http.Response */));
// run
final weather = await weatherRepository.getWeather(city: 'London');
// verify
expect(weather, Weather(...));
});
}
最后,你可以选择是模拟版本库本身还是底层数据源,这取决于你要测试的内容。
在弄清楚了如何测试版本库之后,让我们回到最初关于抽象类的问题上来。
存储库可能不需要抽象类
一般来说,如果你需要许多符合相同接口的实现,创建一个抽象类是有意义的。
例如,StatelessWidget和StatefulWidget都是Flutter SDK中的抽象类,因为它们是要被子类化的。
但在使用资源库时,你可能只需要一个给定资源库的实现。
有可能你只需要一个给定资源库的实现,你可以将其定义为一个单一的具体类。
The Lowest Common Denominator
把所有东西都放在一个接口后面,也会把你锁定在选择具有不同功能的API之间的最小公分母。
也许一个API或后端支持实时更新,这可以用基于流的API进行建模。
但如果你使用的是纯REST(没有websockets),你只能发送一个请求并得到个个响应,这最好用基于未来的API来建模。
处理这个问题很简单:只要使用一个基于流的API,如果你使用REST,只需返回一个有一个值的流。
但有时会有更广泛的API差异。
例如,Firestore支持事务和分批写入。这类API在引擎盖下使用了构建器模式,这种方式不容易在通用接口后面被抽象出来。
如果你迁移到一个不同的后端,新的API有可能会有很大的不同。换句话说,为你当前的API提供未来保护往往是不切实际的,而且会产生反效果。
仓库横向扩展
随着你的应用程序的增长,你可能会发现自己在一个给定的存储库中添加越来越多的方法。
如果你的后端有一个大的API表面,或者你的应用程序连接到许多不同的数据源,这很可能发生。
在这种情况下,考虑创建多个资源库,将相关的方法放在一起。例如,如果你正在建立一个电子商务应用,你可以为产品列表、购物车、订单管理、认证、结账等建立单独的存储库。
保持简单
像往常一样,保持简单的东西总是一个好主意。所以不要太过纠结于你的API。
你可以按照你需要使用的API来模拟你的版本库的接口,然后就可以收工了。如果需要的话,你可以随时重构。👍
结语
如果说我希望你能从这篇文章中得到什么东西,那就是这个。
使用存储库模式来隐藏你的数据层的所有实现细节(例如JSON序列化)。因此,你应用程序的其他部分(领域和表现层)可以直接处理类型安全的模型类/实体。而你的代码库也会对你所依赖的包中的破坏性变化变得更加有弹性。
如果有的话,我希望这个概述能鼓励你更清楚地思考应用程序的架构,以及拥有独立的表现层、应用层、域层和数据层的重要性,并有明确的界限。
即将推出:完整的Flutter课程
我正在开发一个全新的课程,它将非常深入地介绍Flutter应用架构,以及其他重要的主题,如状态管理、导航和路由、测试等等。
如果您对此感兴趣,您可以查看课程页面或在此报名。