mchome / flutter_colorpicker

HSV(HSB)/HSL/RGB/Material color picker inspired by all the good design for your amazing flutter apps.
https://pub.dev/packages/flutter_colorpicker
MIT License
339 stars 198 forks source link

hexInputController causes setState() called after dispose() exception #61

Closed glaceon2000 closed 2 years ago

glaceon2000 commented 2 years ago

The code that throws this exception. This exception is thrown when I close the Dialog and open it again, and is not thrown once I remove the controller.

class ColorPickerTextField extends StatefulWidget {
  const ColorPickerTextField(
      {Key? key, required this.color, this.onColorChanged})
      : super(key: key);
  final Color color;
  final void Function(String)? onColorChanged;

  @override
  _ColorPickerTextFieldState createState() => _ColorPickerTextFieldState();
}

class _ColorPickerTextFieldState extends State<ColorPickerTextField> {
  late Color _color;
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _color = widget.color;
    _controller.text = _color.value.toRadixString(16).toUpperCase();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
        fillColor: _color,
        hoverColor: _color,
        readOnly: true,
        textColor:
            (_color.red * 0.299 + _color.green * 0.587 + _color.blue * 0.114) >
                    186
                ? ColorProvider.kDarkBlue
                : Colors.white,
        controller: _controller,
        onTap: () => showDialog(
              context: context,
              builder: (_) => AlertDialog(
                title: Text(
                  'Pick a color!',
                ),
                content: SingleChildScrollView(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      ColorPicker(
                        displayThumbColor: true,
                        hexInputController: _controller,
                        pickerColor: _color,
                        onColorChanged: (color) {
                          setState(() {
                            _color = color;
                          });
                        },
                        showLabel: true,
                        pickerAreaHeightPercent: 0.8,
                      ),
                      Container(
                        width: 300,
                        padding: const EdgeInsets.all(16),
                        child: TextFormField(
                          controller: _controller,
                          autofocus: true,
                          ),
                          maxLength: 8,
                          inputFormatters: [
                            UpperCaseTextFormatter(),
                            FilteringTextInputFormatter.allow(
                              RegExp(kValidHexPattern),
                            ),
                          ],
                        ),
                      )
                    ],
                  ),
                ),
                actions: <Widget>[
                  CustomOrangeTextButton(
                    onTap: () => Navigator.of(context).pop(),
                    text: 'Done',
                  ),
                ],
              ),
            ));
  }
}

class UpperCaseTextFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(oldValue, TextEditingValue newValue) =>
      TextEditingValue(
          text: newValue.text.toUpperCase(), selection: newValue.selection);
}
mchome commented 2 years ago

I don't have any issues with your code above, is something I missed?

glaceon2000 commented 2 years ago

@mchome Did you try opening, then close then open the Dialog again? The exception is caught when I open the dialog again. It doesn't throw this exception once I remove the hexInputController value so I reckon it's something internal with the ColorPicker widget.

The following assertion was thrown while dispatching notifications for TextEditingController:
setState() called after dispose(): _ColorPickerState#a6d2e(lifecycle state: defunct, not mounted)

This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.

The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
mchome commented 2 years ago

Please provide the full code & steps that I can reproduce the bug.

glaceon2000 commented 2 years ago

@mchome Steps:

  1. Click on text field to open dialog
  2. Click on 'Done'
  3. Click on text field to open dialog again
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(title: const Text(_title)),
        body: MyStatefulWidget(),
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          height: 196,
          child: ColorPickerTextField(
            color: Colors.red,
          ),
        ),
      ),
    );
  }
}

class ColorPickerTextField extends StatefulWidget {
  const ColorPickerTextField(
      {Key? key, required this.color, this.onColorChanged})
      : super(key: key);
  final Color color;
  final void Function(String)? onColorChanged;

  @override
  _ColorPickerTextFieldState createState() => _ColorPickerTextFieldState();
}

