Skip to content

Enhancements for tool building #386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
JCKodel opened this issue Oct 24, 2024 · 8 comments
Closed

Enhancements for tool building #386

JCKodel opened this issue Oct 24, 2024 · 8 comments

Comments

@JCKodel
Copy link

JCKodel commented Oct 24, 2024

I was trying to create a helper widget that would:

  1. Push a scope with dependencies that will be used from now on
  2. Automatically initialize dependencies that require async (or not) initialization

So I wrote this:

Widget build(BuildContext context) {
  return DependencyContext(
    singletons: [
      SomeClassThatImplementsIInitializable(),
    ],
    transients: [
      () => OtherClassThatImplementsIInitializable(),
    ],
  );
}

IInitializable contains a FutureOr<void> initialize();.

But this is impossible for some reasons:

  1. There is no way to register instances or factories without specifying the type.
  2. There is no way of getting all instances of singleton registrations, so I can check if they implements IInitializable and run await instance.initialize().
  3. There is no way of hooking transient (factory) instantations.

It would be nice if:

  1. We could register classes without specifying the type (that could be either by getting the runtype type implicitly (such as GetIt.I.registerSingleton(SomeSingleton()) will register a <SomeSingleton>). Or if we could specify the type as a parameter, such as GetIt.registerSingletonAs(singleton: SomeSingleton(), type: SomeSingleton).
  2. Get.I.getAll() will build all transient instances but, in this case, I just want to initialize the singletons. It would be nice to have more control of what is registered (types, instances, if they are singletons or transient, etc.)
  3. It would be nice to register a hook whenever some factory is built: GetIt.I.registerFactory<ISomeInterface>(() => SomeClass(), onCreated: (instance) => instance.initializeAsync()).

I know we can do all those things using the registration methods, async singletons, etc. but this scenario is for a higher-level widget that would simplify the creation of a new context (that widget will push a new scope, register what needs to be registered, initialize what needs to be initialized (and waiting for them using a FutureBuilder, if any is required), and disposing when the widget is no longer in scope.

@escamoteur
Copy link
Collaborator

escamoteur commented Oct 24, 2024 via email

@escamoteur
Copy link
Collaborator

escamoteur commented Oct 24, 2024 via email

@JCKodel
Copy link
Author

JCKodel commented Oct 24, 2024

Have you checked watch_it the companion package to get_it for state management which contains a pushScope method for exactly this?

Yes. I am using it. Can't live without it!

I think what best describes what I'm trying to accomplish is something like Provider, but using GetIt scopes instead.

So, in my runApp(), I would have something like this:

runApp(
  DependencyContext(
    transients: [
      () => DataRepository() as IDataRepository,
      () => Logger() as ILogger,
    ],
    singletons: [
      AuthViewModel(),
    ],
    busyBuilder: (context) => CircularProgressIndicator.adaptive(),
    child: const MainApp(),
  ),
);

That DependencyContext would have to:

  1. Create a new GetIt scope, registering those factories and singletons (for(final factory in transients) GetIt.I.registerFactory(factory); won't work. It needs a type).
  2. Check if those singletons are IInitializable and, if they are, add all initializables in a Future.wait<void>([]) and provide it for a FutureBuilder (if no initializable, just returns the child).
  3. Dispose the scope as soon as DependencyContext is disposed (it is a Stateful widget).

The way GetIt is now, this is impossible. What I ended up with is:

runApp(
  Context(
    name: "Authentication",
    registration: (getIt) => getIt
      ..registerSingletonAsync(
        () => Repository.singleton("https://my.server"),
      )
      ..registerSingletonAsync(
        () => AuthViewModel.singleton(
          defaultThemeSettings: ThemeSettings(
            seedColorValue: Colors.pink.value,
            themeVariant: ThemeVariant.tonalSpot,
          ),
        ),
      ),
    child: const MainApp(),
  ),
);

Those .singleton are async initializers, such as:

final class Repository {
  ...

  static Future<Repository> singleton(String endpoint) async {
    final instance = Repository._(endpoint);
  
    await instance.initialize();

    return instance;
  }

Works as intended, but it's a bit verbose (although it gives me a lot of power in how things are registered).

What I intend is to write as little as possible and do what needs to be done using the decorator pattern (basically, indicating a class is initializable by implementing an interface). Dispose is already handled by GetIt, but initialization, only by signal ready or async singleton registration. I could use signal ready to accomplish that, but I'm still unable to register dependencies that are in a List.

Why would you want to register a factory without type? How would you call it later then?

Hmmm. I think Dart generics aren't powerful enough to let you know what type a methods returns =\ This could be solved in my case by one of these (not pretty):

final transients = [
  (() => Repository.singleton("https://xxx")) as IRepository Function()
];

final transients = {
  IRepository: () => Repository.singleton("https://xxx"),
}

What do you mean with get All should not create the instances

What I want is: list all singletons registered and, for each one of them, if they implements IInitializable, call the IInitializable.initialize().

getAll, would trigger all transient instances, according to the documentation, which is not good for this scenario. Maybe a getAllSingletons() that returns all singletons registered as an extra method? That would not have any side effect because (I think) they are all instantiated (except the lazy ones, right?))

GetIt is a superb piece of software that already do all of these. I'm just trying to make it more declarative with as little code as possible, with as many automations I can do (so I won't accidentally forget any dispose, etc.)

@escamoteur
Copy link
Collaborator

escamoteur commented Oct 24, 2024 via email

@JCKodel
Copy link
Author

JCKodel commented Oct 24, 2024

Thank you.

I'm from Brazil [<O>] (a.k.a. Life in Hard Mode)

@escamoteur
Copy link
Collaborator

escamoteur commented Oct 24, 2024 via email

@JCKodel
Copy link
Author

JCKodel commented Oct 24, 2024 via email

@escamoteur
Copy link
Collaborator

ok, I now found time to look more closely on it. On one side I like the idea that Singletons that implement IInitialize would automatically get initialized like doing a regiserAsync. On the other side I'm less and less using implicit behaviour that is just based on implementing a certain interface as it is not easy to follow when you read the someone else's code, which is why I don't use the Disposable interface in our project. In general it's a pity that the Dart team did not define certain default interfaces like IDisposable.
I typically don't use static methods for async registrations but a normal method:

class AsyncSingleon{
  Future<AsyncSingleton> init(){
       ... do was it needed
      return this;
}

I'm currently not convinced that it is worth investing a lot of time in this as it merely reduced some lines of code but doesn't add new functionality. I leave it open, maybe we get some more input from other users

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants