You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Quill 6.0.6+1中embedBuilders为空抛出UnimplementedError求助

问题

我使用Flutter Quill 6.0.6+1版本及flutter_quill_extensions.dart,已在QuillEditor中配置:

embedBuilders: FlutterQuillEmbeds.builders()

同时已在工具栏添加图片/相机按钮,但尝试添加图片或拍照时,_buildCustomBlockEmbed方法抛出UnimplementedError:

Widget _buildCustomBlockEmbed(Embed node, BuildContext context,
  QuillController controller, bool readOnly) {
final builders = widget.embedBuilders;

if (builders != null) {
  var _node = node;

  // Creates correct node for custom embed
  if (node.value.type == BlockEmbed.customType) {
    _node = Embed(CustomBlockEmbed.fromJsonString(node.value.data));
  }

  for (final builder in builders) {
    if (builder.key == _node.value.type) {
      return builder.build(context, controller, _node, readOnly);
    }
  }
}

throw UnimplementedError(
  'Embeddable type "${node.value.type}" is not supported by supplied '
  'embed builders. You must pass your own builder function to '
  'embedBuilders property of QuillEditor or QuillField widgets.',
);

调试时发现node为Image类型、controller存在,但widget.embedBuilders始终为空,QuillEditor调试时能看到embeds,但进入该方法后变为null,请问可能的原因是什么?

相关组件代码
class AnswerQuillField extends ConsumerStatefulWidget {
  AnswerQuillField(
      {Key? key,
      required this.index,
      required this.correct,
      required this.label,
      required this.controller,
      required this.hint,
      required this.answer,
      required this.size,
      required this.mode})
      : super(key: key);

  int index;
  int size;
  bool answer;
  bool correct;
  String mode;
  bool validate = true;
  String? errorMessage;
  final String hint;
  final quill.QuillController? controller;
  // The label can be changed
  String label;

  void setLabel(String l) {}

  void setValidate(bool valid) {
    validate = valid;
  }

  // Set the Error messasge
  void setErrorMessage(String e) {
    errorMessage = e;
  }

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

class _AnswerQuillFieldState extends ConsumerState<AnswerQuillField> {
  Color correctColor = Colors.green;
  Color wrongColor = Colors.grey.shade300;
  final FocusNode _focusNode = FocusNode();

  // Renders the image picked by imagePicker from local file storage
  // You can also upload the picked image to any server (eg : AWS s3
  // or Firebase) and then return the uploaded image URL.
  Future<String> _onImagePickCallback(File file) async {
    // Copies the picked file from temporary cache to applications directory
    Directory appDocDir = await getApplicationDocumentsDirectory();
    final copiedFile =
        await file.copy('${appDocDir.path}/${basename(file.path)}');
    return copiedFile.path.toString();
  }

  // Renders the video picked by imagePicker from local file storage
  // You can also upload the picked video to any server (eg : AWS s3
  // or Firebase) and then return the uploaded video URL.
  Future<String> _onVideoPickCallback(File file) async {
    // Copies the picked file from temporary cache to applications directory
    final appDocDir = await getApplicationDocumentsDirectory();
    final copiedFile =
        await file.copy('${appDocDir.path}/${basename(file.path)}');
    return copiedFile.path.toString();
  }

  @override
  Widget build(BuildContext context) {
    // Watch one of the providers so the actual fields gets updated with changes
    // ignore: prefer_typing_uninitialized_variables
    var provider;
    if (widget.mode == "edit") {
      provider = ref.watch(CardEditProvider);
    } else if (widget.mode == "create") {
      provider = ref.watch(CardCreateProvider);
    }
    return Container(
      child: InkWell(
        onTap: () {
          showModalBottomSheet<void>(
            isScrollControlled: true,
            shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(15), topRight: Radius.circular(15)),
            ),
            context: context,
            builder: (BuildContext context) {
              return SingleChildScrollView(
                child: Container(
                  padding: MediaQuery.of(context).viewInsets,
                  child: Column(
                    children: [
                      Container(
                        decoration: const BoxDecoration(
                            color: Colors.white,
                            borderRadius: BorderRadius.only(
                              topLeft: Radius.circular(20.0),
                              topRight: Radius.circular(20.0),
                            )),
                        padding:
                            const EdgeInsets.only(left: 15, right: 15, top: 5),
                        child: Row(
                          children: [
                            const Spacer(),
                            GestureDetector(
                                onTap: () {
                                  Navigator.pop(context);
                                },
                                child: const Icon(Icons.check)),
                          ],
                        ),
                      ),
                      Container(
                        decoration: const BoxDecoration(
                            color: Colors.white,
                            borderRadius: BorderRadius.only(
                              topLeft: Radius.circular(20.0),
                              topRight: Radius.circular(20.0),
                            )),
                        height: 300,
                        padding: EdgeInsets.all(15),
                        child: quill.QuillEditor.basic(
                          controller: widget.controller!,
                          readOnly: false, // true for view only mode
                        ),
                      ),
                      quill.QuillToolbar.basic(
                        showDividers: true,
                        showAlignmentButtons: true,
                        showClearFormat: true,
                        showLink: false,
                        showStrikeThrough: true,
                        showBackgroundColorButton: true,
                        showCodeBlock: false,
                        showCenterAlignment: true,
                        showBoldButton: true,
                        showHeaderStyle: false,
                        showIndent: false,
                        showColorButton: true,
                        showItalicButton: true,
                        showInlineCode: false,
                        showJustifyAlignment: false,
                        showLeftAlignment: true,
                        showListBullets: true,
                        showListCheck: false,
                        showListNumbers: false,
                        showQuote: true,
                        showRedo: false,
                        showRightAlignment: false,
                        showSmallButton: true,
                        showUnderLineButton: true,
                        showUndo: true,
                        embedButtons: FlutterQuillEmbeds.buttons(
                          // provide a callback to enable picking images from device.
                          // if omit, "image" button only allows adding images from url.
                          // same goes for videos.
                          onImagePickCallback: _onImagePickCallback,
                          onVideoPickCallback: _onVideoPickCallback,
                          // uncomment to provide a custom "pick from" dialog.
                          // mediaPickSettingSelector: _selectMediaPickSetting,
                          // uncomment to provide a custom "pick from" dialog.
                          // cameraPickSettingSelector: _selectCameraPickSetting,
                        ),
                        controller: widget.controller!,
                        afterButtonPressed: _focusNode.requestFocus,
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
        child: InputDecorator(
          // ignore: sort_child_properties_last
          child: IgnorePointer(
            child: quill.QuillEditor(
              controller: widget.controller!,
              scrollController: ScrollController(),
              scrollable: false,
              focusNode: _focusNode,
              autoFocus: false,
              readOnly: false,
              placeholder: widget.hint,
              expands: false,
              padding: EdgeInsets.zero,
              showCursor: false,
              customStyles: quill.DefaultStyles(
                placeHolder: quill.DefaultTextBlockStyle(
                    const TextStyle(
                        fontSize: 16,
                        color: Color.fromARGB(255, 128, 115, 115),
                        height: 1.15,
                        fontWeight: FontWeight.normal),
                    const Tuple2(16, 0),
                    const Tuple2(0, 0),
                    null),
              ),
              embedBuilders: FlutterQuillEmbeds.builders(),
            ),
          ),
          decoration: InputDecoration(
            isDense: true,
            labelText: widget.label,
            labelStyle:
                const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
            floatingLabelBehavior: FloatingLabelBehavior.always,
            hintText: 'Write down your answer',
            errorText: widget.validate ? null : widget.errorMessage,
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
            ),
            suffixIcon: (widget.answer && provider.getQuillFields().length > 1)
                ? GestureDetector(
                    onTap: () => {
                      setState(() {
                        widget.correct
                            ? widget.correct = false
                            : widget.correct = true;
                      })
                    },
                    child: Icon(
                      Icons.check_circle,
                      color: widget.correct ? correctColor : wrongColor,
                    ),
                  )
                : const SizedBox.shrink(),
          ),
        ),
      ),
    );
  }
}
原因分析与解决方案

问题出在底部弹窗里的QuillEditor没有配置embedBuilders。

你仅在外部IgnorePointer包裹的QuillEditor中设置了embedBuilders: FlutterQuillEmbeds.builders(),但点击后弹出的底部弹窗内使用的是quill.QuillEditor.basic()构造方法,这个默认构造不会包含图片等嵌入类型的builder。当通过工具栏添加图片后,弹窗内的编辑器需要渲染图片嵌入,但缺少对应builder,触发_buildCustomBlockEmbed方法抛出错误。

解决步骤:

  • 将底部弹窗内的quill.QuillEditor.basic()替换为完整的QuillEditor构造,并添加embedBuilders: FlutterQuillEmbeds.builders()配置,保留原有参数。

修改后的弹窗内编辑器代码示例:

Container(
  decoration: const BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.only(
        topLeft: Radius.circular(20.0),
        topRight: Radius.circular(20.0),
      )),
  height: 300,
  padding: EdgeInsets.all(15),
  child: quill.QuillEditor(
    controller: widget.controller!,
    readOnly: false,
    scrollController: ScrollController(),
    scrollable: true,
    focusNode: _focusNode,
    autoFocus: true,
    expands: false,
    padding: EdgeInsets.zero,
    embedBuilders: FlutterQuillEmbeds.builders(), // 关键配置
  ),
),

另外需保持两个编辑器配置一致性,避免出现其他渲染差异问题。


内容的提问来源于stack exchange,提问作者daniel herzog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 09:10:34