A production-grade Flutter package providing GitHub/GitLab-style content comparison and diff viewing. Built with Clean Architecture, SOLID principles, and designed for enterprise use.
- 🔄 GitHub-style side-by-side diff view
- 📄 Unified diff view (single column with +/- indicators)
- 📱 Stacked diff view (old above, new below — optimized for mobile)
- 🔤 Word-level inline highlighting — shows exactly which words changed
- 🔡 Character-level inline highlighting — maximum precision
- 🔢 Line numbers in the gutter
- 📊 Change summary bar (+N additions, -N deletions)
- 🧭 Change navigation (Previous / Next change buttons)
- 📂 Collapsible unchanged sections with configurable context lines
- 🎨 Light and dark themes (GitHub-style out of the box)
- 🎯 Fully customizable — every UI section has a builder callback
- 🔌 Pluggable diff algorithm — swap in your own engine
- 🔍 Added Whitespace Highlighting — visually marks added/extra spaces (
·) and tabs (→) ↔️ Synchronized Horizontal Scrolling — all line rows scroll together smoothly across each panel- ♿ Accessibility — semantic labels, screen reader support, color-independent indicators
- ⚡ Performant —
ListView.builderhandles 50,000+ line documents - 🖥️ Responsive — auto-selects layout based on screen width
- 🌍 Localization — all user-visible strings are customizable
Add to your pubspec.yaml:
dependencies: flutter_diff_viewer: ^1.4.0
Then run:
flutter pub get
import 'package:flutter_diff_viewer/flutter_diff_viewer.dart'; FlutterDiffViewer( oldContent: 'Hello World\nLine two', newContent: 'Hello Dart\nLine two\nNew line', )
That's it! The widget automatically:
- Calculates the diff asynchronously
- Selects the appropriate layout for the screen size
- Renders with the default GitHub-style light theme
FlutterDiffViewer( oldContent: oldText, newContent: newText, oldLabel: 'v1.2', newLabel: 'v1.3', configuration: FlutterDiffViewerConfiguration( layout: DiffLayout.sideBySide, theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), ), )
FlutterDiffViewer( oldContent: oldText, newContent: newText, configuration: FlutterDiffViewerConfiguration( layout: DiffLayout.unified, showLineNumbers: true, showSummary: true, theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), ), )
FlutterDiffViewer( oldContent: oldText, newContent: newText, theme: FlutterDiffViewerTheme.dark(), )
FlutterDiffViewer( oldContent: oldText, newContent: newText, configuration: FlutterDiffViewerConfiguration.adaptive(context), )
FlutterDiffViewerConfiguration controls all layout and feature options:
FlutterDiffViewerConfiguration( // Layout layout: DiffLayout.auto, // auto, sideBySide, unified, stacked sideBySideBreakpoint: 768.0, // px width threshold for auto layout // Features showHeader: true, showLineNumbers: true, showIndicators: true, // +/- indicator column showSummary: true, // additions/deletions count showChangeNavigation: true, enableTextSelection: true, synchronizedScrolling: true, // sync side-by-side panels collapseUnchangedLines: true, // Comparison granularity: DiffGranularity.word, // line, word, character, auto ignoreWhitespace: false, caseSensitive: true, contextLines: 3, // unchanged lines around each change useIsolateForLargeDocuments: true, // compute() for >1000 lines // Styling theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), )
FlutterDiffViewerTheme.light() // GitHub light style FlutterDiffViewerTheme.dark() // GitHub dark style
FlutterDiffViewer( oldContent: oldText, newContent: newText, theme: FlutterDiffViewerTheme.light().copyWith( addedBackgroundColor: Color(0xFFE8F5E9), removedBackgroundColor: Color(0xFFEDE7F6), addedTextColor: Color(0xFF2E7D32), removedTextColor: Color(0xFF6A1B9A), ), )
// Word-level highlighting FlutterDiffViewer( oldContent: 'The quick brown fox', newContent: 'The fast red cat', configuration: FlutterDiffViewerConfiguration( granularity: DiffGranularity.word, theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), ), ) // Character-level highlighting (maximum precision) FlutterDiffViewer( oldContent: 'colour', newContent: 'color', configuration: FlutterDiffViewerConfiguration( granularity: DiffGranularity.character, theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), ), )
Use FlutterDiffViewerController for programmatic navigation:
final controller = FlutterDiffViewerController(); // In your widget tree FlutterDiffViewer( oldContent: oldText, newContent: newText, controller: controller, ) // Navigate programmatically controller.nextChange(); controller.previousChange(); controller.goToChange(2); controller.scrollToLine(50); // Collapse/expand controller.collapseAll(); controller.expandAll(); // Read state print('${controller.currentChangeIndex} of ${controller.totalChanges}'); // Always dispose if you create it yourself @override void dispose() { controller.dispose(); super.dispose(); }
Every UI section can be replaced with your own widget:
FlutterDiffViewer( oldContent: oldText, newContent: newText, // Custom header headerBuilder: (context, oldLabel, newLabel, config) { return MyCustomHeader(old: oldLabel, new_: newLabel); }, // Custom line renderer lineBuilder: (context, line, config) { return MyCustomDiffLine(line: line); }, // Custom collapsed section collapsedSectionBuilder: (context, count, onExpand, config) { return TextButton( onPressed: onExpand, child: Text('Show $count hidden lines'), ); }, // Custom empty state emptyStateBuilder: (context, config) { return const Center(child: Text('✅ Files are identical')); }, // Custom error state errorBuilder: (context, error, config) { return Center(child: Text('Error: $error')); }, )
Replace the built-in Myers LCS engine with your own:
class MyDiffEngine implements DiffEngine { @override DiffResult compare( String oldContent, String newContent, DiffComparisonOptions options, ) { // Your custom algorithm here return DiffResult(...); } } FlutterDiffViewer( oldContent: oldText, newContent: newText, diffEngine: MyDiffEngine(), )
Provide custom strings for any language:
FlutterDiffViewer( oldContent: oldText, newContent: newText, configuration: FlutterDiffViewerConfiguration( theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations( oldVersionLabel: 'Aktuell', newVersionLabel: 'Geändert', addedLabel: 'Hinzugefügt', removedLabel: 'Entfernt', noChangesLabel: 'Keine Änderungen', nextChangeLabel: 'Nächste Änderung', previousChangeLabel: 'Vorherige Änderung', showMoreLabel: 'Zeile anzeigen', collapseLabel: 'Einklappen', loadingLabel: 'Berechnung…', errorLabel: 'Fehler beim Berechnen', additionsCountLabel: 'Hinzufügungen', deletionsCountLabel: 'Löschungen', modifiedLabel: 'Geändert', unchangedLabel: 'Unverändert', changeOfLabel: 'von', ), ), )
DiffLayout.auto automatically selects:
- Side-by-side when
availableWidth >= sideBySideBreakpoint(default: 768px) - Unified on narrower screens
Override the breakpoint:
FlutterDiffViewerConfiguration( layout: DiffLayout.auto, sideBySideBreakpoint: 600.0, // switch at 600px instead theme: FlutterDiffViewerTheme.light(), typography: DiffTypography.defaults(), spacing: DiffSpacing.defaults(), localizations: DiffLocalizations.defaults(), )
The package never relies solely on color to convey meaning:
- Change indicators:
+,-,~,(in addition to color) - Semantic labels on every line: "Added: content", "Removed: content"
- Screen reader support via Flutter's Semantics framework
- All interactive elements have tooltips
- Uses
ListView.builder— only visible rows are rendered - Diff calculation runs asynchronously (
Future-based) - For documents >1000 lines with
useIsolateForLargeDocuments: true, calculation runs in a separate isolate viacompute() - Staged diff: line-level first, then word/character only for changed lines
Measured: 500-line documents calculate in <20ms on average hardware. Performance with very large documents (50,000+ lines) depends on the device and number of changes.
This package is built with Clean Architecture:
Presentation → Domain ← Data
(pure Dart)
- Domain: Entities, use cases, repository interfaces — zero Flutter/Material deps
- Data: Diff engines, repository implementations, models
- Presentation: Widgets, controller, themes, builders
See the pub.dev documentation for the full API reference.
| Class | Description |
|---|---|
FlutterDiffViewer |
Main widget — the package entry point |
FlutterDiffViewerConfiguration |
Immutable configuration object |
FlutterDiffViewerTheme |
Color theme with light/dark factories |
DiffTypography |
Text style customization |
DiffSpacing |
Layout spacing customization |
DiffLocalizations |
User-visible string customization |
FlutterDiffViewerController |
Programmatic navigation and scroll control |
DiffEngine |
Abstract interface for custom diff algorithms |
DiffResult |
The output of a diff calculation |
DiffLine |
A single row in the diff view |
DiffSegment |
An inline highlighted text segment |
- Syntax highlighting for code files
- Image diff support
- Inline comment support (GitHub PR review style)
- Export diff as HTML/PDF
- Web worker support for Flutter Web
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Ensure all tests pass (
flutter test) - Submit a pull request
This package is licensed under the MIT License.