From 9df19e7e480488d64ab8a5278e2c49aabe810454 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:59:48 +0000 Subject: [PATCH 1/4] Initial plan From c873924b99a6a1dc65133ccda25713810038de56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:10:00 +0000 Subject: [PATCH 2/4] Fix controller and focusNode disposal issue when provided externally Co-authored-by: EmilyMoonstone <81753412+EmilyMoonstone@users.noreply.github.com> --- lib/src/chips_input_autocomplete.dart | 27 +++++-- test/chips_input_autocomplete_test.dart | 97 +++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/lib/src/chips_input_autocomplete.dart b/lib/src/chips_input_autocomplete.dart index 1d1486d..3a77166 100644 --- a/lib/src/chips_input_autocomplete.dart +++ b/lib/src/chips_input_autocomplete.dart @@ -243,13 +243,20 @@ class ChipsInputAutocompleteState extends State { late final ChipsAutocompleteController _chipsAutocompleteController; late final GlobalKey>> _formFieldKey; late final FocusNode _focusNode; + bool _isControllerInternal = false; + bool _isFocusNodeInternal = false; String? _errorText; @override void initState() { super.initState(); - _chipsAutocompleteController = - widget.controller ?? ChipsAutocompleteController(); + if (widget.controller != null) { + _chipsAutocompleteController = widget.controller!; + _isControllerInternal = false; + } else { + _chipsAutocompleteController = ChipsAutocompleteController(); + _isControllerInternal = true; + } if (widget.options != null) { _chipsAutocompleteController.options = widget.options!; } @@ -262,13 +269,23 @@ class ChipsInputAutocompleteState extends State { _formFieldKey = widget.formFieldKey ?? GlobalKey>>(); _chipsAutocompleteController.formFieldKey = _formFieldKey; - _focusNode = widget.focusNode ?? FocusNode(); + if (widget.focusNode != null) { + _focusNode = widget.focusNode!; + _isFocusNodeInternal = false; + } else { + _focusNode = FocusNode(); + _isFocusNodeInternal = true; + } } @override void dispose() { - _chipsAutocompleteController.dispose(); - _focusNode.dispose(); + if (_isControllerInternal) { + _chipsAutocompleteController.dispose(); + } + if (_isFocusNodeInternal) { + _focusNode.dispose(); + } super.dispose(); } diff --git a/test/chips_input_autocomplete_test.dart b/test/chips_input_autocomplete_test.dart index cc7508d..45e005f 100644 --- a/test/chips_input_autocomplete_test.dart +++ b/test/chips_input_autocomplete_test.dart @@ -48,4 +48,101 @@ void main() { reason: 'Chip not added \nchipsAutocompleteController.chips: ${chipsAutocompleteController.chips} \nchipsAutocompleteController.text: ${chipsAutocompleteController.text}'); }); + + // Test 3: External controller with onChange should not cause disposal issues + testWidgets( + 'Widget with external controller and onChange callback handles rebuilds correctly', + (WidgetTester tester) async { + final chipsAutocompleteController = ChipsAutocompleteController(); + int onChangedCallCount = 0; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (context, setState) { + return ChipsInputAutocomplete( + controller: chipsAutocompleteController, + createCharacter: kCreateCharacter, + onChanged: (chips) { + onChangedCallCount++; + // Trigger a rebuild in the parent widget + setState(() {}); + }, + ); + }, + ), + ), + )); + + // Simulate typing 'test,' to trigger chip addition and onChange + await tester.enterText(find.byType(TextFormField), 'test$kCreateCharacter'); + await tester.pump(); + + // Verify the chip was added + expect(chipsAutocompleteController.chips.contains('test'), true); + expect(onChangedCallCount, 1); + + // Simulate adding another chip to verify controller is still functional + await tester.enterText(find.byType(TextFormField), 'test2$kCreateCharacter'); + await tester.pump(); + + // Verify the second chip was added + expect(chipsAutocompleteController.chips.contains('test2'), true); + expect(onChangedCallCount, 2); + }); + + // Test 4: External controller should not be disposed when widget is disposed + testWidgets('External controller should not be disposed when widget is disposed', + (WidgetTester tester) async { + final chipsAutocompleteController = ChipsAutocompleteController(); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: ChipsInputAutocomplete( + controller: chipsAutocompleteController, + ), + ), + )); + + // Verify the widget is present + expect(find.byType(ChipsInputAutocomplete), findsOneWidget); + + // Remove the widget from the tree + await tester.pumpWidget(const MaterialApp( + home: Scaffold( + body: SizedBox(), + ), + )); + + // Verify the controller is still functional after widget disposal + expect(() => chipsAutocompleteController.chips, returnsNormally); + expect(() => chipsAutocompleteController.addChip('test'), returnsNormally); + expect(chipsAutocompleteController.chips.contains('test'), true); + + // Clean up + chipsAutocompleteController.dispose(); + }); + + // Test 5: Internal controller should be disposed when widget is disposed + testWidgets('Internal controller should be disposed when widget is disposed', + (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold( + body: ChipsInputAutocomplete(), + ), + )); + + // Verify the widget is present + expect(find.byType(ChipsInputAutocomplete), findsOneWidget); + + // Remove the widget from the tree + await tester.pumpWidget(const MaterialApp( + home: Scaffold( + body: SizedBox(), + ), + )); + + // Widget disposal should not throw any errors + // (Internal controller is properly disposed) + }); } From 605c8afdce06840b28e0ee8f5f8cc3230f82c40d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:11:33 +0000 Subject: [PATCH 3/4] Add example demonstrating controller with onChange callback Co-authored-by: EmilyMoonstone <81753412+EmilyMoonstone@users.noreply.github.com> --- example/lib/example_on_changed.dart | 77 +++++++++++++++++++++++++++++ example/lib/main.dart | 5 ++ 2 files changed, 82 insertions(+) create mode 100644 example/lib/example_on_changed.dart diff --git a/example/lib/example_on_changed.dart b/example/lib/example_on_changed.dart new file mode 100644 index 0000000..e659e16 --- /dev/null +++ b/example/lib/example_on_changed.dart @@ -0,0 +1,77 @@ +import 'package:chips_input_autocomplete/chips_input_autocomplete.dart'; +import 'package:chips_input_autocomplete_example/constants.dart'; +import 'package:flutter/material.dart'; + +/// Example demonstrating the use of ChipsInputAutocomplete with an external +/// controller and onChange callback that triggers setState in parent widget. +/// +/// This example specifically tests the fix for the issue where rendering went +/// weird when both controller and onChange callback were set. +class ChipsInputExampleOnChanged extends StatefulWidget { + const ChipsInputExampleOnChanged({super.key}); + + @override + State createState() => + _ChipsInputExampleOnChangedState(); +} + +class _ChipsInputExampleOnChangedState + extends State { + final ChipsAutocompleteController _controller = ChipsAutocompleteController(); + String _selectedChipsText = ''; + int _changeCount = 0; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'External Controller with onChange', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + const Text( + 'This example uses an external controller and onChange callback ' + 'that triggers setState. This previously caused rendering issues ' + 'but is now fixed.', + style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 16), + Text('Options: ${options.join(', ')}'), + const SizedBox(height: 8), + ChipsInputAutocomplete( + controller: _controller, + options: options, + decorationTextField: const InputDecoration( + hintText: 'Type a fruit...', + ), + onChanged: (chips) { + // This setState call previously caused issues + setState(() { + _selectedChipsText = chips?.join(', ') ?? ''; + _changeCount++; + }); + }, + ), + const SizedBox(height: 8), + Text('Selected: $_selectedChipsText'), + Text('Changes: $_changeCount'), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 16e5a87..c7e019c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'package:chips_input_autocomplete_example/example_basic.dart'; import 'package:chips_input_autocomplete_example/example_form.dart'; import 'package:chips_input_autocomplete_example/example_insert_on_select.dart'; +import 'package:chips_input_autocomplete_example/example_on_changed.dart'; import 'package:chips_input_autocomplete_example/example_only_options.dart'; import 'package:chips_input_autocomplete_example/constants.dart'; import 'package:chips_input_autocomplete_example/example_options_async_basic.dart'; @@ -240,6 +241,10 @@ class _HomePageState extends State { const SizedBox( height: 16, ), + const ChipsInputExampleOnChanged(), + const SizedBox( + height: 16, + ), const ChipsInputExampleForm(), ], ), From 3404251dc4f9ef35e9747545b1f94244811f68e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:13:07 +0000 Subject: [PATCH 4/4] Update version to 1.2.2 and add CHANGELOG entry Co-authored-by: EmilyMoonstone <81753412+EmilyMoonstone@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5642ab2..a404080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## 1.2.2 +- fixed rendering issue when both `controller` and `onChange` properties are set +- widget now properly manages disposal of externally provided controllers and focus nodes +- added comprehensive tests for controller disposal scenarios +- added example demonstrating external controller with `onChange` callback + ## 1.2.1 - added `initialChips` to set initial chips - added `onChanged` and renamed old `onChanged` to `onChangedTextField` diff --git a/pubspec.yaml b/pubspec.yaml index 54b5b1f..040dc04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: chips_input_autocomplete description: "A chips input with autocomplete. Ideal for tagging or categorizing user input. Many options to customize the behavior and appearance." -version: 1.2.1 +version: 1.2.2 #homepage: repository: https://github.com/BruckCode/chips_input_autocomplete issue_tracker: https://github.com/BruckCode/chips_input_autocomplete/issues