剖析Flutter的常用庫get_it

QiShare發表於2021-07-27

前言

通常外企的領導在工作群發通知訊息

就馬上看到有人回覆:I get it.

這時候,也會有人說: I got it.

只是當你說 I get it. 的時候,更像是明白了一些之前不明白的事情,或者配合其他吐槽的話會顯得不耐煩。

而說 I got it. 的時候,就是純粹表示,我知道了明白了。

今天我們就來介紹Flutter中的常用庫get_it

一、來由

在Dart和Flutter工程中,為一個元件提供物件/服務的預設方式是通過InheritedWidget。

還有Provider、Singleton、IoC等方式。

1.1 InheritedWidget

如果希望一個部件或其模型能夠訪問服務,則元件必須是繼承的元件的子元件。然後這會導致不必要的巢狀。而且依賴性強,持續性維護差。

///建立資料類 DataWidget,然後在頁面中用資料類DataWidget包含頁面child部分
///1、建立 DataWidget
class DataWidget extends InheritedWidget {
  DataWidget({
    @required this.data,
    Widget child
  }) :super(child: child);
  final int data;
  static DataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<DataWidget>();
  }
  @override
  bool updateShouldNotify(DataWidget old) {
    return old.data != data;
  }
}

///2、建立 InheritedWidgetTestRoute.dart 檔案
class InheritedWidgetTestRoute extends StatefulWidget {
  @override
  _InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return  Center(
      child: DataWidget( // 重點:使用DataWidget包裹
        data: count,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: Text(DataWidget.of(context)
              .data.toString())
            ),
            RaisedButton(
              child: Text("Increment"),
              //每點選一次,將count自增,然後更新頁面渲染,DataWidget的data將被更新  
              onPressed: () => setState(() => ++count),
            )
          ],
        ),
      ),
    );
  }
}
複製程式碼

1.2 Provider

為元件提供物件/服務,還有一種方式是使用Provider,使用比較繁瑣。

///建立資料類 `CountNotifier`,然後在頁面中用資料類 `CountNotifier`包裹child部分。
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 建立 Widget 持有 CountNotifier 資料
    return ChangeNotifierProvider.value(
      value: CountNotifier(),
      child: MaterialApp(
        title: 'Privoder Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: ProvidePage(title: 'Provider 測試頁面'),
      ),
    );
  }
}

class ProvidePage extends StatelessWidget {
  final String title;
  ProvidePage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 獲取 CountNotifier 資料 (最簡單的方式)
    final counter = Provider.of<CountNotifier>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text( '按下按鈕,使數字增加:', ),
            Text(
              '${counter.count}',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          counter.increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

/// 核心:繼承自ChangeNotifier
/// 可以單獨放在一個類檔案裡
class CountNotifier with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  increment() {
    _count++;
    // 核心方法,通知重新整理UI,呼叫build方法
    notifyListeners();
  }
}
複製程式碼

1.3 Singleton、IoC

我們也可以通過其他的方式在App中的任意位置獲取到要訪問的物件,但是:

  • 如果使用Singleton,則無法輕鬆地將實現切換到另一個(例如用於單元測試的模擬版本)

  • 用於依賴項注入的IoC容器提供了類似的功能,但代價是啟動時間慢且可讀性差,因為不知道神奇注入的物件來自何處。 由於大多數IoC庫都依賴反射,因此它們不能與Flutter一起使用。

1.4 GetIt

在App迭代發展中,隨著程式碼工程的增長,在某些時候需要將App的部分邏輯放在與Widget分離的類中。使Widget不具有直接依賴關係可以使程式碼更好地組織並更易於測試和維護。但是現在需要一種從 UI 程式碼訪問這些物件的方法。

作者escamoteur 借鑑.net中的Service Locator Splat概念,在Dart中開發而成。

Service Locators 的概念,它是一種將介面(抽象基類)與具體實現解耦的方法,同時允許通過介面從您的 App 中的任何地方訪問具體實現。

故GetIt應運而生,從1.0到現在的7.x

二、介紹

2.1 定義

[GetIt] 官方介紹

This is a simple Service Locator for Dart and Flutter projects with some additional goodies highly inspired by Splat. It can be used instead of InheritedWidget or Provider to access objects e.g. from your UI.

Typical usage:

