Skip to content
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

feat: add MultiReactionBuilder widget #917

Merged
merged 5 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions flutter_mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.1.0

- feat: add `MultiReactionBuilder` widget by [@amondnet](https://github.com/amondnet)

## 2.0.6+3 - 2.0.6+5

- Moved the version into its own file (`version.dart`) and exported from the main library file
Expand Down
57 changes: 57 additions & 0 deletions flutter_mobx/lib/src/multi_reaction_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_mobx/src/reaction_builder.dart';
import 'package:provider/provider.dart';

/// {@template multi_reaction_builder}
/// Merges multiple [ReactionBuilder] widgets into one widget tree.
///
/// [MultiReactionBuilder] improves the readability and eliminates the need
/// to nest multiple [ReactionBuilder]s.
///
/// By using [MultiReactionBuilder] we can go from:
///
/// ```dart
/// ReactionBuilder(
/// builder: (context) {},
/// child: ReactionBuilder(
/// builder: (context) {},
/// child: ReactionBuilder(
/// builder: (context) {},
/// child: ChildA(),
/// ),
/// ),
/// )
/// ```
///
/// to:
///
/// ```dart
/// MultiReactionBuilder(
/// builders: [
/// ReactionBuilder(
/// builder: (context) {},
/// ),
/// ReactionBuilder(
/// builder: (context) {},
/// ),
/// ReactionBuilder(
/// builder: (context) {},
/// ),
/// ],
/// child: ChildA(),
/// )
/// ```
///
/// [MultiReactionBuilder] converts the [ReactionBuilder] list into a tree of nested
/// [ReactionBuilder] widgets.
/// As a result, the only advantage of using [MultiReactionBuilder] is improved
/// readability due to the reduction in nesting and boilerplate.
/// {@endtemplate}
class MultiReactionBuilder extends MultiProvider {
/// {@macro multi_reaction_builder}
MultiReactionBuilder({
Key? key,
required List<ReactionBuilder> builders,
required Widget child,
}) : super(key: key, providers: builders, child: child);
}
25 changes: 15 additions & 10 deletions flutter_mobx/lib/src/reaction_builder.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/single_child_widget.dart';

/// A builder function that creates a reaction
typedef ReactionBuilderFunction = ReactionDisposer Function(
Expand All @@ -16,19 +17,18 @@ typedef ReactionBuilderFunction = ReactionDisposer Function(
/// [builder] that takes in a [BuildContext] and prepares the reaction. It should
/// end up returning a [ReactionDisposer]. This will be disposed when the [ReactionBuilder]
/// is disposed. The [child] Widget gets rendered as part of the build process.
class ReactionBuilder extends StatefulWidget {
class ReactionBuilder extends SingleChildStatefulWidget {
final ReactionBuilderFunction builder;
final Widget child;

const ReactionBuilder({Key? key, required this.child, required this.builder})
: super(key: key);
const ReactionBuilder({Key? key, Widget? child, required this.builder})
: super(key: key, child: child);

@override
ReactionBuilderState createState() => ReactionBuilderState();
}

@visibleForTesting
class ReactionBuilderState extends State<ReactionBuilder> {
class ReactionBuilderState extends SingleChildState<ReactionBuilder> {
late ReactionDisposer _disposeReaction;

bool get isDisposed => _disposeReaction.reaction.isDisposed;
Expand All @@ -40,14 +40,19 @@ class ReactionBuilderState extends State<ReactionBuilder> {
_disposeReaction = widget.builder(context);
}

@override
Widget build(BuildContext context) {
return widget.child;
}

@override
void dispose() {
_disposeReaction();
super.dispose();
}

@override
Widget buildWithChild(BuildContext context, Widget? child) {
assert(
child != null,
'''${widget.runtimeType} used outside of MultiReactionBuilder must specify a child''',
);

return child!;
}
}
3 changes: 2 additions & 1 deletion flutter_mobx/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: flutter_mobx
description:
Flutter integration for MobX. It provides a set of Observer widgets that automatically rebuild
when the tracked observables change.
version: 2.0.6+5
version: 2.1.0

homepage: https://github.com/mobxjs/mobx.dart
issue_tracker: https://github.com/mobxjs/mobx.dart/issues
Expand All @@ -14,6 +14,7 @@ dependencies:
flutter:
sdk: flutter
mobx: ^2.0.6
provider: ^6.0.0

dev_dependencies:
build_runner: ^2.0.6
Expand Down
2 changes: 2 additions & 0 deletions flutter_mobx/test/all_tests.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'flutter_mobx_test.dart' as flutter_mobx_test;
import 'reaction_builder_test.dart' as reaction_builder_test;
import 'multi_reaction_builder_test.dart' as multi_reaction_builder_test;

void main() {
flutter_mobx_test.main();
reaction_builder_test.main();
multi_reaction_builder_test.main();
}
61 changes: 61 additions & 0 deletions flutter_mobx/test/multi_reaction_builder_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/src/multi_reaction_builder.dart';
import 'package:flutter_mobx/src/reaction_builder.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobx/mobx.dart';

class Counter {
Counter();

Observable<int> state = Observable(0);

void increment() => runInAction(() => state.value = state.value + 1);
}

void main() {
group('MultiReactionBuilder', () {
testWidgets('calls reactions on state changes', (tester) async {
final statesA = <int>[];
const expectedStatesA = [1, 2];
final counterA = Counter();

final statesB = <int>[];
final expectedStatesB = [1];
final counterB = Counter();

await tester.pumpWidget(
MultiReactionBuilder(
key: const Key('MultiReactionBuilder'),
builders: [
ReactionBuilder(
builder: (context) => reaction(
(_) => counterA.state.value,
(int state) => statesA.add(state),
),
),
ReactionBuilder(
builder: (context) => reaction(
(_) => counterB.state.value,
(int state) => statesB.add(state),
),
),
],
child: const SizedBox(key: Key('multiListener_child')),
),
);
await tester.pumpAndSettle();

expect(find.byKey(const Key('multiListener_child')), findsOneWidget);

counterA.increment();
await tester.pump();
counterA.increment();
await tester.pump();
counterB.increment();
await tester.pump();

expect(statesA, expectedStatesA);
expect(statesB, expectedStatesB);
});
});
}
17 changes: 17 additions & 0 deletions flutter_mobx/test/reaction_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,22 @@ void main() {
message.value += 1;
expect(count, 2);
});

testWidgets(
'throws AssertionError if child is not specified in the builder',
(tester) async {
final message = Observable(0);
const expected =
'''ReactionBuilder used outside of MultiReactionBuilder must specify a child''';
await tester.pumpWidget(ReactionBuilder(
builder: (context) {
return reaction((_) => message.value, (int value) {});
},
));
expect(
tester.takeException(),
isA<AssertionError>().having((e) => e.message, 'message', expected),
);
});
});
}