Skip to content

Commit e34b8d7

Browse files
authored
Rebuild widget with new converter / state of the store when the Widget (#160)
is updated.
1 parent d1c5c06 commit e34b8d7

File tree

2 files changed

+190
-55
lines changed

2 files changed

+190
-55
lines changed

lib/flutter_redux.dart

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,18 @@ class _StoreStreamListenerState<S, ViewModel>
426426

427427
@override
428428
void initState() {
429-
_init();
429+
if (widget.onInit != null) {
430+
widget.onInit(widget.store);
431+
}
432+
433+
if (widget.onInitialBuild != null) {
434+
WidgetsBinding.instance.addPostFrameCallback((_) {
435+
widget.onInitialBuild(latestValue);
436+
});
437+
}
438+
439+
latestValue = widget.converter(widget.store);
440+
_createStream();
430441

431442
super.initState();
432443
}
@@ -442,76 +453,75 @@ class _StoreStreamListenerState<S, ViewModel>
442453

443454
@override
444455
void didUpdateWidget(_StoreStreamListener<S, ViewModel> oldWidget) {
456+
latestValue = widget.converter(widget.store);
457+
445458
if (widget.store != oldWidget.store) {
446-
_init();
459+
_createStream();
447460
}
448461

449462
super.didUpdateWidget(oldWidget);
450463
}
451464

452-
void _init() {
453-
if (widget.onInit != null) {
454-
widget.onInit(widget.store);
455-
}
465+
@override
466+
Widget build(BuildContext context) {
467+
return widget.rebuildOnChange
468+
? StreamBuilder<ViewModel>(
469+
stream: stream,
470+
builder: (context, snapshot) => widget.builder(
471+
context,
472+
latestValue,
473+
),
474+
)
475+
: widget.builder(context, latestValue);
476+
}
456477

457-
latestValue = widget.converter(widget.store);
478+
ViewModel _mapConverter(S state) {
479+
return widget.converter(widget.store);
480+
}
458481

459-
if (widget.onInitialBuild != null) {
460-
WidgetsBinding.instance.addPostFrameCallback((_) {
461-
widget.onInitialBuild(latestValue);
462-
});
482+
bool _whereDistinct(ViewModel vm) {
483+
if (widget.distinct) {
484+
return vm != latestValue;
463485
}
464486

465-
var _stream = widget.store.onChange;
487+
return true;
488+
}
466489

490+
bool _ignoreChange(S state) {
467491
if (widget.ignoreChange != null) {
468-
_stream = _stream.where((state) => !widget.ignoreChange(state));
492+
return !widget.ignoreChange(state);
469493
}
470494

471-
stream = _stream.map((_) => widget.converter(widget.store));
495+
return true;
496+
}
472497

473-
// Don't use `Stream.distinct` because it cannot capture the initial
474-
// ViewModel produced by the `converter`.
475-
if (widget.distinct) {
476-
stream = stream.where((vm) {
477-
final isDistinct = vm != latestValue;
498+
void _createStream() {
499+
stream = widget.store.onChange
500+
.where(_ignoreChange)
501+
.map(_mapConverter)
502+
// Don't use `Stream.distinct` because it cannot capture the initial
503+
// ViewModel produced by the `converter`.
504+
.where(_whereDistinct)
505+
// After each ViewModel is emitted from the Stream, we update the
506+
// latestValue. Important: This must be done after all other optional
507+
// transformations, such as ignoreChange.
508+
.transform(StreamTransformer.fromHandlers(handleData: _handleChange));
509+
}
478510

479-
return isDistinct;
480-
});
511+
void _handleChange(ViewModel vm, EventSink<ViewModel> sink) {
512+
latestValue = vm;
513+
514+
if (widget.onWillChange != null) {
515+
widget.onWillChange(latestValue);
481516
}
482517

483-
// After each ViewModel is emitted from the Stream, we update the
484-
// latestValue. Important: This must be done after all other optional
485-
// transformations, such as ignoreChange.
486-
stream =
487-
stream.transform(StreamTransformer.fromHandlers(handleData: (vm, sink) {
488-
latestValue = vm;
489-
490-
if (widget.onWillChange != null) {
491-
widget.onWillChange(latestValue);
492-
}
493-
494-
if (widget.onDidChange != null) {
495-
WidgetsBinding.instance.addPostFrameCallback((_) {
496-
widget.onDidChange(latestValue);
497-
});
498-
}
499-
500-
sink.add(vm);
501-
}));
502-
}
518+
if (widget.onDidChange != null) {
519+
WidgetsBinding.instance.addPostFrameCallback((_) {
520+
widget.onDidChange(latestValue);
521+
});
522+
}
503523

504-
@override
505-
Widget build(BuildContext context) {
506-
return widget.rebuildOnChange
507-
? StreamBuilder<ViewModel>(
508-
stream: stream,
509-
builder: (context, snapshot) => widget.builder(
510-
context,
511-
snapshot.hasData ? snapshot.data : latestValue,
512-
),
513-
)
514-
: widget.builder(context, latestValue);
524+
sink.add(vm);
515525
}
516526
}
517527

test/flutter_redux_test.dart

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void main() {
4242

4343
testWidgets('should update the children if the store changes',
4444
(WidgetTester tester) async {
45-
Widget widget([String state]) {
45+
Widget widget(String state) {
4646
return StoreProvider<String>(
4747
store: Store<String>(
4848
identityReducer,
@@ -487,14 +487,139 @@ void main() {
487487

488488
expect(numBuilds, 1);
489489

490-
// Dispatch another action of a different type. This should trigger another
491-
// rebuild
490+
// Dispatch another action of a different type. This should trigger
491+
// another rebuild
492492
store.dispatch('A');
493493

494494
await tester.pumpWidget(widget);
495495

496496
expect(numBuilds, 2);
497497
});
498+
499+
group('Updates', () {
500+
testWidgets(
501+
'converter update results in proper rebuild',
502+
(WidgetTester tester) async {
503+
String currentState;
504+
final store = Store<String>(
505+
identityReducer,
506+
initialState: 'I',
507+
);
508+
Widget widget([StoreConverter<String, String> converter = selector]) {
509+
return StoreProvider<String>(
510+
store: store,
511+
child: StoreConnector<String, String>(
512+
converter: converter,
513+
onInit: (store) => store.dispatch('A'),
514+
builder: (context, vm) {
515+
currentState = vm;
516+
return Container();
517+
},
518+
),
519+
);
520+
}
521+
522+
// Build the widget with the initial state
523+
await tester.pumpWidget(widget());
524+
525+
// Expect the Widget to be rebuilt and the onInit method to be called
526+
expect(currentState, 'A');
527+
528+
// Rebuild the widget with a new converter
529+
await tester.pumpWidget(widget((Store<String> s) => 'B'));
530+
531+
// Expect the Widget to be rebuilt and the converter should be rerun
532+
expect(currentState, 'B');
533+
},
534+
);
535+
536+
testWidgets(
537+
'onDidChange works as expected',
538+
(WidgetTester tester) async {
539+
String currentState;
540+
final store = Store<String>(
541+
identityReducer,
542+
initialState: 'I',
543+
);
544+
Widget widget([void Function(String viewModel) onDidChange]) {
545+
return StoreProvider<String>(
546+
store: store,
547+
child: StoreConnector<String, String>(
548+
converter: selector,
549+
onDidChange: onDidChange,
550+
onInit: (store) => store.dispatch('A'),
551+
builder: (context, vm) {
552+
return Container();
553+
},
554+
),
555+
);
556+
}
557+
558+
// Build the widget with the initial state
559+
await tester.pumpWidget(widget());
560+
561+
// No onDidChange function to run, so currentState should be null
562+
expect(currentState, isNull);
563+
564+
// Build the widget with a new onDidChange
565+
final newWidget = widget((_) => currentState = 'S');
566+
await tester.pumpWidget(newWidget);
567+
568+
// Dispatch a new value, which should cause onDidChange to run
569+
store.dispatch('B');
570+
571+
// Run pumpWidget, which should flush the after build (didChange)
572+
// callbacks
573+
await tester.pumpWidget(newWidget);
574+
575+
// Expect our new onDidChange to run
576+
expect(currentState, 'S');
577+
},
578+
);
579+
testWidgets(
580+
'onWillChange works as expected',
581+
(WidgetTester tester) async {
582+
String currentState;
583+
final store = Store<String>(
584+
identityReducer,
585+
initialState: 'I',
586+
);
587+
Widget widget([void Function(String viewModel) onWillChange]) {
588+
return StoreProvider<String>(
589+
store: store,
590+
child: StoreConnector<String, String>(
591+
converter: selector,
592+
onWillChange: onWillChange,
593+
onInit: (store) => store.dispatch('A'),
594+
builder: (context, vm) {
595+
return Container();
596+
},
597+
),
598+
);
599+
}
600+
601+
// Build the widget with the initial state
602+
await tester.pumpWidget(widget());
603+
604+
// No onWillChange function to run, so currentState should be null
605+
expect(currentState, isNull);
606+
607+
// Build the widget with a new onWillChange
608+
final newWidget = widget((_) => currentState = 'S');
609+
await tester.pumpWidget(newWidget);
610+
611+
// Dispatch a new value, which should cause onWillChange to run
612+
store.dispatch('B');
613+
614+
// Run pumpWidget, which should flush the after build (didChange)
615+
// callbacks
616+
await tester.pumpWidget(newWidget);
617+
618+
// Expect our new onWillChange to run
619+
expect(currentState, 'S');
620+
},
621+
);
622+
});
498623
});
499624

500625
group('StoreBuilder', () {

0 commit comments

Comments
 (0)