From 23c33c5c2974ecbf3321e87c31b083da6b6fcd7c Mon Sep 17 00:00:00 2001 From: Menna Elwan Date: Thu, 9 Jul 2026 14:11:36 +0300 Subject: [PATCH 1/2] fix: notify listeners on setConfiguration and setTextDirection changes Both methods mutated internal state without calling notifyListeners(), so PlutoStateWithChange-based cells (icons, Select, Date) never rebuilt on their own after a configuration or RTL/LTR direction change - only when an unrelated event happened to force a rebuild. setTextDirection defers via notifyListenersOnPostFrame since it's invoked mid-build (PlutoGridLayoutDelegate's constructor), matching the existing pattern used by setKeepFocus. --- .../manager/pluto_change_notifier_filter.dart | 2 + lib/src/manager/state/grid_state.dart | 4 + lib/src/manager/state/layout_state.dart | 14 ++- test/mock/shared_mocks.mocks.dart | 9 +- test/src/manager/state/grid_state_test.dart | 119 ++++++++++++++++++ test/src/manager/state/layout_state_test.dart | 87 +++++++++++++ 6 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 test/src/manager/state/grid_state_test.dart 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..f2f1987e2 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, setConfiguration.hashCode); } @override diff --git a/lib/src/manager/state/layout_state.dart b/lib/src/manager/state/layout_state.dart index 0732270bb..6c889e1cb 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, setTextDirection.hashCode); } @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()); + }, + ); + }); } From 0b2fa606d948bc2975dddc0a541dfe290473b31e Mon Sep 17 00:00:00 2001 From: Menna Elwan <120137109+menna3lwan@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:13:33 +0300 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/src/manager/state/grid_state.dart | 2 +- lib/src/manager/state/layout_state.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/manager/state/grid_state.dart b/lib/src/manager/state/grid_state.dart index f2f1987e2..cfe144958 100644 --- a/lib/src/manager/state/grid_state.dart +++ b/lib/src/manager/state/grid_state.dart @@ -149,7 +149,7 @@ mixin GridState implements IPlutoGridState { _state._configuration!.applyColumnFilter(refColumns.originalList); } - notifyListeners(notify, setConfiguration.hashCode); + notifyListeners(notify); } @override diff --git a/lib/src/manager/state/layout_state.dart b/lib/src/manager/state/layout_state.dart index 6c889e1cb..4f4398408 100644 --- a/lib/src/manager/state/layout_state.dart +++ b/lib/src/manager/state/layout_state.dart @@ -514,7 +514,7 @@ mixin LayoutState implements IPlutoGridState { // 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, setTextDirection.hashCode); + notifyListenersOnPostFrame(notify); } @override