最近在開發過程當中看到不少同窗問過這個問題。我想要在網絡請求失敗的時候彈出一個統一的處理頁面告訴用戶檢查網絡鏈接。因爲這個行爲能夠發生在任何頁面,咱們固然不但願在每個頁面之中都要從新實現一遍這個邏輯,那樣耦合就過高了,這時候咱們的第一反應是在網絡請求後某個部分統一處理這部分邏輯。設計模式
看上去沒什麼問題,可是若是你作過這個需求話,你就會發現:當咱們實現跳轉提示頁面的時候,須要使用到 Navigator
這個組件。回想一下咱們通常是如何進行跳轉的。網絡
Navigator.of(context).pushNamed('/errorPage');
app
咱們發現,要實現跳轉到 ErrorPage 這個操做,咱們缺乏了一個重要的元素 BuildContext
。Navigator.of(context)
操做實際上是在祖先節點中尋找最近的一個 NavigatorState
。而這裏的 BuildContext
就是尋找的起點。 因此不少同窗都卡在這裏了,那咱們就來解決這個問題。less
在正式開始本文以前你須要已經理解下面幾個概念:ide
什麼是Navigator,MaterialApp作了什麼
咱們常常會在應用中打開許多頁面,當咱們返回的時候,它會前後退到上一個打開的頁面,而後一層一層後退,沒錯這就是一個堆棧。而在Flutter中,則是由Navigator來負責管理維護這些頁面堆棧。函數
壓一個新的頁面到屏幕上
Navigator.of(context).push
把路由頂層的頁面移除
Navigator.of(context).pop
複製代碼
一般咱們咱們在構建應用的時候並無手動去建立一個 Navigator,也能進行頁面導航,這又是爲何呢。post
沒錯,這個 Navigator 正是 MaterialApp 爲咱們提供的。可是若是 home,routes,onGenerateRoute 和 onUnknownRoute 都爲 null,而且 builder 不爲 null,MaterialApp 則不會建立任何 Navigator。性能
既然咱們的 Navigator.of(context)
實際上就是在獲取 MaterialApp 提供的 NavigatorState
實例。而 BuildContext
跟當前 Element 有關,要統一控制實際上至關複雜。咱們是否可使用另一種方式來獲取 Navigator
,這樣就能夠再也不受 BuildContext 的約束了。測試
要獲取某個 Widget 咱們在以前的文章中介紹了可使用 GlobalKey
來實現。那咱們應該如何獲取到 Navigator
呢?ui
class _AppState extends State<App> {
GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'navigate');
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
home: HomeScreen(),
);
}
}
複製代碼
因爲 MaterialApp 封裝了 Navigator,而且將 Navigator 的 key 屬性做爲 navigatorKey 暴露出來,咱們只須要綁定一個 GlobalKey 就好了。
可是如今問題又來了,咱們假如想要在外部使用這個 GlobalKey 好像仍是不太方便。咱們的 Navigator 可能在多處須要使用,假如直接依賴的話每一處都包含了用於建立、定位和管理依賴項的重複代碼。假如咱們如今僅僅只是想進行網絡調試的測試,因爲依賴了 Navigator 相關的代碼,想要進行測試很是困難。
這時候就須要 ServiceLocator 來幫助咱們進行解耦。
這是一種經典的設計模式,主要目的是將類與依賴解耦,讓類在編譯的時候並知道依賴相的具體實現。從而提高其隔離性和可測試性。
而今天咱們要介紹的是一個來自 Flutter Community 和 Thomas Burkhart 製做的庫 get_it。它是一個輕量級 ServiceLocator 庫,僅僅用到了 99 行代碼(包括註釋)。建議有時間都去閱讀一下。
get_it 很是簡單,使用就分兩步。
首先建立出一個 GetIt 容器對象。
GetIt getIt = new GetIt();
複製代碼
而後把須要註冊的服務在容器中註冊。
getIt.registerSingleton<AppModel>(new AppModelImplementation());
getIt.registerLazySingleton<RESTAPI>(() =>new RestAPIImplementation());
複製代碼
在須要使用到這個依賴的地方咱們仍是經過這個容器來獲取依賴。
var myAppModel = getIt<AppModel>();
你也可使用 var myAppModel = getIt.get<AppModel>();
這個方法,效果是同樣的。
因爲 dart 支持全局變量,咱們就把容器直接寫在一個 Dart 文件中就行了。是否是很簡單呢?
這樣咱們的服務就是在容器中建立的,在實際依賴的時候,咱們能夠只依賴於接口,而後經過容器注入(DI)實現了該接口的實際對象,達到了解耦的效果。
如今咱們來看看該如何使用 get_it 實現一個 NavigateService。
咱們在項目中新建一個 service_locator.dart 文件。而後在這個文件中建立一個全局 GetIt 實例。
import 'package:get_it/get_it.dart';
final GetIt getIt = GetIt();
void setupLocator(){}
複製代碼
這裏先寫上 setupLocator 方法,以後會在這裏進行服務註冊。
咱們把導航相關的功能封裝成 Service,方便以後使用。
import 'package:flutter/material.dart';
class NavigateService {
final GlobalKey<NavigatorState> key = GlobalKey(debugLabel: 'navigate_key');
NavigatorState get navigator => key.currentState;
get pushNamed => navigator.pushNamed;
get push => navigator.push;
}
複製代碼
經過 key.currentState 獲取到 NavigatorState 實例。
我這裏簡單暴露了導航的 push 和 pushName 功能,你能夠根據本身的功能來進行擴展。
如今就須要在容器中註冊這個服務,回到 service_locator.dart。
void setupLocator(){
getIt.registerSingleton(NavigateService());
}
複製代碼
經過調用 registerSingleton,咱們在容器中註冊了一個單例模式使用的 NavigateService。以後咱們全部須要註冊的 Service 都在這裏註冊一遍便可。
剛剛已經寫好了註冊函數,如今就須要在咱們的 Flutter 應用運行時初始化一次,main 函數是一個不錯的選擇。
void main() {
setupLocator();
runApp(App());
}
複製代碼
這樣在咱們程序運行的時候就可以把服務都初始化到容器中。
剛纔咱們說了,要想得到 Navigator 須要在 MaterialApp 的 navigatorKey 綁定一個 GlobalKey。因此咱們如今經過容器注入服務,來綁定這個 GlobalKey。
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: getIt<NavigateService>().key,
routes: {'/ErrorScreen': (_) => ErrorScreen()},
home: HomeScreen(),
);
}
}
複製代碼
上面經過 getIt() 注入了 NavigateService 的依賴。這個 getIt 就是咱們的全局實例。
而後添加了一個命名路由。這裏我把 HomeScreen 和 ErrorScreen 的代碼放在下面。
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {
getIt<NavigateService>().pushNamed('/ErrorScreen');
}),
);
}
}
class ErrorScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
color: Colors.red,
child: Text('Error'),
);
}
}
複製代碼
在 HomeScreen 中點擊一下 FloatingActionButton 就會經過注入的 NavigateService 跳轉到 ErrorScreen。
在進行跳轉時,咱們能夠看到並無使用 context。
getIt<NavigateService>().pushNamed('/ErrorScreen');
這樣你就能夠在你想要的地方恰當的處理一些全局導航操做了。它的一個巨大的好處在於你不只能夠在 Widget 中使用,並且能夠在任何地方使用容器中的服務。
GetIt 提供了多種註冊方式,這將會影響這些對象的生命週期。目前有三種:
void registerFactory<T>(FactoryFunc<T> func)
每次都會返回新的實例。void registerSingleton<T>(T instance)
每次返回同一實例。 這種模式須要手動初始化,就像咱們上面例子中那樣。void registerLazySingleton<T>(FactoryFunc<T> func)
這種方式只有第一次注入依賴的時候,纔會初始化服務,而且每次返回相同實例。若是你在容器中註冊了兩次同一服務的話,默認狀況下會在調試模式中獲得一個斷言,就像下面這樣。
void setupLocator(){
getIt.registerSingleton(NavigateService());
getIt.registerSingleton(NavigateService());
}
複製代碼
Failed assertion: line 53 pos 12: 'allowReassignment || !_factories.containsKey(T)': Type NavigateService is already registered
get_it 會認爲你多是寫錯了,因此提醒你這裏註冊了兩次相同服務。若是你真的必須覆蓋註冊,那麼你能夠經過設置屬性 allowReassignment == true
來關閉此斷言。
若是你想要重置全部容器,能夠調用 reset()
方法。通常在作測試的時候會用到。
咱們在上面看到,當咱們使用 ServiceLocator 以後,實現了控制反轉(Ioc)。服務再也不由使用者建立,而是經過容器注入。這樣咱們能夠再也不依賴於具體的實現,而是依賴於一層薄薄的的接口。這樣調用者再也不知道服務具體實現細節,能夠很輕鬆的使用 mock 數據進行替換。ServiceLocator 其實就是一種特殊的控制反轉。
Dependency Injection 實際上和 ServiceLocator 解決的是一樣的問題。可是它又與DI的實現原理上有所不一樣。因爲 Flutter 爲了減小打包後應用體積禁用了 dart 的反射包,因此你不知道神奇注入對象的來源,這樣一來大多數依賴於反射的 DI 包也就無法用了。
咱們能夠從 get_it 的源碼中看到,這個 ServiceLocator 就是用一個 map 在儲存數據。
final _factories = new Map<Type, _ServiceFactory<dynamic>>();
複製代碼
因此獲取服務的性能是 O(1)。
本文參考瞭如下資料:
感興趣的同窗能夠去閱讀一下大師的文章。
此次介紹的庫很是輕量,你能夠很快速的上手它。這裏你可能會以爲它與 InheritWidget 有些類似。雖然都在解決模型依賴問題,get_it 不只可以在 Widget tree 中進行使用,並且可以解決模型間的依賴問題。你們能夠根據本身項目的狀況來選擇使用。
若是文章中還存在任何問題還請老師指正!歡迎在下方評論區以及個人郵箱1652219550a@gmail.com 一塊兒討論,我會及時回覆!
題外話,個人我的博客也在同步連載中!歡迎各位光顧鴨 xinlei.dev/