flutter架构:Repository设计模式
在软件开发中,我们可以使用设计模式有效的解决我们软件设计中的常见问题。而在app的架构中,structural设计模式可以帮助我们很好的划分应用结构。
在本文,我们将使用Repository设计模式,访问各种来源的数据,如后端的API,蓝牙等等。并将这些数据转化成类型安全的实体类提供给上层(领域层),即我们业务逻辑所在的位置。
本文中我们将详细讲解Repository设计模式,包含以下部分:
- Repository设计模式是什么以及何时使用它
- 使用具体和抽象类的实现以及如何权衡使用
- 如何使用Repository设计模式单元测试
1.什么是Repository设计模式
为了帮助我们理解,我们先看看下面的app架构设计图:
在这张图中,repositories位于 数据层(data layer),它的作用是:
- 将领域模型(或实体)与数据源(data sources)的实现细节隔离开来。
- 将数据源的数据对象转换为领域层(domain layer)中使用的实体或模型
- (可选)执行数据缓存等操作。
上图仅展示了构建APP的其中一种架构模式。如果使用其他的架构模式,例如 MVC、MVVM 或 Clean Architecture,虽然看起来不一样,但repository设计模式的应用都一样。
还要注意在**表示层(UI或Presentation)**中,widget是需要与业务逻辑或网络等是无关的。
如果在Widget中直接使用来自REST API 或远程数据库的key-value,这样做是有很大风险的。换句话说:不要将业务逻辑与您的 UI 代码混合,这会使你的代码更难测试、调试和推理。
2.什么时候使用Repository设计模式
如果你的APP有一个复杂的数据层,包含许多不同的数据来源,并且这些来源返回非结构化数据(例如 JSON),这样需要将其与其他部分隔离,这时候使用Repository设计模式非常方便。
如果说更具体的话,下面这些场景我认为Repository设计模式更合适:
- 与 REST API 交互
- 与本地或远程数据库(例如 Sembast、Hive、Firestore 等)交互
- 与设备的 API(例如权限、摄像头、位置等)交互
这样做的最大的好处是,如果任何第三方API 发生重大更改,我们只需要更新Repository的代码。
仅仅这一点就我就觉得使Repository模式 是100% 值得我们在实际中使用的。💯
下面我们就看看如何使用吧!🚀
3.Repository设计模式在实际中的使用
我们以OpenWeatherMap(https://openweathermap.org/api)提供的天气查询API为例,做一个简单的天气查询APP。
我们先看看API 文档(openweathermap.org/current),先了解需要如何调用 API,以及响应数据的JSON 格式。
我们通过Repository设计模式能非常快速的抽象出所有网络相关和 JSON 序列化代码。下面,我们就来具体实现吧。
首先,我们为repository定义一个抽象类:
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
复制代码
我们的WeatherRepository现在只添加了一个方法,但是在实际应用中我们可能会有很多个,根据需求决定。
接下来,我们还需要一个具体的实现类,来实现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
}
}
这些具体的细节在data layer实现,其他层就不需要关心数据是如何来的。
3.1数据解析
我们需要定义一个具体的model(或者entity),用来接收和解析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
}
}
api返回的字段可能很多,我们这里只需要解析我们使用到的字段。
json解析有很多方法,ide(vscode、android studio)提供了很多插件,帮助我们快速的实现fromJson,感兴趣的同学可以自己去找找。
3.2 初始化repository
repository定义后,我们需要在一个合适的时机进行初始化,以便app其他层能够访问。
如何进行repository的初始化,我们需要根据我们选择的状态管理工具来决定。
例如,我们使用get_it(pub.dev/packages/ge…)来进行管理:
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());
});
或者是使用bloc:
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider<WeatherRepository>(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
))
不管使用哪种方式,我们的目的是repository初始化一次全局都可以使用。
4.抽象还是具体?
当创建一个repository的时候,我们也许会有疑惑,我们需要创建一个抽象类吗?还是只需要一个具体类?如果添加的方法越来越多,可能会觉得工作越来越多,如下:
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
}
到底需不需要,答案就像软件设计中的给出的一样:视情况而定。那么,我们就来分析下两种方法的优缺点。
4.1 使用抽象类
- 优点:提供了统一的接口,不关心具体实现,使用时比较统一。
- 优点 : 完全可以使用不同的实现 ****,替换时只需要更改初始化时的一行代码。
- 缺点**:**当我们在IDE点击“跳转到引用”时只能到抽象类中的方法定义而不是具体类中的实现。
- 缺点:会写更多代码。
4.2只有具体类
- 优点:更少的代码。
- 优点:IDE中点击“跳转到引用”能跳转到正确的方法。
- 缺点:如果我们repository名字,需要多处修改。
但是呢,具体如何选择,我们还有一个重要的参考标准,就是我们需要为它添加单元测试。
5.repository的单元测试
单元测试时,我们需要mock掉网络调用的部分,是我们的测试更快更准确。
这样的话,我们使用抽象类就没有任何优势,因为在Dart中所有类都有一个隐式接口,如下,我们可以这样mock数据:
// 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(...)));
在测试里,我们可以mock HttpWeatherRepository,也可以mock HttpClient,
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(...));
});
}
具体的是mock Repository还是HttpClient,可以根据你需要测试的内容来定。
最后,对于Repository到底需不需要抽象类,我觉得是没必要的,对于Repository我们只需要一个具体的实现,而且每个Repository是不一样的。
Repository的扩展
这里我们只实例了一个库,但是随着业务的增长,我们的应用功能越来越多,在一个Repository里添加所有api显然不是一个明智的选择。
所有,我们可以根据场景划分不同的Repository,将相关的方法放在同一个Repository中。比如在电商app中,我们划分为产品列表、购物车、订单管理、身份验证、结算等Repository。
总结
所有事情保持简单是最好的,我希望这篇概述能够激发大家更清晰地去思考App的架构,以及分层(UI层、领域和数据层)的重要性。
作者:AaronLei
链接:https://juejin.cn/post/7053426006312845325
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。