  • Accessing service objects like REST API clients or databases so that they easily can be mocked.
  • Accessing View/AppModels/Managers/BLoCs from Flutter Views

V7.0 has some breaking changes Check please check the release notes to see what's new.

譯文:

GetIt是一個用於 Dart 和 Flutter 專案的簡單服務定位器,其中包含一些受到 Splat 啟發的附加功能。 它可以用來代替 InheritedWidget 或 Provider 比如從你的使用者介面來訪問物件。

典型用法(使用場景):

  • 訪問 REST API 客戶端或資料庫等服務物件,以便輕鬆模擬它們
  • 從 Flutter 檢視訪問 View/AppModels/Managers/BLoCs

簡而言之

GetIt是一個簡單的直接服務定位器,允許將介面與具體實現分離,並從應用程式的任何地方訪問具體實現。

一句話總結

GetIt是一個工具箱。

2.2 特點

  • 呼叫極快 (複雜度O(1))
  • 易學/易用
  • 不會像提供程式或 Redux 那樣使用特殊的小部件來使您的 UI 樹混亂以訪問您的資料。

Redux 的設計思想很簡單,就兩句話。

(1)Web 應用是一個狀態機,檢視與狀態是一一對應的。

(2)所有的狀態,儲存在一個物件裡面。

三、使用

3.1 引入

在某包下的pubspec.yaml引入框架

dependencies:
  flutter:
    sdk: flutter
  domain_common:
    path: ../domain_common
  res_common:
    path: ../res_common
  provider: ^5.0.0
  get_it: ^7.1.3
複製程式碼

3.2 獲取

GetIt以單例模式使用

GetIt getIt = GetIt.instance;
//or
GetIt getIt = GetIt.I;

//重新自定義一個新的
GetIt getIt = GetIt.asNewInstance();
複製程式碼

3.3 註冊

GetIt locator = GetIt.instance;

//在GetIt裡註冊工廠類Mutou
locator.registerFactory<Mutou>(() => MutouImpl());

//在GetIt裡註冊單例
locator.registerSingleton<FileSystem>(() => LocalFileSystem());
locator.registerLazySingleton<ClientRepository>(() => ClientRepositoryImpl());
複製程式碼

3.4 呼叫

GetIt locator = GetIt.instance;
//呼叫工廠物件
var mutouModel = locator.get<Mutou>();

//呼叫單例
var fileSystemSingleton = locator<FileSystem>();
複製程式碼

3.5 註冊方式

有三種常用的註冊方式

GetIt locator = GetIt.instance;

Future<void> setupLocator({bool test = false}) async {
	// 一,單例模式,使用 registerSingleton 
	// 在main.dart中呼叫setupLocator()時,就會立即註冊FileSystem
	// 註冊單例,保證每一次呼叫FileSystem工具時,都是同一個
	locator.registerSingleton<FileSystem>(FileSystem());
  //有擴充套件函式可以傳參可以非同步
	//locator.registerFactoryParam
  //locator.registerFactoryAsync
  //locator.registerFactoryParamAsync
  //還可以擴充套件依賴
  //locator.registerSingletonWithDependencies

	// 二,懶漢模式單例,使用 registerLazySingleton 
	// 呼叫setupLocator()時,沒有生成,直到在其他地方第一次呼叫ClientRepository工具時,才會註冊
	// registerSingleton註冊單例,保證每一次呼叫ClientRepository工具時,都是同一個
	locator.registerLazySingleton<ClientRepository>(ClientRepositoryImpl());
  //也支援非同步模式
  //locator.registerLazySingletonAsync
	
	// 三, 工廠模式, 使用 registerFactory / registerFactoryParam / registerFactoryParamAsync
	// 呼叫setupLocator()時,沒有生成,直到在其他地方第一次呼叫Mutou工具時,才會註冊
	// 引數必須時工廠函式,工廠函式不需要引數用registerFactory
	// 引數必須時工廠函式,工廠函式需要引數用registerFactoryParam
	// 引數必須時工廠函式,工廠函式是非同步且需要引數用registerFactoryParamAsync
	locator.registerFactory<Mutou>( () => MutouImpl()) );
	
