Skip to content

Commit

Permalink
feat: add MultiReactionBuilder widget (#917)
Browse files Browse the repository at this point in the history
* feat: add `MultiReactionBuilder` widget
  • Loading branch information
amondnet authored Sep 22, 2023
1 parent 9a97f09 commit 2c2a7bb
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 11 deletions.
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),
);
});
});
}

0 comments on commit 2c2a7bb

Please sign in to comment.