現(xiàn)代化Flutter架構(gòu)-Riverpod數(shù)據(jù)層
設(shè)計(jì)模式是幫助我們解決軟件設(shè)計(jì)中常見問題的有用模板。
說到應(yīng)用程序架構(gòu),結(jié)構(gòu)設(shè)計(jì)模式可以幫助我們決定如何組織應(yīng)用程序的不同部分。
在這種情況下,我們可以使用Repository模式從各種來源(如后端 API)訪問數(shù)據(jù)對象,并將它們作為類型安全的實(shí)體提供給應(yīng)用程序的領(lǐng)域?qū)樱次覀兊臉I(yè)務(wù)邏輯的所在層)。
在本文中,我們將詳細(xì)了解Repository Pattern:
- 它是什么,何時(shí)使用
- 一些實(shí)際示例
- 使用具體類或抽象類的實(shí)現(xiàn)細(xì)節(jié)及其取舍
- 如何使用Repository測試代碼
我還將分享一個(gè)帶有完整源代碼的天氣應(yīng)用程序示例。
準(zhǔn)備好了嗎?讓我們開始吧!
什么是Repository Pattern?
要理解這一點(diǎn),讓我們來看看下面的架構(gòu)圖:
圖片
在這種情況下,Repository位于數(shù)據(jù)層。它們的任務(wù)是:
- 將領(lǐng)域模型(或?qū)嶓w)與數(shù)據(jù)層中數(shù)據(jù)源的實(shí)現(xiàn)細(xì)節(jié)隔離開來。
- 將數(shù)據(jù)傳輸對象轉(zhuǎn)換為領(lǐng)域?qū)涌衫斫獾挠行?shí)體
- (可選)執(zhí)行數(shù)據(jù)緩存等操作
?
上圖顯示的只是架構(gòu)應(yīng)用程序的多種可能方法之一。如果您采用不同的架構(gòu)(如 MVC、MVVM 或簡潔架構(gòu)),情況會有所不同,但概念是相同的。
還要注意的是,Widget屬于表現(xiàn)層,與業(yè)務(wù)邏輯或網(wǎng)絡(luò)代碼無關(guān)。
?
如果您的 widget 直接使用來自 REST API 或遠(yuǎn)程數(shù)據(jù)庫的鍵值對,那您就做錯了。換句話說:不要將業(yè)務(wù)邏輯與用戶界面代碼混在一起。這會使你的代碼更難測試、調(diào)試和推理。
何時(shí)使用Repository Pattern?
如果您的應(yīng)用程序有一個(gè)復(fù)雜的數(shù)據(jù)層,其中有許多不同的端點(diǎn)返回非結(jié)構(gòu)化數(shù)據(jù)(如 JSON),而您希望將這些數(shù)據(jù)與應(yīng)用程序的其他部分隔離開來,那么Repository Pattern就非常方便。
廣而言之,以下是我認(rèn)為最適合使用Repository模式的幾種用例:
- 與 REST API 通信
- 與本地或遠(yuǎn)程數(shù)據(jù)庫(如 Sembast、Hive、Firestore 等)通信
- 與特定設(shè)備的 API(如權(quán)限、攝像頭、位置等)通信
這種方法的一大好處是,如果您使用的任何第三方應(yīng)用程序接口發(fā)生重大變更,您只需更新版本庫代碼即可。
僅憑這一點(diǎn),Repository就值得 100%使用。??
讓我們看看如何使用它們!??
實(shí)踐中的Repository Pattern
舉個(gè)例子,我構(gòu)建了一個(gè)簡單的 Flutter 應(yīng)用程序(這里是源代碼),從 OpenWeatherMap API 獲取天氣數(shù)據(jù)。
通過閱讀 API 文檔,我們可以找到如何調(diào)用 API,以及一些 JSON 格式響應(yīng)數(shù)據(jù)的示例。
Repository模式非常適合抽象掉所有網(wǎng)絡(luò)和 JSON 序列化代碼。
例如,這里有一個(gè)抽象類,定義了Repository的接口:
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
上述 WeatherRepository 只有一個(gè)方法,但也可以有更多方法(例如,如果您想支持所有 CRUD 操作)。
重要的是,該Repository允許我們?yōu)?,如何檢索給定城市的天氣定義一個(gè)接口。
我們需要用一個(gè)具體類來實(shí)現(xiàn) WeatherRepository,該類可以使用網(wǎng)絡(luò)客戶端(如 http 或 dio)進(jìn)行必要的 API 調(diào)用:
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
}
}
所有這些實(shí)現(xiàn)細(xì)節(jié)都與數(shù)據(jù)層有關(guān),應(yīng)用程序的其他部分不應(yīng)該關(guān)心或知道這些細(xì)節(jié)。解析 JSON 數(shù)據(jù) 當(dāng)然,我們還必須定義氣象模型類(或?qū)嶓w),以及用于解析 API 響應(yīng)數(shù)據(jù)的 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 響應(yīng)可能包含許多不同的字段,但我們只需要解析將在用戶界面中使用的字段。我們可以手動編寫 JSON 解析代碼,或者使用代碼生成包(如 Freezed)。
在應(yīng)用程序中初始化Repository
一旦定義了Repository,我們就需要一種方法來初始化它,并使應(yīng)用程序的其他部分可以訪問它。執(zhí)行此操作的語法會根據(jù)您選擇的 DI/狀態(tài)管理解決方案而改變。下面是一個(gè)使用 get_it 的示例:
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton<WeatherRepository>(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
下面是另一個(gè)使用 Riverpod 軟件包中的提供程序的例子:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});
如果你喜歡 flutter_bloc 軟件包,這里也有相應(yīng)的功能:
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider<WeatherRepository>(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
))
底層是一樣的:一旦初始化了Repository,就可以在應(yīng)用程序的其他任何地方(Widget、模塊、Controller等)訪問它。
抽象類還是具體類?
在創(chuàng)建Repository時(shí),一個(gè)常見的問題是:你真的需要一個(gè)抽象類嗎?這是個(gè)非常合理的問題,因?yàn)樵趦蓚€(gè)類中添加越來越多的方法可能會變得相當(dāng)乏味:
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
}
正如軟件設(shè)計(jì)中經(jīng)常出現(xiàn)的情況一樣,答案是:視情況而定。
因此,讓我們來看看每種方法的優(yōu)缺點(diǎn)。
使用抽象類
優(yōu)點(diǎn):我們可以在一個(gè)地方看到Repository的接口,而不會感到雜亂無章。
優(yōu)點(diǎn):我們可以將Repository換成完全不同的實(shí)現(xiàn)(例如 DioWeatherRepository 而不是 HttpWeatherRepository),只需修改一行初始化代碼,因?yàn)閼?yīng)用程序的其他部分只知道 WeatherRepository。
缺點(diǎn):當(dāng)我們 “跳轉(zhuǎn)到引用 ”時(shí),VSCode 會有點(diǎn)困惑,它會把我們帶到抽象類中的方法定義,而不是具體類中的實(shí)現(xiàn)。
缺點(diǎn):更多模板代碼。
只使用具體類
優(yōu)點(diǎn):減少模板代碼。
優(yōu)點(diǎn):“跳轉(zhuǎn)到引用 ”只適用于一個(gè)類中的Repository方法。
缺點(diǎn):如果我們更改了Repository名稱,那么切換到不同的實(shí)現(xiàn)就需要進(jìn)行更多更改(不過使用 VSCode 對整個(gè)項(xiàng)目進(jìn)行重命名很容易)。
在決定使用哪種方法時(shí),我們還應(yīng)考慮如何為代碼編寫測試。
使用Repository編寫測試代碼
在測試過程中,一個(gè)常見的要求是將網(wǎng)絡(luò)代碼換成模擬代碼或 “偽代碼”,這樣我們的測試就能運(yùn)行得更快、更可靠。
然而,抽象類并不能給我們帶來任何優(yōu)勢,因?yàn)樵?Dart 中,所有類都有一個(gè)隱式接口。
這意味著我們可以這樣做:
// 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(...));
}
}
換句話說,如果我們打算在測試中模擬我們的Repository,就沒有必要創(chuàng)建抽象類。事實(shí)上,像 mocktail 這樣的包就利用了這一點(diǎn),我們可以這樣使用它們:
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock implements HttpWeatherRepository {}
final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));
模擬數(shù)據(jù)源
在編寫測試時(shí),可以模擬Repository并返回預(yù)制響應(yīng),就像我們上面做的那樣。但還有另一種方法,那就是模擬底層數(shù)據(jù)源。讓我們回顧一下 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 構(gòu)造函數(shù)的 http.Client 對象。下面是一個(gè)測試示例,展示了如何做到這一點(diǎn):
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(...));
});
}
最后,你可以根據(jù)要測試的內(nèi)容,選擇是模擬Repository本身還是模擬底層數(shù)據(jù)源。
了解了如何測試版本庫之后,讓我們回到最初關(guān)于抽象類的問題上來。
Repository可能不需要抽象類
一般來說,如果你需要許多符合相同接口的實(shí)現(xiàn),創(chuàng)建抽象類是有意義的。
例如,在 Flutter SDK 中,StatelessWidget 和 StatefulWidget 都是抽象類,因?yàn)樗鼈兛梢员蛔宇惢?/span>
但在使用Repository時(shí),您可能只需要一個(gè)給定Repository的實(shí)現(xiàn)。
您很可能只需要一個(gè)特定Repository的實(shí)現(xiàn),您可以將其定義為一個(gè)單一的具體類。
最小公分母
把所有東西都放在接口后面,也會使你不得不在具有不同功能的 API 之間選擇最小公分母。
也許某個(gè) API 或后端支持實(shí)時(shí)更新,這可以用基于 Stream 的 API 來建模。
但如果您使用的是純 REST(不含 websockets),您只能發(fā)送一個(gè)請求并獲得一個(gè)響應(yīng),這最好使用基于 Future 的 API 來建模。
處理這個(gè)問題非常簡單:只需使用基于流的 API,如果使用的是 REST,則只需返回包含一個(gè)值的流即可。
但有時(shí)會存在更廣泛的 API 差異。
例如,F(xiàn)irestore 支持事務(wù)和批量寫入。這類 API 在源碼中使用了構(gòu)建器模式,而這種模式不容易抽象為通用接口。
如果遷移到不同的后端,新的 API 很可能會有很大不同。換句話說,面向未來的當(dāng)前應(yīng)用程序接口往往不切實(shí)際,而且會適得其反。
Repository橫向擴(kuò)展
隨著應(yīng)用程序的增長,您可能會發(fā)現(xiàn)自己向給定的Repository中添加的方法越來越多。
如果您的后端有很大的 API 列表,或者如果您的應(yīng)用程序連接到許多不同的數(shù)據(jù)源,就可能出現(xiàn)這種情況。
在這種情況下,可以考慮創(chuàng)建多個(gè)Repository,將相關(guān)的方法放在一起。例如,如果您正在構(gòu)建一個(gè)電子商務(wù)應(yīng)用程序,您可以為產(chǎn)品列表、購物車、訂單管理、身份驗(yàn)證、結(jié)賬等創(chuàng)建單獨(dú)的Repository。
保持簡單
與往常一樣,保持簡單總是個(gè)好主意。因此,不要對應(yīng)用程序接口想得太多。
您可以根據(jù)您需要使用的 API 來構(gòu)建您的版本庫接口模型,然后就可以收工了。如果需要,您可以隨時(shí)重構(gòu)。??
結(jié)論
如果我想讓你從這篇文章中得到什么啟發(fā),那就是:使用Repository模式來隱藏你的代碼:
使用Repository模式來隱藏?cái)?shù)據(jù)層的所有實(shí)現(xiàn)細(xì)節(jié)(如 JSON 序列化)。這樣,應(yīng)用程序的其余部分(領(lǐng)域?qū)雍捅憩F(xiàn)層)就可以直接處理類型安全的模型類/實(shí)體。您的代碼庫也將變得更有彈性,可以抵御您所依賴的包中出現(xiàn)的破壞性變化。
如果說有什么收獲的話,我希望這篇概述能鼓勵您更清晰地思考應(yīng)用程序架構(gòu),以及擁有邊界清晰的獨(dú)立表現(xiàn)層、應(yīng)用層、領(lǐng)域?qū)雍蛿?shù)據(jù)層的重要性。
本文翻譯自:https://codewithandrea.com/articles/flutter-repository-pattern/