	locator.registerFactoryParam<MutouPlus,String,int>((str,num) 
  	=> MutouPlusImpl(param1:str, param2: num));
  	
  	locator.registerFactoryParamAsync<MutouPro,String,int>((str,num) 
  	=> MutouProImpl(param1:str, param2: num));

}

複製程式碼

3.6 其他

//判斷某個物件是否已註冊
  bool isRegistered<T extends Object>({Object? instance, String? instanceName});

//重置所有註冊的物件
  Future<void> reset({bool dispose = true});

//畫個圈V5.0後才有
//是否範圍內重置
Future<void> resetScope({bool dispose = true});


  //之前的所有註冊作廢,在一個新的範圍內新建
  void pushNewScope({
    void Function(GetIt getIt)? init,
    String? scopeName,
    ScopeDisposeFunc? dispose,
  });


 //在某個範圍內退出,在這個範圍內註冊的所有物件作廢
  Future<void> popScope();

 //若有多個範圍,則制定某個範圍內退出,相應範圍註冊的所有物件作廢
  Future<bool> popScopesTill(String name);

  //獲取當前範圍名字
  String? get currentScopeName;


//在特定時間timeout內完成所有非同步物件建立,ignorePendingAsyncCreation是是否忽略非同步物件的建立
Future<void> allReady({
    Duration? timeout,
    bool ignorePendingAsyncCreation = false,
  });

//在特定timeout時長下,是否完成非同步的物件建立
 Future<void> isReady<T extends Object>({
    Object? instance,
    String? instanceName,
    Duration? timeout,
    Object? callee,
  });

//不要被這個名字誤解了,這個還是檢查非同步物件是否被建立,因為同步的不需要檢查
 bool isReadySync<T extends Object>({
    Object? instance,
    String? instanceName,
  });

//檢查是否完成所有非同步物件建立,ignorePendingAsyncCreation是是否忽略非同步物件的建立
  bool allReadySync([bool ignorePendingAsyncCreation = false]);

// 註冊為非同步物件的時候,在init函式裡手動調動
/// If you want to use this mechanism you have to pass [signalsReady==true] when registering
/// the Singleton.
/// Typically this is used in this way inside the registered objects init
/// method `GetIt.instance.signalReady(this);`
void signalReady(Object? instance);
複製程式碼

四、原始碼

本文以 get_it: ^7.1.3為樣本分析

4.1 本身

本身是一個服務工廠或工具箱類

class _ServiceFactory<T extends Object, P1, P2> {
  final _ServiceFactoryType factoryType;

  //裡面的單例
  final _GetItImplementation _getItInstance;

  late final Type param1Type;
  late final Type param2Type;

  //儲存各種物件
  /// Because of the different creation methods we need alternative factory functions
  /// only one of them is always set.
  final FactoryFunc<T>? creationFunction;
  final FactoryFuncAsync<T>? asyncCreationFunction;
  final FactoryFuncParam<T, P1?, P2?>? creationFunctionParam;
  final FactoryFuncParamAsync<T, P1?, P2?>? asyncCreationFunctionParam;

  ///  Dispose function that is used when a scope is popped
  final DisposingFunc<T>? disposeFunction;

  /// In case of a named registration the instance name is here stored for easy access
  final String? instanceName;

