Skip to content

Instantly share code, notes, and snippets.

@escamoteur
Last active January 12, 2020 14:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save escamoteur/c4c717518729a4c24a4ce95c10111473 to your computer and use it in GitHub Desktop.
Save escamoteur/c4c717518729a4c24a4ce95c10111473 to your computer and use it in GitHub Desktop.
GetIt supports since recently a mechanic to make initialization of registered
Instances easier.
The idea is that you can mark Singletons/LazySingletons that they will signal
when they are ready.
You then can wait for either that all are ready or that one of them is ready or
trigger the readyFuture manually
In case of a timeout you get detailed debug information.
/// Returns a Future that is completed once all registered Singletons have signaled that they are ready
/// Or when the global [signalReady] is called without an instance
/// [timeOut] if this is set and the future wasn't completed within that time period an
Future<void> allReady({Duration timeOut});
/// Returns a Future that is completed when a given registered Singletons has signaled that it is ready
/// [T] Type of the Singleton to be waited for
/// [instance] registered instance to be waited for
/// [instanceName] Singleton to be waited for that was registered by name instead of a type.
/// You should only use one of the
/// [timeOut] if this is set and the future wasn't completed within that time period an
/// [callee] optional parameter which makes debugging easier. Pass `this` in here.
Future<void> isReady<T>(
{Object instance, String instanceName, Duration timeOut, Object callee});
/// if [instance] is `null` and no singleton is waiting to be signaled this will complete the future you got
/// from [allReady]
///
/// If [instance] has a value GetIt will search for the responsible singleton and complete all futures you might
/// have received by calling [isReady]
/// Typically this is use in this way inside the registered objects init method `GetIt.instance.signalReady(this);`
/// If all waiting singletons/factories have signaled ready the future you can get from [allReady] is automatically completed
///
/// Both ways are mutual exclusive meaning either only use the global `signalReady()` and don't register a singlton/fatory as signaling ready
/// Or let indiviual instance signal their ready state on their own.
void signalReady([Object instance]);
/// In case of an timeout while waiting for an instance to signal ready
/// This exception is thrown whith information about who is still waiting
‎‎​class WaitingTimeOutException implements Exception {
/// if you pass the [callee] parameter to [isReady]
/// this maps lists which callees is waiting for whom
final Map<Type, Type> isWaitingFor;
/// Lists with Types that are still not ready
final List<Type> notSignaledYet;
/// Lists with Types that are already ready
final List<Type> hasSignaled;
WaitingTimeOutException(
this.isWaitingFor, this.notSignaledYet, this.hasSignaled)
: assert(isWaitingFor != null &&
notSignaledYet != null &&
hasSignaled != null);
@override
String toString() {
print(
'GetIt: There was a timeout while waiting for an instance to signal ready');
print('The following instance types where waiting for completion');
for (var entry in isWaitingFor.entries) {
print('${entry.key} is waiting for ${entry.value}');
}
print('The following instance types have NOT signaled ready yet');
for (var entry in notSignaledYet) {
print('$entry');
}
print('The following instance types HAVE signaled ready yet');
for (var entry in hasSignaled) {
print('$entry');
}
return super.toString();
}
}
@rhalff
Copy link

rhalff commented Dec 4, 2019

Another approach could be to register a factory for the singleton instead which would return a Future of the instance:

typedef Future<T> SingletonFactory<T>();
registerSingletonFactory<T>(SingletonFactory<T> factory, {String instanceName, Duration timeout}) {}

Not sure if the signature is totally correct, but something like that.

This way getIt can instantiate the list of Futures and figure out the timeout for each singleton.

isReady could then just return a boolean value indicating whether the singleton factory has been resolved.

@escamoteur
Copy link
Author

Not sure if I GetIt completely.
The idea is that yi can use the allReady() inside a future builder that's why I return a Future.

In the same way registered instances can wait for other instances to be ready before they continue

@rhalff
Copy link

rhalff commented Dec 4, 2019

Sorry, I didn't realize signalsReady is already part of the released api, the suggestion was to have FactoryFunc return a Future which could remove the need for signaling as the completion of the returned Future would then indicate the registered instance is ready.

@escamoteur
Copy link
Author

Ah, understood, the good thing of having signalReady is it can be called at any time within a function

@Kavantix
Copy link

Kavantix commented Dec 5, 2019

Will the timeout always be something that should not happen?
Because otherwise you might want to consider renaming it to Error instead of exception.

@escamoteur
Copy link
Author

No, the timeout will only occur if you set one. Also I can image szenarios where you want to handle such a problem and give it another try.
I think it's a border line case.
Besides that ok and helpful?

@Kavantix
Copy link

Kavantix commented Dec 5, 2019

Yes it seems like a useful feature.
The one thing I am wondering about right now is if it makes sense for the signalsReady to be in the register.
Because that way there is a disconnect between the singleton class implementation that has to call signalReady and the signalsReady being set outside that class.

So might it be an idea to have the singleton implement some interface (abstract class) that tells getit that it will signalReady when it’s ready?

@escamoteur
Copy link
Author

Not sure if I understood this correctly. Currently you tell GetIt in the register functions if this Singleton will signal ready.