class _ColorPickerTextFieldState extends State<ColorPickerTextField> {
  late Color _color;
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    _color = widget.color;
    _controller.text = _color.value.toRadixString(16).toUpperCase();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
        controller: _controller,
        onTap: () => showDialog(
              context: context,
              builder: (_) => AlertDialog(
                title: Text(
                  'Pick a color!',
                ),
                content: SingleChildScrollView(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      ColorPicker(
                        displayThumbColor: true,
                        hexInputController: _controller,
                        pickerColor: _color,
                        onColorChanged: (color) {
                          setState(() {
                            _color = color;
                          });
                        },
                        showLabel: true,
                        pickerAreaHeightPercent: 0.8,
                      ),
                      Container(
                        width: 300,
                        padding: const EdgeInsets.all(16),
                        child: TextFormField(
                          controller: _controller,
                          autofocus: true,
                          readOnly: true,
                          maxLength: 8,
                          inputFormatters: [
                            UpperCaseTextFormatter(),
                            FilteringTextInputFormatter.allow(
                              RegExp(kValidHexPattern),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
                actions: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: Text('Done'),
                  ),
                ],
              ),
            ));
  }
}

class UpperCaseTextFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(oldValue, TextEditingValue newValue) =>
      TextEditingValue(
          text: newValue.text.toUpperCase(), selection: newValue.selection);
}
mchome commented 2 years ago

Nan, here's my log:

Restarted application in 1,158ms.
D/InputMethodManager( 9702): showSoftInput() view=io.flutter.embedding.android.FlutterView{9712b15 VFED..... .F...... 0,0-1080,2028 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
W/IInputConnectionWrapper( 9702): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): endBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): endBatchEdit on inactive InputConnection
D/InsetsController( 9702): show(ime(), fromIme=true)
D/InsetsController( 9702): show(ime(), fromIme=true)
D/InputMethodManager( 9702): showSoftInput() view=io.flutter.embedding.android.FlutterView{9712b15 VFED..... .F...... 0,0-1080,2028 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
W/IInputConnectionWrapper( 9702): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): getTextBeforeCursor on inactive InputConnection
W/IInputConnectionWrapper( 9702): getTextAfterCursor on inactive InputConnection
W/IInputConnectionWrapper( 9702): getSelectedText on inactive InputConnection
W/IInputConnectionWrapper( 9702): endBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): getTextAfterCursor on inactive InputConnection
W/IInputConnectionWrapper( 9702): endBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): beginBatchEdit on inactive InputConnection
W/IInputConnectionWrapper( 9702): getTextAfterCursor on inactive InputConnection
W/IInputConnectionWrapper( 9702): endBatchEdit on inactive InputConnection
D/InsetsController( 9702): show(ime(), fromIme=true)
D/InsetsController( 9702): show(ime(), fromIme=true)
D/InputMethodManager( 9702): showSoftInput() view=io.flutter.embedding.android.FlutterView{9712b15 VFED..... .F...... 0,0-1080,2028 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
D/InsetsController( 9702): show(ime(), fromIme=true)
D/InputMethodManager( 9702): showSoftInput() view=io.flutter.embedding.android.FlutterView{9712b15 VFED..... .F...... 0,0-1080,2028 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
D/InsetsController( 9702): show(ime(), fromIme=true)

Device: Pixel 3 (Android 12) Flutter: 2.6.0-11.0.pre

glaceon2000 commented 2 years ago

I'm currently using it on Flutter Web - Chrome. I didn't expect this to be a platform issue

mchome commented 2 years ago

Oh, maybe I am writing v1.0.0, I will try v0.6.0 again.

glaceon2000 commented 2 years ago

I actually just tested on Pixel 4 XL (Android 11) - Flutter 2.5.2 stable and it produced the same exception. Would you kindly check again

mchome commented 2 years ago

I post v0.6.1 with the fixes.

glaceon2000 commented 2 years ago

May I ask what led to the exception and how you handled it?

mchome commented 2 years ago

Just remove the listener of textController in dispose() of ColorPicker.

colorpicker.dart:
class ColorPicker extends StatefulWidget {
...
  @override
  void dispose() {
    widget.hexInputController?.removeListener(colorPickerTextInputListener);
    super.dispose();
  }
...

flutter_colorpicker-v0.6.1.zip