  /// true if one of the async registration functions have been used
  final bool isAsync;

  /// If a an existing Object gets registered or an async/lazy Singleton has finished
  /// its creation it is stored here
  Object? instance;

  /// the type that was used when registering. used for runtime checks
  late final Type registrationType;

  /// to enable Singletons to signal that they are ready (their initialization is finished)
  late Completer _readyCompleter;

  /// the returned future of pending async factory calls or factory call with dependencies
  Future<T>? pendingResult;

  /// If other objects are waiting for this one
  /// they are stored here
  final List<Type> objectsWaiting = [];

  bool get isReady => _readyCompleter.isCompleted;

  bool get isNamedRegistration => instanceName != null;

  String get debugName => '$instanceName : $registrationType';

  bool get canBeWaitedFor =>
      shouldSignalReady || pendingResult != null || isAsync;

  final bool shouldSignalReady;

  _ServiceFactory(
    this._getItInstance,
    this.factoryType, {
    this.creationFunction,
    this.asyncCreationFunction,
    this.creationFunctionParam,
    this.asyncCreationFunctionParam,
    this.instance,
    this.isAsync = false,
    this.instanceName,
    required this.shouldSignalReady,
    this.disposeFunction,
  }) : assert(
            !(disposeFunction != null &&
                instance != null &&
                instance is Disposable),
            ' You are trying to register type ${instance.runtimeType.toString()} '
            'that implements "Disposable" but you also provide a disposing function') {
    registrationType = T;
    param1Type = P1;
    param2Type = P2;
    _readyCompleter = Completer();
  }
複製程式碼

4.2 註冊

以常用的registerLazySingleton為例看下注冊的原始碼

 @override
  void registerLazySingleton<T>(FactoryFunc<T> func, {String instanceName}) {
    _register<T, void, void>(
        type: _ServiceFactoryType.lazy,
        instanceName: instanceName,
        factoryFunc: func,
        isAsync: false,
        shouldSignalReady: false);
  }

