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