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
相关产品推荐
相关产品推荐