  void _register<T, P1, P2>({
    @required _ServiceFactoryType type,
    FactoryFunc<T> factoryFunc,
    FactoryFuncParam<T, P1, P2> factoryFuncParam,
    FactoryFuncAsync<T> factoryFuncAsync,
    FactoryFuncParamAsync<T, P1, P2> factoryFuncParamAsync,
    T instance,
    @required String instanceName,
    @required bool isAsync,
    Iterable<Type> dependsOn,
    @required bool shouldSignalReady,
  }) {
    //......引數判斷已忽略
    final serviceFactory = _ServiceFactory<T, P1, P2>(
      type,
      creationFunction: factoryFunc,
      creationFunctionParam: factoryFuncParam,
      asyncCreationFunctionParam: factoryFuncParamAsync,
      asyncCreationFunction: factoryFuncAsync,
      instance: instance,
      isAsync: isAsync,
      instanceName: instanceName,
      shouldSignalReady: shouldSignalReady,
    );

    if (instanceName == null) {
      _factories[T] = serviceFactory;
    } else {
      _factoriesByName[instanceName] = serviceFactory;
    }

    // 普通單例立即被建立註冊
    if (type == _ServiceFactoryType.constant &&
        !shouldSignalReady &&
        !isAsync &&
        (dependsOn?.isEmpty ?? false)) {
      serviceFactory.instance = factoryFunc();
      serviceFactory._readyCompleter.complete();
      return;
    }

    //若是非同步或有其他依賴函式建立後的單例,要檢查它若依賴於其他單例被建立後再建立
    if ((isAsync || (dependsOn?.isNotEmpty ?? false)) &&
        type == _ServiceFactoryType.constant) {
      /// Any client awaiting the completion of this Singleton
      /// Has to wait for the completion of the Singleton itself as well
      /// as for the completion of all the Singletons this one depends on
      /// For this we use [outerFutureGroup]
      /// A `FutureGroup` completes only if it's closed and all contained
      /// Futures have completed
      final outerFutureGroup = FutureGroup();
      Future dependentFuture;

      if (dependsOn?.isNotEmpty ?? false) {
        //有非同步依賴 就 等待單例外部依賴的函式完成稿後 
        final dependentFutureGroup = FutureGroup();

        for (final type in dependsOn) {
          final dependentFactory = _factories[type];
          throwIf(dependentFactory == null,
              ArgumentError('Dependent Type $type is not registered in GetIt'));
          throwIfNot(dependentFactory.canBeWaitedFor,
              ArgumentError('Dependent Type $type is not an async Singleton'));
          dependentFactory.objectsWaiting.add(serviceFactory.registrationType);
          dependentFutureGroup.add(dependentFactory._readyCompleter.future);
        }
        dependentFutureGroup.close();//依賴的函式完成了
        
        dependentFuture = dependentFutureGroup.future;
      } else {//無依賴直接執行
        dependentFuture = Future.sync(() {}); 
      }
      outerFutureGroup.add(dependentFuture);

      /// if someone uses getAsync on an async Singleton that has not be started to get created
      /// because its dependent on other objects this doesn't work because [pendingResult] is not set in
      /// that case. Therefore we have to set [outerFutureGroup] as [pendingResult]
      dependentFuture.then((_) {
        Future<T> isReadyFuture;
        if (!isAsync) {//非非同步,單例有依賴
          serviceFactory.instance = factoryFunc();
          isReadyFuture = Future<T>.value(serviceFactory.instance as T);
          if (!serviceFactory.shouldSignalReady) {//單例未建立完成
						//標記為完成
            serviceFactory._readyCompleter.complete();
          }
        } else {
          //非同步單例
          final asyncResult = factoryFuncAsync();

          isReadyFuture = asyncResult.then((instance) {
            serviceFactory.instance = instance;

            if (!serviceFactory.shouldSignalReady && !serviceFactory.isReady) {
              serviceFactory._readyCompleter.complete();
            }
            return instance;
          });
        }
        outerFutureGroup.add(isReadyFuture);
        outerFutureGroup.close();
      });

      //等待返回的結果是outerFutureGroup列表中的最近一個
      serviceFactory.pendingResult =
          outerFutureGroup.future.then((completedFutures) {
        return completedFutures.last as T;
      });
    }
  }


複製程式碼

4.3 呼叫

以呼叫get函式的原始碼分析

T get<T extends Object>({
    String? instanceName,
    dynamic param1,
    dynamic param2,
  }) {
    //一、獲取陣列  _findFactoryByNameOrType
    final instanceFactory = _findFactoryByNameAndType<T>(instanceName);

    Object instance = Object; //late
    if (instanceFactory.isAsync || instanceFactory.pendingResult != null) {
      /// We use an assert here instead of an `if..throw` for performance reasons
      assert(
        instanceFactory.factoryType == _ServiceFactoryType.constant ||
            instanceFactory.factoryType == _ServiceFactoryType.lazy,
        "You can't use get with an async Factory of ${instanceName ?? T.toString()}.",
      );
      assert(
        instanceFactory.isReady,
        'You tried to access an instance of ${instanceName ?? T.toString()} that is not ready yet',
      );
      instance = instanceFactory.instance!;
    } else {
      //二、陣列中獲取對應物件
      instance = instanceFactory.getObject(param1, param2);
    }

    assert(
      instance is T,
      'Object with name $instanceName has a different type '
      '(${instanceFactory.registrationType.toString()}) than the one that is inferred '
      '(${T.toString()}) where you call it',
    );

    return instance as T;
  }

//一、獲取陣列  _findFactoryByNameOrType
/// Is used by several other functions to retrieve the correct [_ServiceFactory]
  _ServiceFactory _findFactoryByNameAndType<T extends Object>(
    String? instanceName, [
    Type? type,
  ]) {
    final instanceFactory =
        _findFirstFactoryByNameAndTypeOrNull<T>(instanceName, type: type);
    assert(
      instanceFactory != null,
      // ignore: missing_whitespace_between_adjacent_strings
      'Object/factory with ${instanceName != null ? 'with name $instanceName and ' : ' '}'
      'type ${T.toString()} is not registered inside GetIt. '
      '\n(Did you accidentally do GetIt sl=GetIt.instance(); instead of GetIt sl=GetIt.instance;'
      '\nDid you forget to register it?)',
    );
    return instanceFactory!;
  }