Then typically `signaReady(this) would be called from the Singleton.

I don't know how this could be done automatically as I don't know when a singleton is completely initialized.
But Perhaps I miss something here.

@Kavantix
Copy link

Kavantix commented Dec 5, 2019

I mean only the register part.
The singleton still will have to call signalReady(this)
But by implementing some interface GetIt can do a type check and use that instead of the signalsReady boolean in the register.
That way the boolean will be handled completely by the singleton and no longer depends on the register calls.

Not sure though if there would be a case where you explicitly do not want this behaviour

@escamoteur
Copy link
Author

Hmm, could you descibe it with some pseudo code? I think I haven't got the idea fully.

@Kavantix
Copy link

Kavantix commented Dec 5, 2019

Register singleton will be back to:

 void registerSingleton<T>(T instance,
      {String instanceName});

And then a singleton that wants to use the feature:

class SomeSingleton implements GetItSingleton {}

And implementation of registerSingleton:

void registerSingleton<T>(T instance) {
  var signalsReady = instance is GetItSingleton;
  ...
}

Or optionally the interface could define a getter signalsReady so then:

void registerSingleton<T>(T instance) {
  var signalsReady = instance is GetItSingleton && (instance as GetItSingleton).signalsReady;
  ...
}

class SomeSingleton implements GetItSingleton {

@override
bool get signalsReady => true; // or false if it doesnt
}

@escamoteur
Copy link
Author

Thanks!
Hmm, what would be the advantage to passing a bool value?

@Kavantix
Copy link

Kavantix commented Dec 6, 2019

That the class decides if the bool is true or not.
So if the class changes you don't have to also change the register since it will stay the same

@Lootwig
Copy link

Lootwig commented Dec 6, 2019

@Kavantix

void registerSingleton<T extends GetItSingleton>(T instance, {String instanceName});

(note the added "extends" restriction)?

@escamoteur
Copy link
Author

escamoteur commented Jan 12, 2020

I implemented a different mechanism which almost automatically signals that an instance is ready by passing async builder functions to new async registration functions.
See: https://pub.dev/packages/get_it/versions/4.0.0-beta3

But I'm really unsure because when integrating this system in my current app the result looks not as easy to read and is more cumbersome. Compare

old:

  backend.registerSingleton<ErrorReporter>(reporter);

  backend.registerSingleton<SystemService>(
    SystemServiceImplementation(),
  );

  backend.registerSingleton<ConfigService>(
    ConfigServiceImplementation(),
    signalsReady: true,
  );
  backend.registerSingleton<RTSAService>(
    RTSAServiceImplementation(),
    signalsReady: true,
  );
  backend.registerSingleton<MapTileService>(
    MapTileServiceImplementation(),
    signalsReady: false, // check if this is necessary outside of rtsa_map
  );

  backend.registerLazySingleton<InteractionService>(
    () => InteractionServiceImplementation(signalsReady: false),
  );

  backend.registerLazySingleton<GeoLocationService>(
      () => GeoLocationServiceImplementation());

  backend.registerSingleton<DetectionManager>(
    DetectionManager(),
    signalsReady: false,
  );
  backend.registerSingleton<MapManager>(
    MapManager(),
    signalsReady: true,
  );
 ///and later:

  backend<ConfigService>().loadAppConfig().then((config) {
    backend<ErrorReporter>().setServerConfig(config.serverAdress,config.port);
    backend<RTSAService>().init(config.serverAdress, config.port);
    backend<MapTileService>().init(config.serverAdress, config.port);

    backend<GeoLocationService>().init(
        useFixedUserPosition: config.useFixedUserPosition ?? false,
        fixedUserPosition:
            LatLng(config.fixedUserLatitude ?? 0, config.fixedUserLongitude ?? 0));
    backend<DetectionManager>().init(appConfig: config);

    backend<MapManager>().init(
      useCustomMap: config.useCustomMap,
      customMapName: config.customMapName,
    );
  });

to the new Version without separate Initialization:

  backend.registerSingletonAsync<ConfigService>(
      (_) => ConfigServiceImplementation()..loadAppConfig());

  backend.registerSingletonAsync<ErrorReporter>((_) async {
    var config = backend<ConfigService>().currentConfig;
    return reporter
      ..setServerConfig(
        config.serverAdress,
        config.port,
      );
  }, dependsOn: [ConfigService]);

  backend.registerSingletonAsync<RTSAService>(
      (_) async => RTSAServiceImplementation(),
      dependsOn: [ConfigService]);

  backend.registerSingletonAsync<MapTileService>((_) async {
    var config = backend<ConfigService>().currentConfig;
    return MapTileServiceImplementation()
      ..init(
        config.serverAdress,
        config.port,
      );
  }, dependsOn: [ConfigService]);

  backend.registerSingletonAsync<InteractionService>(
    (completer) => InteractionServiceImplementation(completer),
  );

  backend.registerSingletonAsync<GeoLocationService>((_) async {
    var config = backend<ConfigService>().currentConfig;
    return GeoLocationServiceImplementation()
      ..init(
          useFixedUserPosition: config.useFixedUserPosition ?? false,
          fixedUserPosition: LatLng(
              config.fixedUserLatitude ?? 0, config.fixedUserLongitude ?? 0));
  }, dependsOn: [ConfigService]);

  backend.registerSingletonAsync<DetectionManager>(
      (_) async => DetectionManager(),
      dependsOn: [ConfigService]);
  backend.registerSingletonAsync<MapManager>((_) async => MapManager(),
      dependsOn: [ConfigService]);

So maybe back to the drawing board and pick the best of both worlds.

@Kavantix @Lootwig
The idea of having to implement a certain interface is interesting but I'm not sure if we limit the way a registered instance is inherited. Also haveing signalsReady at the same place as the registration makes it easier to see which of them might act asynchronously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment