diff --git a/lib/src/manager/pluto_change_notifier_filter.dart b/lib/src/manager/pluto_change_notifier_filter.dart index 6a15ef96e..ed0a8b240 100644 --- a/lib/src/manager/pluto_change_notifier_filter.dart +++ b/lib/src/manager/pluto_change_notifier_filter.dart @@ -107,6 +107,7 @@ abstract class PlutoChangeNotifierFilterResolver { /// grid_state stateManager.resetCurrentState.hashCode: 'resetCurrentState', + stateManager.setConfiguration.hashCode: 'setConfiguration', /// layout_state stateManager.setShowColumnTitle.hashCode: 'setShowColumnTitle', @@ -115,6 +116,7 @@ abstract class PlutoChangeNotifierFilterResolver { stateManager.setShowLoading.hashCode: 'setShowLoading', stateManager.notifyChangedShowFrozenColumn.hashCode: 'notifyChangedShowFrozenColumn', + stateManager.setTextDirection.hashCode: 'setTextDirection', /// pagination_state stateManager.setPageSize.hashCode: 'setPageSize', diff --git a/lib/src/manager/state/grid_state.dart b/lib/src/manager/state/grid_state.dart index aff6f729b..cfe144958 100644 --- a/lib/src/manager/state/grid_state.dart +++ b/lib/src/manager/state/grid_state.dart @@ -60,6 +60,7 @@ abstract class IGridState { PlutoGridConfiguration configuration, { bool updateLocale = true, bool applyColumnFilter = true, + bool notify = true, }); void setGridMode(PlutoGridMode mode); @@ -134,6 +135,7 @@ mixin GridState implements IPlutoGridState { PlutoGridConfiguration configuration, { bool updateLocale = true, bool applyColumnFilter = true, + bool notify = true, }) { if (_state._configuration == configuration) return; @@ -146,6 +148,8 @@ mixin GridState implements IPlutoGridState { if (applyColumnFilter) { _state._configuration!.applyColumnFilter(refColumns.originalList); } + + notifyListeners(notify); } @override diff --git a/lib/src/manager/state/layout_state.dart b/lib/src/manager/state/layout_state.dart index 0732270bb..4f4398408 100644 --- a/lib/src/manager/state/layout_state.dart +++ b/lib/src/manager/state/layout_state.dart @@ -120,7 +120,7 @@ abstract class ILayoutState { void notifyChangedShowFrozenColumn(); - void setTextDirection(TextDirection textDirection); + void setTextDirection(TextDirection textDirection, {bool notify = true}); @visibleForTesting void setGridGlobalOffset(Offset offset); @@ -503,8 +503,18 @@ mixin LayoutState implements IPlutoGridState { } @override - void setTextDirection(TextDirection textDirection) { + void setTextDirection(TextDirection textDirection, {bool notify = true}) { + if (_state._textDirection == textDirection) return; + _state._textDirection = textDirection; + + // setTextDirection is invoked from PlutoGridLayoutDelegate's constructor, + // which is created while PlutoGrid itself is building (inside + // LayoutBuilder). Notifying synchronously at that point would ask + // dependent widgets to rebuild while the widget tree above them is still + // being built, which Flutter disallows. Defer to the next frame instead, + // matching the pattern already used by setKeepFocus. + notifyListenersOnPostFrame(notify); } @override diff --git a/test/mock/shared_mocks.mocks.dart b/test/mock/shared_mocks.mocks.dart index ba46e8b29..d77338173 100644 --- a/test/mock/shared_mocks.mocks.dart +++ b/test/mock/shared_mocks.mocks.dart @@ -2427,6 +2427,7 @@ class MockPlutoGridStateManager extends _i1.Mock _i2.PlutoGridConfiguration? configuration, { bool? updateLocale = true, bool? applyColumnFilter = true, + bool? notify = true, }) => super.noSuchMethod( Invocation.method( @@ -2435,6 +2436,7 @@ class MockPlutoGridStateManager extends _i1.Mock { #updateLocale: updateLocale, #applyColumnFilter: applyColumnFilter, + #notify: notify, }, ), returnValueForMissingStub: null, @@ -2774,10 +2776,15 @@ class MockPlutoGridStateManager extends _i1.Mock ); @override - void setTextDirection(_i5.TextDirection? textDirection) => super.noSuchMethod( + void setTextDirection( + _i5.TextDirection? textDirection, { + bool? notify = true, + }) => + super.noSuchMethod( Invocation.method( #setTextDirection, [textDirection], + {#notify: notify}, ), returnValueForMissingStub: null, ); diff --git a/test/src/manager/state/grid_state_test.dart b/test/src/manager/state/grid_state_test.dart new file mode 100644 index 000000000..d82504f34 --- /dev/null +++ b/test/src/manager/state/grid_state_test.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pluto_grid/pluto_grid.dart'; + +import '../../../helper/column_helper.dart'; +import '../../../helper/row_helper.dart'; +import '../../../mock/mock_methods.dart'; +import '../../../mock/shared_mocks.mocks.dart'; + +void main() { + PlutoGridStateManager createStateManager({ + required List columns, + required List rows, + FocusNode? gridFocusNode, + PlutoGridScrollController? scroll, + PlutoGridConfiguration configuration = const PlutoGridConfiguration(), + }) { + final stateManager = PlutoGridStateManager( + columns: columns, + rows: rows, + gridFocusNode: gridFocusNode ?? MockFocusNode(), + scroll: scroll ?? MockPlutoGridScrollController(), + configuration: configuration, + ); + + stateManager.setEventManager(MockPlutoGridEventManager()); + + return stateManager; + } + + group('setConfiguration', () { + testWidgets( + 'When the configuration is changed, listeners should be notified.', + (WidgetTester tester) async { + // given + List columns = ColumnHelper.textColumn('body', count: 3); + List rows = RowHelper.count(3, columns); + + PlutoGridStateManager stateManager = createStateManager( + columns: columns, + rows: rows, + ); + + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setConfiguration( + const PlutoGridConfiguration( + style: PlutoGridStyleConfig(enableGridBorderShadow: true), + ), + ); + + // then + verify(listener.noParamReturnVoid()).called(1); + }, + ); + + testWidgets( + 'When the configuration is set to an equal value, ' + 'listeners should not be notified.', + (WidgetTester tester) async { + // given + List columns = ColumnHelper.textColumn('body', count: 3); + List rows = RowHelper.count(3, columns); + + const configuration = PlutoGridConfiguration(); + + PlutoGridStateManager stateManager = createStateManager( + columns: columns, + rows: rows, + configuration: configuration, + ); + + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setConfiguration(configuration); + + // then + verifyNever(listener.noParamReturnVoid()); + }, + ); + + testWidgets( + 'When notify is false, listeners should not be notified ' + 'even though the configuration changed.', + (WidgetTester tester) async { + // given + List columns = ColumnHelper.textColumn('body', count: 3); + List rows = RowHelper.count(3, columns); + + PlutoGridStateManager stateManager = createStateManager( + columns: columns, + rows: rows, + ); + + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setConfiguration( + const PlutoGridConfiguration( + style: PlutoGridStyleConfig(enableGridBorderShadow: true), + ), + notify: false, + ); + + // then + verifyNever(listener.noParamReturnVoid()); + }, + ); + }); +} diff --git a/test/src/manager/state/layout_state_test.dart b/test/src/manager/state/layout_state_test.dart index fa883999f..889fe72cb 100644 --- a/test/src/manager/state/layout_state_test.dart +++ b/test/src/manager/state/layout_state_test.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:pluto_grid/pluto_grid.dart'; import '../../../helper/column_helper.dart'; import '../../../helper/pluto_widget_test_helper.dart'; import '../../../helper/row_helper.dart'; +import '../../../mock/mock_methods.dart'; import '../../../mock/shared_mocks.mocks.dart'; void main() { @@ -158,4 +160,89 @@ void main() { }, ); }); + + group('setTextDirection', () { + late PlutoGridStateManager stateManager; + + setUp(() { + List columns = ColumnHelper.textColumn('body', count: 3); + List rows = RowHelper.count(3, columns); + + stateManager = PlutoGridStateManager( + columns: columns, + rows: rows, + gridFocusNode: MockFocusNode(), + scroll: MockPlutoGridScrollController(), + ); + + stateManager.setEventManager(MockPlutoGridEventManager()); + }); + + testWidgets( + 'When the text direction is changed, ' + 'listeners should be notified on the next frame.', + (WidgetTester tester) async { + // given + expect(stateManager.isLTR, isTrue); + + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setTextDirection(TextDirection.rtl); + + // notifyListenersOnPostFrame defers the notification, so listeners + // must not have been called synchronously yet. + verifyNever(listener.noParamReturnVoid()); + + await tester.pump(); + + // then + expect(stateManager.isRTL, isTrue); + verify(listener.noParamReturnVoid()).called(1); + }, + ); + + testWidgets( + 'When the text direction is set to the same value, ' + 'listeners should not be notified.', + (WidgetTester tester) async { + // given + expect(stateManager.isLTR, isTrue); + + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setTextDirection(TextDirection.ltr); + + await tester.pump(); + + // then + verifyNever(listener.noParamReturnVoid()); + }, + ); + + testWidgets( + 'When notify is false, listeners should not be notified ' + 'even though the text direction changed.', + (WidgetTester tester) async { + // given + final listener = MockMethods(); + + stateManager.addListener(listener.noParamReturnVoid); + + // when + stateManager.setTextDirection(TextDirection.rtl, notify: false); + + await tester.pump(); + + // then + expect(stateManager.isRTL, isTrue); + verifyNever(listener.noParamReturnVoid()); + }, + ); + }); }