Building a Custom Rich Text Editor in Flutter: Why I Skipped Flutter Quill


TL;DR
Flutter Quill caused 4 hours of dependency conflicts
Built custom solution in 2 hours with ~200 lines of code
Markdown-first approach works across all platforms
Zero external dependencies, complete control
The Problem Statement
When building ShabDify (a multi-platform content publisher), I needed a rich text editor with these requirements:
Functionality: Bold, italic, headers, lists, links
Output: Clean text for multiple platforms (Medium, LinkedIn, Reddit)
Maintainability: No dependency hell
Performance: Lightweight, fast rendering
Flutter Quill seemed like the obvious choice. Here's why it wasn't.
The Dependency Nightmare
Initial Setup Attempt
dependencies:
flutter_quill: ^11.0.0
flutter_quill_extensions: ^11.0.0
flutter_quill_delta_from_html: ^1.5.0
The Cascade of Conflicts
Conflict #1:
Because flutter_quill >=9.3.12 depends on intl ^0.19.0
and shabdify depends on intl ^0.20.2, version solving failed.
Solution: Downgrade intl to ^0.19.0
Conflict #2:
Because webview_flutter 4.13.0 depends on webview_flutter_android ^4.7.0
and quill_html_editor depends on webview_flutter_android ^3.13.2,
version solving failed.
Solution: Remove quill_html_editor
Conflict #3:
flutter_quill_delta_from_html >=1.4.1 is incompatible with flutter_quill ^9.4.4
At this point, I was spending more time managing dependencies than building features.
The Custom Solution Architecture
Core Components
// 1. Toolbar Widget
class FormattingToolbar extends StatelessWidget {
final TextEditingController controller;
final VoidCallback onFormatApplied;
const FormattingToolbar({
required this.controller,
required this.onFormatApplied,
});
}
// 2. Format Application Logic
class TextFormatter {
static void applyFormat(
TextEditingController controller,
String startTag,
String endTag,
) {
final selection = controller.selection;
final text = controller.text;
final selectedText = selection.textInside(text);
final newText = text.replaceRange(
selection.start,
selection.end,
'$startTag$selectedText$endTag',
);
controller.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: selection.start + startTag.length + selectedText.length + endTag.length,
),
);
}
}
// 3. Preview Renderer
class MarkdownPreview extends StatelessWidget {
final String markdown;
Widget _buildFormattedContent(String text) {
// Convert markdown to styled widgets
return _parseMarkdownToWidgets(text);
}
}
Implementation Details
Bold/Italic Formatting
void _applyBold() {
TextFormatter.applyFormat(controller, '**', '**');
}
void _applyItalic() {
TextFormatter.applyFormat(controller, '*', '*');
}
Heading Support
void _applyHeading() {
final selection = controller.selection;
final text = controller.text;
final lines = text.split('\n');
// Find current line and toggle heading
final currentLineIndex = _getCurrentLineIndex(selection.start, lines);
final currentLine = lines[currentLineIndex];
if (currentLine.startsWith('# ')) {
lines[currentLineIndex] = currentLine.substring(2);
} else {
lines[currentLineIndex] = '# $currentLine';
}
controller.text = lines.join('\n');
}
HTML Paste Detection
void _handlePaste(String pastedText) {
if (_isHtmlContent(pastedText)) {
final markdown = _convertHtmlToMarkdown(pastedText);
_insertText(markdown);
} else {
_insertText(pastedText);
}
}
bool _isHtmlContent(String text) {
return text.contains('<p>') ||
text.contains('<strong>') ||
text.contains('<em>') ||
text.contains('<h1>');
}
String _convertHtmlToMarkdown(String html) {
return html
.replaceAll(RegExp(r'<strong[^>]*>(.*?)</strong>'), '**\$1**')
.replaceAll(RegExp(r'<em[^>]*>(.*?)</em>'), '*\$1*')
.replaceAll(RegExp(r'<h1[^>]*>(.*?)</h1>'), '# \$1')
.replaceAll(RegExp(r'<p[^>]*>(.*?)</p>'), '\$1\n')
.replaceAll(RegExp(r'<[^>]+>'), ''); // Remove remaining tags
}
Platform-Specific Output
class PlatformFormatter {
static String formatForPlatform(String markdown, String platform) {
switch (platform.toLowerCase()) {
case 'medium':
return _convertToHtml(markdown);
case 'linkedin':
return _stripFormatting(markdown);
case 'reddit':
return markdown; // Reddit supports markdown
default:
return markdown;
}
}
static String _convertToHtml(String markdown) {
return markdown
.replaceAllMapped(RegExp(r'\*\*(.*?)\*\*'),
(match) => '<strong>${match.group(1)}</strong>')
.replaceAllMapped(RegExp(r'\*(.*?)\*'),
(match) => '<em>${match.group(1)}</em>')
.replaceAllMapped(RegExp(r'^# (.*)$', multiLine: true),
(match) => '<h1>${match.group(1)}</h1>');
}
}
Performance Comparison
MetricFlutter QuillCustom SolutionBundle size impact+2.3MB+0KBSetup time4+ hours2 hoursDependencies470Control levelLimitedCompleteMaintenanceExternalInternal
Advantages of the Custom Approach
1. Zero Dependencies
No version conflicts
No external maintenance burden
Smaller bundle size
2. Markdown-First Philosophy
Universal format support
Platform-agnostic content
Easy to parse and convert
3. Complete Control
Custom behaviors
Platform-specific optimizations
Easy debugging
4. Maintainability
~200 lines of understandable code
No black box behaviors
Easy to extend
Trade-offs and Considerations
What You Lose
Advanced features (tables, images, complex formatting)
Collaborative editing capabilities
Extensive plugin ecosystem
What You Gain
Predictable behavior
Easy customization
No dependency management
Complete understanding of the codebase
Conclusion
For ShabDify's use case, the custom solution was clearly superior. The key insight was realizing that I didn't need a full-featured editor—I needed a specific set of formatting tools with reliable output.
When to Build Custom:
Simple formatting requirements
Platform-specific output needs
Dependency conflicts with existing libraries
Need for complete control
When to Use Libraries:
Complex features (collaborative editing, plugins)
Rapid prototyping
Standard use cases with no customization needs
What's Next
In the next article, I'll cover:
Advanced markdown parsing techniques
Platform-specific content optimization
Building a plugin system for the custom editor
Have you faced similar dependency challenges? Share your experiences in the comments below.
Subscribe to my newsletter
Read articles from Nash9 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
