[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-editor-di-testo-ricco-in-flutter-con-flutter-quill-delta-toolbar-e-persistenza":3,"comments-article-editor-di-testo-ricco-in-flutter-con-flutter-quill-delta-toolbar-e-persistenza":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},136,"Editor di testo ricco in Flutter con flutter_quill: Delta, toolbar e persistenza","editor-di-testo-ricco-in-flutter-con-flutter-quill-delta-toolbar-e-persistenza","Come integrare un editor WYSIWYG in un'app Flutter con flutter_quill: setup, formato Delta, toolbar personalizzata, salvataggio su backend, conversione in HTML e modalità sola lettura.","Prima o poi capita a quasi tutti: l'app deve permettere all'utente di scrivere qualcosa di più di una stringa piatta. Note formattate, descrizioni di prodotti, commenti con link e liste puntate, documenti collaborativi. Un `TextField` non basta più e serve un editor **rich text**.\n\nIn Flutter la soluzione più matura e diffusa è **flutter_quill**, il porting dell'editor Quill (quello usato da Slack, LinkedIn e molti CMS) sul framework di Google. In questo articolo vediamo come integrarlo, come funziona il formato **Delta** su cui si basa, come personalizzare la toolbar, salvare i contenuti e renderizzarli in sola lettura.\n\n## Perché flutter_quill (e le alternative)\n\nPrima di partire, una panoramica onesta del panorama:\n\n- **flutter_quill**: il più usato, API stabile, supporto a embed (immagini, video), undo\u002Fredo, scorciatoie da tastiera. Ottimo per editor \"da form\", note e descrizioni.\n- **super_editor** (Flutter Bounty Hunters): più potente e componibile, pensato per editor stile Notion\u002FMedium, ma con curva di apprendimento più ripida.\n- **appflowy_editor**: editor a blocchi, nato dentro AppFlowy; ideale se vuoi un'esperienza \"a blocchi\" con slash commands.\n\nSe l'obiettivo è \"un campo di testo formattabile dentro un form\", flutter_quill è quasi sempre la scelta giusta per rapporto sforzo\u002Frisultato.\n\n## Setup\n\nAggiungi le dipendenze (questo articolo si riferisce alla serie **11.x**, che ha rinominato i vecchi parametri `configurations` in `config`):\n\n```yaml\ndependencies:\n  flutter_quill: ^11.0.0\n  flutter_quill_extensions: ^11.0.0 # opzionale: immagini e video\n```\n\nflutter_quill ha le proprie stringhe localizzate: vanno registrate nel `MaterialApp`, altrimenti in debug ottieni un'eccezione sul delegate mancante.\n\n```dart\nimport 'package:flutter_quill\u002Fflutter_quill.dart';\nimport 'package:flutter_localizations\u002Fflutter_localizations.dart';\n\nMaterialApp(\n  localizationsDelegates: const [\n    FlutterQuillLocalizations.delegate,\n    GlobalMaterialLocalizations.delegate,\n    GlobalCupertinoLocalizations.delegate,\n    GlobalWidgetsLocalizations.delegate,\n  ],\n  supportedLocales: const [Locale('it'), Locale('en')],\n  home: const EditorPage(),\n);\n```\n\n## Il primo editor funzionante\n\nIl cuore di tutto è il `QuillController`: contiene il documento, la selezione corrente e lo stack di undo\u002Fredo. La UI è composta da due widget indipendenti, una toolbar e un editor, legati dallo stesso controller.\n\n```dart\nclass EditorPage extends StatefulWidget {\n  const EditorPage({super.key});\n\n  @override\n  State\u003CEditorPage> createState() => _EditorPageState();\n}\n\nclass _EditorPageState extends State\u003CEditorPage> {\n  final QuillController _controller = QuillController.basic();\n  final FocusNode _focusNode = FocusNode();\n  final ScrollController _scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    _focusNode.dispose();\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Nuova nota')),\n      body: Column(\n        children: [\n          QuillSimpleToolbar(\n            controller: _controller,\n            config: const QuillSimpleToolbarConfig(\n              showFontFamily: false,\n              showFontSize: false,\n              showSearchButton: false,\n              multiRowsDisplay: false, \u002F\u002F toolbar su una riga scrollabile\n            ),\n          ),\n          const Divider(height: 1),\n          Expanded(\n            child: Padding(\n              padding: const EdgeInsets.all(16),\n              child: QuillEditor.basic(\n                controller: _controller,\n                focusNode: _focusNode,\n                scrollController: _scrollController,\n                config: const QuillEditorConfig(\n                  placeholder: 'Scrivi qui...',\n                  padding: EdgeInsets.zero,\n                  autoFocus: false,\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n```\n\nCon poche righe hai già grassetto, corsivo, liste, titoli, allineamenti, link, undo\u002Fredo e scorciatoie da tastiera (Cmd\u002FCtrl+B, Cmd\u002FCtrl+Z…) funzionanti su mobile, desktop e web.\n\n> **Attenzione al `dispose()`**: il controller va sempre liberato, altrimenti si accumulano listener e il documento resta in memoria. È una delle cause più comuni di leak negli editor tenuti dentro bottom sheet o dialog.\n\n## Il formato Delta: perché non è HTML\n\nQuill non salva HTML, ma un formato JSON chiamato **Delta**: una lista di operazioni che descrivono il contenuto e i suoi attributi.\n\n```json\n[\n  { \"insert\": \"Titolo\\n\", \"attributes\": { \"header\": 1 } },\n  { \"insert\": \"Testo con una parte \" },\n  { \"insert\": \"in grassetto\", \"attributes\": { \"bold\": true } },\n  { \"insert\": \"\\n\" }\n]\n```\n\nI vantaggi rispetto all'HTML sono concreti:\n\n- è **non ambiguo** (nessun tag malformato, nessun problema di sanitizzazione all'origine);\n- è **compatto** e facile da diffare;\n- è il formato nativo per implementare **merge e collaborazione** (operational transformation).\n\nLo svantaggio è che se il backend o un sito web devono mostrare lo stesso contenuto, serve una conversione. Ne parliamo tra poco.\n\n## Salvare e ricaricare un documento\n\nIl salvataggio è semplicemente la serializzazione del Delta:\n\n```dart\nimport 'dart:convert';\n\nString serializeDocument(QuillController controller) {\n  final delta = controller.document.toDelta().toJson();\n  return jsonEncode(delta);\n}\n\nQuillController deserializeDocument(String? json) {\n  if (json == null || json.isEmpty) return QuillController.basic();\n  final document = Document.fromJson(jsonDecode(json) as List\u003Cdynamic>);\n  return QuillController(\n    document: document,\n    selection: const TextSelection.collapsed(offset: 0),\n  );\n}\n```\n\nSe ti serve anche una versione testuale (per la ricerca full-text o per l'anteprima in una lista), c'è `document.toPlainText()`.\n\n### Autosalvataggio con debounce\n\nIl controller espone uno stream di modifiche, perfetto per un salvataggio automatico senza martellare il backend:\n\n```dart\nStreamSubscription\u003CDocChange>? _sub;\nTimer? _debounce;\n\n@override\nvoid initState() {\n  super.initState();\n  _sub = _controller.document.changes.listen((event) {\n    \u002F\u002F event.source: local oppure remote\n    _debounce?.cancel();\n    _debounce = Timer(const Duration(milliseconds: 800), _save);\n  });\n}\n\nFuture\u003Cvoid> _save() async {\n  final payload = serializeDocument(_controller);\n  await repository.updateNote(noteId, payload);\n}\n\n@override\nvoid dispose() {\n  _debounce?.cancel();\n  _sub?.cancel();\n  _controller.dispose();\n  super.dispose();\n}\n```\n\nEvita invece di chiamare `setState()` a ogni carattere: l'editor si aggiorna da solo tramite il controller, e un rebuild dell'intera pagina a ogni battuta è lo scenario ideale per far scendere i frame rate.\n\n## Modalità sola lettura\n\nDalla versione 10 la proprietà `readOnly` vive sul controller, non più sulle configurazioni dell'editor. Per mostrare un contenuto senza permetterne la modifica:\n\n```dart\nfinal viewController = QuillController(\n  document: Document.fromJson(jsonDecode(json)),\n  selection: const TextSelection.collapsed(offset: 0),\n  readOnly: true,\n);\n\nQuillEditor.basic(\n  controller: viewController,\n  config: const QuillEditorConfig(\n    showCursor: false,\n    embedBuilders: [],\n    padding: EdgeInsets.zero,\n  ),\n);\n```\n\nSe l'anteprima è dentro una `ListView`, ricorda di impostare l'editor come **non scrollabile** (`QuillEditorConfig(scrollable: false)`) e di avvolgerlo in un contenitore con altezza vincolata, altrimenti ti scontri con i classici errori di layout di due scroll annidati.\n\n## Una toolbar su misura\n\nLa `QuillSimpleToolbar` è comoda ma generica. Quando il design lo richiede, puoi comporre la tua toolbar usando i bottoni esposti dal pacchetto:\n\n```dart\nQuillToolbar(\n  config: const QuillToolbarConfig(),\n  child: Row(\n    children: [\n      QuillToolbarToggleStyleButton(\n        controller: _controller,\n        attribute: Attribute.bold,\n      ),\n      QuillToolbarToggleStyleButton(\n        controller: _controller,\n        attribute: Attribute.italic,\n      ),\n      QuillToolbarToggleStyleButton(\n        controller: _controller,\n        attribute: Attribute.ul,\n      ),\n      QuillToolbarLinkStyleButton(controller: _controller),\n      QuillToolbarHistoryButton(controller: _controller, isUndo: true),\n      QuillToolbarHistoryButton(controller: _controller, isUndo: false),\n    ],\n  ),\n);\n```\n\nNiente vieta di usare bottoni completamente custom: tutte le operazioni sono disponibili in modo programmatico sul controller.\n\n```dart\n\u002F\u002F Applica\u002Frimuove il grassetto sulla selezione\n_controller.formatSelection(Attribute.bold);\n\n\u002F\u002F Trasforma il paragrafo corrente in titolo H2\n_controller.formatSelection(Attribute.h2);\n\n\u002F\u002F Inserisce testo alla posizione corrente\nfinal index = _controller.selection.baseOffset;\n_controller.replaceText(\n  index,\n  0,\n  '@mario.rossi ',\n  TextSelection.collapsed(offset: index + 13),\n);\n```\n\nQuesto è il meccanismo da usare per funzionalità come menzioni, slash command o inserimento di template.\n\n## Immagini e contenuti embeddati\n\nGli embed (immagini, video, formule) non sono inclusi nel pacchetto base per non appesantirlo: vivono in `flutter_quill_extensions`.\n\n```dart\nQuillEditor.basic(\n  controller: _controller,\n  config: QuillEditorConfig(\n    embedBuilders: FlutterQuillEmbeds.editorBuilders(),\n  ),\n);\n\nQuillSimpleToolbar(\n  controller: _controller,\n  config: QuillSimpleToolbarConfig(\n    embedButtons: FlutterQuillEmbeds.toolbarButtons(),\n  ),\n);\n```\n\nUn punto di attenzione architetturale: di default l'immagine viene inserita nel Delta come **path locale** o URL. In produzione quasi sempre vuoi intercettare la selezione, caricare il file sul tuo storage e salvare nel documento l'URL remoto; altrimenti il contenuto sarà illeggibile su un altro dispositivo. Il pacchetto permette di fornire callback personalizzate per la selezione e l'inserimento delle immagini proprio per gestire questo flusso.\n\n## Interoperabilità: da Delta a HTML e ritorno\n\nSe il contenuto deve essere mostrato anche su un sito, in una email o in un PDF, servono due conversioni:\n\n- **Delta → HTML**: `vsc_quill_delta_to_html`, porting della libreria JS di riferimento, gestisce correttamente liste annidate e attributi inline.\n- **HTML → Delta**: `flutter_quill_delta_from_html`, utile per importare contenuti già esistenti nel database.\n\n```dart\nimport 'package:vsc_quill_delta_to_html\u002Fvsc_quill_delta_to_html.dart';\n\nString toHtml(QuillController controller) {\n  final deltaOps = controller.document.toDelta().toJson();\n  return QuillDeltaToHtmlConverter(\n    List.castFrom(deltaOps),\n    ConverterOptions.forEmail(),\n  ).convert();\n}\n```\n\nRegola d'oro: **la fonte di verità resta il Delta**. Converti in HTML solo al momento della visualizzazione esterna, e sanifica sempre l'output lato server prima di iniettarlo in una pagina web.\n\n## Consigli pratici e trappole comuni\n\n- **Tastiera e insets**: su mobile, metti la toolbar in fondo (sopra la tastiera) usando `Scaffold(resizeToAvoidBottomInset: true)` e un `Column` con l'editor dentro un `Expanded`. Una toolbar in alto, su schermi piccoli, costringe l'utente a scrollare continuamente.\n- **Documento vuoto**: un Delta valido termina sempre con `\\n`. Per capire se l'utente ha davvero scritto qualcosa, usa `_controller.document.toPlainText().trim().isEmpty`.\n- **Stili coerenti col tema**: `QuillEditorConfig(customStyles: DefaultStyles(...))` permette di allineare titoli, paragrafi e blockquote alla tipografia della tua app, dark mode inclusa.\n- **Limitare la formattazione**: in molti form non servono 20 bottoni. Disattivare opzioni nella `QuillSimpleToolbarConfig` riduce il rischio di contenuti illeggibili e semplifica la conversione in HTML.\n- **Test**: nei widget test puoi pilotare l'editor direttamente dal controller (`controller.document.insert(0, 'testo')`) e verificare attributi e output serializzato, senza dover simulare tap sulla toolbar.\n- **Web**: su Flutter Web la gestione del copia\u002Fincolla di contenuti formattati e dell'IME è migliorata molto, ma vale la pena testarla presto se il tuo target principale è il browser.\n\n## Conclusioni\n\nflutter_quill risolve in modo pragmatico un problema che da zero costerebbe settimane di lavoro: un editor rich text completo, multipiattaforma e con un formato dati serio alle spalle. Le decisioni importanti non sono nel widget, ma intorno: scegliere il Delta come fonte di verità, gestire l'upload delle immagini verso uno storage remoto, limitare le opzioni di formattazione a quelle realmente necessarie e pianificare da subito la conversione verso HTML se il contenuto deve vivere anche fuori dall'app.\n\nFatte queste scelte, integrare l'editor diventa la parte più semplice del lavoro.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F4a5a681a-fdb6-46b1-9d7a-9be504cce9d3.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1621241529943-17ee160b1d9d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI3MjgxMTh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Brett Jordan","https:\u002F\u002Funsplash.com\u002F@brett_jordan","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-JlVJ10hdJ1c",null,"published","2026-10-23T04:01:59+00:00","Editor rich text in Flutter con flutter_quill","Guida pratica a flutter_quill: setup, formato Delta, toolbar personalizzata, salvataggio, immagini e conversione in HTML per editor rich text in Flutter.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1793098107667]