  /// Is used by several other functions to retrieve the correct [_ServiceFactory]
_ServiceFactory<T, dynamic, dynamic>?
      _findFirstFactoryByNameAndTypeOrNull<T extends Object>(
          String? instanceName,
          {Type? type,
          bool lookInScopeBelow = false}) {
    /// We use an assert here instead of an `if..throw` because it gets called on every call
    /// of [get]
    /// `(const Object() is! T)` tests if [T] is a real type and not Object or dynamic
    assert(
      type != null || const Object() is! T,
      'GetIt: The compiler could not infer the type. You have to provide a type '
      'and optionally a name. Did you accidentally do `var sl=GetIt.instance();` '
      'instead of var sl=GetIt.instance;',
    );

    _ServiceFactory<T, dynamic, dynamic>? instanceFactory;

    int scopeLevel = _scopes.length - (lookInScopeBelow ? 2 : 1);

  //快速查詢
    while (instanceFactory == null && scopeLevel >= 0) {
      // 注意:factoriesByName = <String?, Map<Type, _ServiceFactory<Object, dynamic, dynamic>>>{};
      // factoriesByName這是個Map,所以複雜度是O(1))
      final factoryByTypes = _scopes[scopeLevel].factoriesByName[instanceName];
      if (type == null) {
        instanceFactory = factoryByTypes != null
            ? factoryByTypes[T] as _ServiceFactory<T, dynamic, dynamic>?
            : null;
      } else {
        /// in most cases we can rely on the generic type T because it is passed
        /// in by callers. In case of dependent types this does not work as these types
        /// are dynamic
        instanceFactory = factoryByTypes != null
            ? factoryByTypes[type] as _ServiceFactory<T, dynamic, dynamic>?
            : null;
      }
      scopeLevel--;
    }

    return instanceFactory;
  }

  //二、陣列中獲取對應物件
  /// returns an instance depending on the type of the registration if [async==false]
  T getObject(dynamic param1, dynamic param2) {
    assert(
        !(factoryType != _ServiceFactoryType.alwaysNew &&
            (param1 != null || param2 != null)),
        'You can only pass parameters to factories!');

    try {
      switch (factoryType) {
          //始終建立最新的模式
        case _ServiceFactoryType.alwaysNew:
          if (creationFunctionParam != null) {
 
            return creationFunctionParam(param1 as P1, param2 as P2);
          } else {
            return creationFunction();
          }
          break;
          //常量模式
        case _ServiceFactoryType.constant:
          return instance as T;
          break;
          //懶漢模式
        case _ServiceFactoryType.lazy:
          if (instance == null) {
            instance = creationFunction();
            _readyCompleter.complete();
          }
          return instance as T;
          break;
        default:
          throw StateError('Impossible factoryType');
      }
    } catch (e, s) {
      print('Error while creating ${T.toString()}');
      print('Stack trace:\n $s');
      rethrow;
    }
  }


複製程式碼

總結

GetIt作為一個常用的動態服務定位器,一個工具箱,擺脫了InheritedWidget、Provider、Singleton、IoC等苛刻繁瑣的流程,簡單易用,脫穎而出。

作為Flutter開發者,經常流於表面的使用,當靜下心來分析其原始碼,自有一套完整健全的邏輯。

作為使用者,管中窺豹,時見一斑。

作為剖析者,細品,別有一番風味。

相關文章