QuillEditorAppi
A powerful rich text editor widget powered by Quill.js, providing comprehensive text formatting capabilities with image support, compression, and flexible output formats.
Overview
QuillEditorAppi
integrates the popular Quill.js rich text editor into Flutter applications through WebView technology. It offers a full-featured editing experience with support for text formatting, image handling, and both JSON and plain text output formats.
Features
- 📝 Rich Text Editing - Full Quill.js editor with formatting toolbar
- 🖼️ Image Support - Image insertion, resizing, and compression
- 📱 Responsive Design - Adaptive layout for different screen sizes
- 🎨 Custom Styling - Configurable editor appearance and behavior
- 💾 Flexible Output - Support for both JSON and plain text formats
- 🔄 Real-time Updates - Live content change callbacks
- 📋 Prefill Content - Initialize with existing JSON or text content
- 🖱️ Drag & Drop - Image drag and drop functionality
- 🗜️ Image Compression - Automatic image optimization
- 🔧 Editable Control - Toggle between edit and read-only modes
Basic Usage
QuillEditorAppi(
editable: true,
outputType: 'data', // 'data' for JSON, 'text' for plain text
onChanged: (content) {
print('Editor content changed: $content');
},
prefillString: 'Initial text content',
)
Properties
Property | Type | Default | Description |
---|---|---|---|
editable | bool? | true | Whether the editor is editable or read-only |
onChanged | Function(String)? | null | Callback when content changes |
outputType | String? | 'data' | Output format: 'data' (JSON) or 'text' (plain text) |
prefillJson | String? | null | Initial content as JSON string |
prefillString | String? | null | Initial content as plain text |
Examples
Basic Rich Text Editor
class BasicEditorExample extends StatefulWidget {
_BasicEditorExampleState createState() => _BasicEditorExampleState();
}
class _BasicEditorExampleState extends State<BasicEditorExample> {
String editorContent = '';
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 400,
child: QuillEditorAppi(
editable: true,
outputType: 'text',
onChanged: (content) {
setState(() {
editorContent = content;
});
},
prefillString: 'Start typing your content here...',
),
),
SizedBox(height: 20),
Text('Content: $editorContent'),
],
);
}
}
JSON Output Editor
class JsonEditorExample extends StatefulWidget {
_JsonEditorExampleState createState() => _JsonEditorExampleState();
}
class _JsonEditorExampleState extends State<JsonEditorExample> {
String jsonContent = '';
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 500,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: QuillEditorAppi(
editable: true,
outputType: 'data', // JSON output
onChanged: (content) {
setState(() {
jsonContent = content;
});
saveContent(content);
},
),
),
SizedBox(height: 20),
Expanded(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
child: Text(
'JSON Output:\n$jsonContent',
style: TextStyle(fontFamily: 'monospace'),
),
),
),
),
],
);
}
void saveContent(String content) {
// Save JSON content to database or storage
print('Saving content: $content');
}
}
Read-Only Viewer
class ContentViewerExample extends StatelessWidget {
final String savedContent;
const ContentViewerExample({required this.savedContent});
Widget build(BuildContext context) {
return Container(
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: QuillEditorAppi(
editable: false, // Read-only mode
outputType: 'text',
prefillJson: savedContent, // Load saved JSON content
),
);
}
}
Blog Post Editor
class BlogPostEditor extends StatefulWidget {
final String? existingContent;
const BlogPostEditor({this.existingContent});
_BlogPostEditorState createState() => _BlogPostEditorState();
}
class _BlogPostEditorState extends State<BlogPostEditor> {
String title = '';
String content = '';
bool isPublished = false;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Blog Post Editor'),
actions: [
TextButton(
onPressed: saveDraft,
child: Text('Save Draft'),
),
ElevatedButton(
onPressed: publishPost,
child: Text('Publish'),
),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: 'Post Title',
border: OutlineInputBorder(),
),
onChanged: (value) => title = value,
),
SizedBox(height: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: QuillEditorAppi(
editable: true,
outputType: 'data',
onChanged: (value) {
content = value;
},
prefillJson: widget.existingContent,
),
),
),
],
),
),
);
}
void saveDraft() {
// Save as draft
saveBlogPost(title, content, false);
}
void publishPost() {
if (title.isNotEmpty && content.isNotEmpty) {
saveBlogPost(title, content, true);
Navigator.pop(context);
}
}
void saveBlogPost(String title, String content, bool published) {
// Save to database
print('Saving: $title, Published: $published');
}
}
Comment System
class CommentEditor extends StatefulWidget {
final Function(String) onSubmit;
const CommentEditor({required this.onSubmit});
_CommentEditorState createState() => _CommentEditorState();
}
class _CommentEditorState extends State<CommentEditor> {
String commentContent = '';
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Add a comment',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(height: 12),
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: QuillEditorAppi(
editable: true,
outputType: 'text',
onChanged: (content) {
commentContent = content;
},
prefillString: 'Share your thoughts...',
),
),
SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
// Clear content
setState(() {
commentContent = '';
});
},
child: Text('Cancel'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: commentContent.trim().isNotEmpty
? () {
widget.onSubmit(commentContent);
setState(() {
commentContent = '';
});
}
: null,
child: Text('Post Comment'),
),
],
),
],
),
);
}
}
Advanced Features
Image Handling
The editor includes built-in image support with compression:
// The editor automatically handles:
// - Image drag and drop
// - Image resizing
// - Image compression
// - Base64 encoding for storage
QuillEditorAppi(
editable: true,
outputType: 'data', // JSON includes image data
onChanged: (content) {
// Content includes embedded images as base64
handleImageContent(content);
},
)
Custom Styling
class StyledEditor extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[50]!, Colors.white],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: QuillEditorAppi(
editable: true,
outputType: 'data',
onChanged: handleContentChange,
),
),
);
}
}
Content Validation
class ValidatedEditor extends StatefulWidget {
_ValidatedEditorState createState() => _ValidatedEditorState();
}
class _ValidatedEditorState extends State<ValidatedEditor> {
String content = '';
String? errorMessage;
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 300,
decoration: BoxDecoration(
border: Border.all(
color: errorMessage != null ? Colors.red : Colors.grey[300]!,
),
borderRadius: BorderRadius.circular(8),
),
child: QuillEditorAppi(
editable: true,
outputType: 'text',
onChanged: (value) {
setState(() {
content = value;
errorMessage = validateContent(value);
});
},
),
),
if (errorMessage != null)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
errorMessage!,
style: TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
);
}
String? validateContent(String content) {
if (content.trim().isEmpty) {
return 'Content cannot be empty';
}
if (content.length < 10) {
return 'Content must be at least 10 characters';
}
if (content.length > 5000) {
return 'Content must be less than 5000 characters';
}
return null;
}
}
Output Formats
JSON Output (outputType: 'data')
{
"ops": [
{
"insert": "Hello "
},
{
"attributes": {
"bold": true
},
"insert": "World"
},
{
"insert": "\n"
}
]
}
Plain Text Output (outputType: 'text')
Hello World
Integration Patterns
State Management
// Using Provider
class EditorProvider extends ChangeNotifier {
String _content = '';
String get content => _content;
void updateContent(String newContent) {
_content = newContent;
notifyListeners();
}
}
// In widget
Consumer<EditorProvider>(
builder: (context, provider, child) {
return QuillEditorAppi(
editable: true,
outputType: 'data',
onChanged: provider.updateContent,
prefillJson: provider.content,
);
},
)
API Integration
class ApiIntegratedEditor extends StatefulWidget {
final String documentId;
const ApiIntegratedEditor({required this.documentId});
_ApiIntegratedEditorState createState() => _ApiIntegratedEditorState();
}
class _ApiIntegratedEditorState extends State<ApiIntegratedEditor> {
String content = '';
bool isLoading = true;
bool isSaving = false;
void initState() {
super.initState();
loadDocument();
}
Future<void> loadDocument() async {
try {
final response = await api.getDocument(widget.documentId);
setState(() {
content = response.content;
isLoading = false;
});
} catch (e) {
// Handle error
setState(() {
isLoading = false;
});
}
}
Future<void> saveDocument() async {
setState(() {
isSaving = true;
});
try {
await api.updateDocument(widget.documentId, content);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Document saved successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to save document')),
);
} finally {
setState(() {
isSaving = false;
});
}
}
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return Column(
children: [
Expanded(
child: QuillEditorAppi(
editable: true,
outputType: 'data',
onChanged: (value) {
content = value;
},
prefillJson: content,
),
),
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: isSaving ? null : saveDocument,
child: isSaving
? CircularProgressIndicator()
: Text('Save Document'),
),
),
],
);
}
}
Best Practices
- Performance: Use appropriate output type for your use case
- Content Validation: Always validate content before saving
- Error Handling: Implement proper error handling for WebView issues
- Accessibility: Ensure proper focus management and keyboard navigation
- Image Optimization: Monitor image sizes and compression settings
- Auto-save: Implement periodic auto-save for long-form content
Common Use Cases
- Blog Editors: Rich content creation for blogs and articles
- Comment Systems: Enhanced commenting with formatting
- Documentation: Technical documentation with code and images
- Email Composition: Rich email content creation
- Note Taking: Advanced note-taking applications
- Content Management: CMS content editing interfaces
Related Widgets
- QuillInputPopAppi - For popup rich text input
- JsonInputFieldAppi - For JSON editing
- TextFieldAppi - For simple text input
Migration Notes
When upgrading from basic text editors:
- Replace
TextField
withQuillEditorAppi
for rich text needs - Update content handling to support JSON format
- Implement image handling if using image features
- Consider WebView permissions and requirements
Technical Notes
- Uses WebView for Quill.js integration
- Requires internet connection for initial Quill.js loading
- Image compression reduces file sizes automatically
- JSON output preserves all formatting information
- Text output strips formatting for plain text use
Ready for popup rich text editing? Check out QuillInputPopAppi for modal editing experiences!, formatting, and embedding