rodydavis / signals.dart

Reactive programming made simple for Dart and Flutter
http://dartsignals.dev
Apache License 2.0
378 stars 44 forks source link

Should we use Provider or GetIt to inject dependency of signal service class? #213

Closed milansurelia closed 3 months ago

milansurelia commented 3 months ago

Description This issue aims to discuss the best approach for dependency injection of the signal service class within the signals library. Currently, there's no clear guidance on whether to use Provider or GetIt.

Context:

We want to decouple the signal service class from dependent components. We need a mechanism to inject the signal service dependency. Possible Solutions:

Provider:

Pros: Widely adopted in the Flutter community. Offers good integration with the widget lifecycle. Provides features like change notifications.

Cons: Might introduce additional complexity for non-widget contexts.

GetIt:

Pros: Simple and lightweight service locator pattern. Easier to access dependencies from anywhere in the app (including non-widget contexts). Supports mocking for easier testing.

Cons: Less familiar to developers coming from other frameworks. Might lead to tighter coupling if not used carefully.

Discussion Points:

Which approach aligns better with the overall design philosophy of the signals library? Are there any specific use cases that favor one option over the other? Should the library provide guidance or examples for both approaches?

Additional Information:

I hope this discussion helps us determine the best way to handle dependency injection for the signal service class. Let's share our thoughts and experiences!

jinyus commented 3 months ago

I would suggest Provider, lite_ref(my package) or something that hooks into the lifecycle of widgets because signals are disposable and GetIt wouldn't know when to dispose them. Therefore, using GetIt for disposables doesn't make much sense unless it's for long-lived signals (eg. Apptheme).

Provider/lite_ref also allow easy mocking.

In the example below, the Controller will be disposed when CounterText is unmounted. You can't achieve this with GetIt.

class Controller {
  late final _count = signal(0);

  // we expose it as a readonly
  // so it cannot be changed from outside the controller.
  ReadonlySignal<int> get count => _count;

  void increment() => _count.value++;
  void decrement() => _count.value--;

  void dispose() {
    _count.dispose()
  }
}

final countControllerRef = Ref.scoped((ctx) => Controller());

class CounterText extends StatelessWidget {
  const CounterText({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = countControllerRef.of(context);
    final count = controller.count.watch(context);
    return Text('$count');
  }
}

In my signals package (state_beacon). I created a BeaconController that disposes all beacons created in it; as well as a select extension method that makes watching beacons more ergonomic. Rody could do the same for signals. This is how the code would look:

class Controller extends BeaconController {
  late final _count = B.writable(0);

  // we expose it as a readable beacon
  // so it cannot be changed from outside the controller.
  ReadableBeacon<int> get count => _count;

  void increment() => _count.value++;
  void decrement() => _count.value--;
}

final countControllerRef = Ref.scoped((ctx) => Controller());

class CounterText extends StatelessWidget {
  const CounterText({super.key});

  @override
  Widget build(BuildContext context) {
    final count = countControllerRef.select(context, (c) => c.count);
    return Text('$count');
  }
}