flutter-quill icon indicating copy to clipboard operation
flutter-quill copied to clipboard

Custom UIBuilder does not preserve widget state

Open fa0311 opened this issue 8 months ago • 1 comments

Have you checked for an existing issue?

Flutter Quill Version

11.2.0

Steps to Reproduce

     QuillEditorConfig(
        customStyles: DefaultStyles(
          lists: DefaultListBlockStyle(
            const TextStyle(color: Colors.red),
            HorizontalSpacing.zero,
            const VerticalSpacing(4, 4),
            const VerticalSpacing(4, 4),
            null,
            _CustomCheckBox(), // <-- custom builder
          ),
        )
class _CustomCheckBox extends QuillCheckboxBuilder {
  @override
  Widget build({
    required BuildContext context,
    required bool isChecked,
    required ValueChanged<bool> onChanged,
  }) {
    return GestureDetector(
      onTap: () => onChanged(!isChecked),
      child: const Padding(
        padding: EdgeInsets.symmetric(horizontal: 4),
        child: CountStateWidget(),
      ),
    );
  }
}

int count = 0;

class CountStateWidget extends StatefulWidget {
  const CountStateWidget({super.key});

  @override
  State<CountStateWidget> createState() => _CountStateWidgetState();
}

class _CountStateWidgetState extends State<CountStateWidget> {
  late final int i;

  @override
  void initState() {
    super.initState();
    count++;
    i = count;
  }

  @override
  Widget build(BuildContext context) => Text(i.toString());
}

Observe the numbers (0 1 2 …) next to each line: they do not stay fixed; the same line may rebuild with a new value.

Expected results

CountStateWidget should have its initState called exactly once for each checklist line, retaining its internal data as long as that line exists. Tapping the checkbox should not create a brand‑new CountStateWidget.

Actual results

initState is called multiple times for the same logical item, so the counter keeps changing unexpectedly.

Additional Context

Add a stable Key when bulletPointLeading returns QuillBulletPoint:

Widget bulletPointLeading(LeadingConfig config) => QuillBulletPoint(
  key: ...,
  style:   config.style!,
  width:   config.width!,
  padding: config.padding!,
);

With the key in place, Flutter can match the widget across rebuilds and your nested StatefulWidget keeps its state.

fa0311 avatar Apr 17 '25 02:04 fa0311

🛠️ Fix: Add a Stable Key to Your Bullet Builder

The issue here is that without a key, Flutter can’t match your checkbox widgets across rebuilds—so each rebuild creates a brand-new CountStateWidget (and calls initState again). You can fix this by giving your QuillBulletPoint a stable key based on the line’s identity (for example its index or document offset):

Widget bulletPointLeading(LeadingConfig config) {
  return QuillBulletPoint(
    key: ValueKey(config.index), // ← use a stable identifier per checklist line
    style: config.style!,
    width: config.width!,
    padding: config.padding!,
    // ...or wrap your custom builder here instead of the default
  );
}

If config.index isn’t available, you can also use something like:

key: Key('bullet-${config.documentOffset}'),

or any other unique & consistent value for each list item.

With this change, each CountStateWidget is matched across rebuilds, so its initState only runs once and your counter stays fixed per line.

shafisma avatar May 01 '25 10:05 shafisma