flutter_md 0.3.1
flutter_md: ^0.3.1 copied to clipboard
Fast Markdown parser and canvas renderer for Flutter with cross-widget text selection, streaming parsing and syntax highlighting.
flutter_md - Markdown Parser and Renderer for Flutter #
A high-performance, lightweight Markdown parser and renderer specifically designed for Flutter applications. Perfect for displaying formatted text from AI assistants like ChatGPT, Gemini, and other LLMs.
š® Live Demo #
Open the demo. It is the example app built for the web, so it runs in the browser with nothing to install.
- Editor: type Markdown and see it rendered next to the source
- Selection: select and copy text across blocks
- Chat: selection across messages, and a reply streamed token by token
- Highlight: syntax highlighting of code blocks
š Features #
- š High Performance: Hand-tuned single-pass parser with minimal allocations
- šØ Fully Customizable: Theme-based styling with complete control over appearance
- š± Flutter Native: Built from the ground up for Flutter with custom render objects
- š Interactive Elements: Clickable links with customizable tap handlers
- āļø Text Selection: Cross-block and cross-widget (chat) selection via a controller that survives list disposal and streaming updates
- š Streaming Parser:
StreamingMarkdownParserparses LLM token output incrementally ā completed blocks are frozen, only the live tail re-parses - š Cross Platform: Works on all Flutter-supported platforms
- š GitHub Flavored: Alerts (
> [!NOTE]), task lists (- [x]), tables with column alignment, thematic breaks, strikethrough, and more - š§® Inline Math: Opt-in
$...$LaTeX ā Unicode (commands + super/subscripts) - š Syntax Highlighting: Opt-in, dependency-free code-block highlighting ā 65+ languages and GitHub light/dark themes; only the languages you import are bundled (the rest tree-shake away)
- šÆ AI-Optimized: Specifically designed for AI-generated content display
- š§ Extensible: Easy to extend with custom block and span renderers
- ā Well Tested: 1,700+ tests; ~97% line coverage (parser and node model ~100%)
š Supported Markdown Syntax #
Text Formatting #
- Bold:
**text** - Underline:
__text__ - Italic:
*text*or_text_ Strikethrough:~~text~~Inline code:`code`- ==Highlight==:
==text== - ||Spoiler||:
||text|| - Inline math (opt-in):
$\alpha$,$\pi \approx 3.14$,$x^2$,$H_2O$(common LaTeX commands + super/subscripts ā Unicode)
Emphasis follows CommonMark-inspired flanking rules, so stray markers
(5 * 6 = 30), intraword underscores (snake_case), and unterminated markers
(**oops) are left as literal text instead of leaking styles.
Inline math (opt-in)
Inline $...$ LaTeX math is disabled by default (so prices like $5 and
shell variables like $HOME are never altered). Enable it per parse or per
decoder:
// Per parse:
final md = Markdown.fromString(r'The angle $\alpha$ and $x^2 + y^2$.',
inlineMath: true);
// Or a reusable decoder, optionally extending the command table:
const decoder = MarkdownDecoder(
inlineMath: true,
mathReplacements: {...kMarkdownMathCommands, r'\R': 'ā'},
);
It converts LaTeX commands (\alpha, \rightarrow, ...) and super/subscripts
(x^2, H_2O, x^{10}), is code-span and code-block safe, and treats \$ as
a literal dollar. Write \$\alpha\$ to keep a literal $\alpha$.
Headers #
# H1 Header
## H2 Header
### H3 Header
#### H4 Header
##### H5 Header
###### H6 Header
Lists #
- Unordered list item
- Another item
- Nested item
- Deep nested item
1. Ordered list item
2. Another numbered item
1. Nested numbered item
2. Another nested item
- [x] Completed task-list item
- [ ] Pending task-list item
Task-list state is exposed on MD$ListItem.checked (true/false/null) and
MD$ListItem.isTask, and rendered as a checkbox.
Blockquotes #
> This is a blockquote
> It can span multiple lines
>
> And have multiple paragraphs
Alerts (Admonitions) #
GitHub-style alerts are rendered from blockquotes with a type marker:
> [!NOTE]
> Highlights information that users should take into account.
> [!TIP]
> Optional information to help a user be more successful.
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
> [!WARNING]
> Critical content demanding immediate user attention.
> [!CAUTION]
> Negative potential consequences of an action.
Each alert becomes an MD$Alert block (MD$AlertType.note, .tip,
.important, .warning, .caution). Per-type accent colors are configurable
via MarkdownThemeData.alertColors / alertColorFor.
Code Blocks #
```dart
void main() {
print('Hello, Markdown!');
}
```
Code blocks can be syntax-highlighted ā see Syntax Highlighting.
Tables #
Column alignment is supported via the delimiter row (:--- left, :--:
center, ---: right):
| Left | Center | Right |
| :------- | :------: | -----: |
| Cell 1 | Cell 2 | Cell 3 |
| **Bold** | _Italic_ | `Code` |
Links and Images #
[Link text](https://example.com)

Images render as their alt text unless the theme has an imageResolver ā see
Images & link icons.
Horizontal Rules #
Any of ---, ***, or ___ (optionally spaced, e.g. - - -) produce a rule:
---
---
---
š Quick Start #
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
flutter_md: ^x.x.x # Replace with the latest version
Then run:
flutter pub get
Upgrading from 0.0.x? See the Migration guide: 0.0.x ā 0.2.x. The upgrade is almost entirely backward compatible ā most apps need no code changes.
Upgrading from 0.2.x? See the Migration guide: 0.2.x ā 0.3.x. Only custom painters, selection surfaces and quote / alert model readers need changes.
Example #
Parse the text once and pass the model to MarkdownWidget:
import 'package:flutter/material.dart';
import 'package:flutter_md/flutter_md.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
static final Markdown _markdown = Markdown.fromString('''
# Hello, **flutter_md**
A paragraph with *emphasis*, `inline code` and a
[link](https://pub.dev/packages/flutter_md).
- [x] Parse
- [x] Render
- [ ] Ship
''');
@override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: MarkdownWidget(markdown: _markdown),
),
),
),
);
}
Selection, streaming, highlighting and theming are covered below. See them working in the live demo, or run the example app from a clone of this repository:
cd example
flutter run
āļø Text Selection #
Selection is anchored on the immutable Markdown model, not on the render
objects, so it spans multiple blocks (heading ā paragraph ā list ā table cell)
and multiple MarkdownWidgets (e.g. chat messages), and it survives widgets
being scrolled off-screen and disposed. Wrap a group of widgets in a
MarkdownSelectionScope, give each a stable documentId, and register the
models with the controller:
final controller = MarkdownSelectionController();
// Register the documents in reading order (a chat feeds this from its list).
controller.setDocuments([
for (final (i, m) in messages.indexed)
MarkdownDocumentRef(id: m.id, model: m.markdown, order: i),
]);
MarkdownSelectionScope(
controller: controller,
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, i) => MarkdownWidget(
documentId: messages[i].id,
markdown: messages[i].markdown,
),
),
);
// Any time ā even for messages scrolled off-screen:
final String text = controller.getText(); // default formatter
final String md = controller.getText(const MarkdownMarkupFormatter()); // as Markdown
final MarkdownSelectedContent structured = controller.selectedContent();
- Every block is selectable. Paragraphs, headings, quotes, code, alerts,
lists and tables ā a drag can start or end inside a list item or table cell,
and copied text preserves the list
\n/ table\tseparators. - Get the text your way.
getText()uses the defaultMarkdownPlainTextFormatter(configurable block/document separators). For richer output pass the built-inMarkdownMarkupFormatter("Copy as Markdown"): it re-emits heading#s, nested list markers with task checkboxes, blockquote/alert>prefixes, fenced code and pipe tables for fully-selected blocks (partially-selected edges fall back to plain text). Or implement your ownMarkdownSelectionFormatter.selectedContent()returns the structured per-document / per-block result each formatter consumes. - Streaming stays anchored. Call
controller.putDocument(id, newModel)when a message grows; the defaultMarkdownReconciliationPolicy.contentAnchoredkeeps the selection (append fast-path, else relocate by content, else clamp). Prefer unique reading-orderordervalues.removeDocumentwaits for surface detach when the body is still mounted; a remountputDocumentcancels a pending remove. - One selection at a time. Share a
MarkdownSelectionGroupbetween controllers so selecting in one clears the others; callgroup.clearExternal()when a plainSelectableText/SelectionAreastarts its own selection. - Gestures. A mouse/trackpad/stylus drag selects; on touch a long-press-then-drag selects (so a plain swipe still scrolls the list).
- Native handles, magnifier & toolbar. On touch platforms the selection shows draggable handles and a magnifier (they follow the content as it scrolls, even across widgets); right-click (desktop) or long-press (mobile) shows an adaptive Copy / Select-all toolbar.
- Keyboard shortcuts. When the scope is focused:
Ctrl/Cmd+Ccopies,Ctrl/Cmd+Aselects all,Shift+arrows extend (character / word / line / document, plus vertical),Escclears ā using the ambientDefaultTextEditingShortcuts. - Customizable like
SelectableText.MarkdownSelectionScopetakesselectionColor,contextMenuBuilder,magnifierConfiguration,selectionControls,focusNode,enabledandonSelectionChanged; its publicMarkdownSelectionScopeStateexposescopySelection/selectAll/clearSelection/showToolbar/contextMenuButtonItems/contextMenuAnchorsfor a fully custom menu. - Host gates for embedded mounts. A chat that owns its own taps and swipes
can hand back the pieces it needs:
enableTouchGestures: falseleaves every touch entry to the host (mouse, handles, toolbar and keyboard stay live),enableTouchConsecutiveTaps: falsekeeps only long-press ā word ā drag,canStartSelectionAtrefuses selection starts on chrome (links, code headers) without claiming the pointer, andownsSelectionChromedecides which scope paints handles and toolbar when several share one controller. - Edge autoscroll, any scroll protocol. Dragging into the viewport edge
scrolls the host (
autoscroll:, defaultedgeZone: 48). It drives the nearest ancestorScrollableout of the box, and anything else throughMarkdownSelectionAutoscrollConfig.targetResolverā an anchored chat viewport, aRenderBoxthat positions children itself, a transform canvas. - Opt-in & compatible. A
MarkdownWidgetwith nodocumentId/controller is inert ā existing usage is unchanged.
MarkdownSelectionScope(
controller: controller,
selectionColor: Colors.amber.withValues(alpha: 0.3),
onSelectionChanged: (sel) => debugPrint('selection: $sel'),
contextMenuBuilder: (context, state) => AdaptiveTextSelectionToolbar.buttonItems(
anchors: state.contextMenuAnchors,
buttonItems: [
...state.contextMenuButtonItems, // Copy, Select all
ContextMenuButtonItem(
label: 'Copy LOUD',
onPressed: () => Clipboard.setData(
ClipboardData(text: state.controller.getText().toUpperCase())),
),
],
),
child: /* ... */,
);
Driving a custom scroll implementation ā the whole surface is three members
(viewport, canScroll, applyScrollDelta), and deltas are screen-space
content movement, so a reverse axis or an inverted anchor is the adapter's
problem, not the library's:
MarkdownSelectionScope(
controller: controller,
autoscroll: MarkdownSelectionAutoscrollConfig(
targetResolver: (request) => MarkdownCallbackAutoscrollTarget(
viewportOf: () {
final box = viewportKey.currentContext?.findRenderObject();
if (box is! RenderBox || !box.hasSize) return null;
return MarkdownAutoscrollViewport(
globalBounds: box.localToGlobal(Offset.zero) & box.size,
padding: const EdgeInsets.only(bottom: 72), // composer overlaps
);
},
canScrollAt: ({required forward}) =>
forward ? !chat.isAtTail.value : !chat.reachedOldest,
// This host's scrollBy is anchor-relative: positive reveals *older*
// messages, the opposite of screen-space movement.
onScrollDelta: (delta) {
chat.scrollBy(-delta);
return delta;
},
),
),
child: /* ... */,
);
See the runnable Selection and Chat tabs in example/.
š Streaming (LLM output) #
LLM replies arrive token by token. Re-parsing the whole accumulated buffer on
every token is O(N²) and janks long messages. StreamingMarkdownParser keeps
the accumulated source and only re-parses the still-growing tail: once a
block is provably complete (terminated by a blank line, and not inside an open
code fence) it is frozen and never parsed again.
final parser = StreamingMarkdownParser();
llmTokenStream.listen((token) {
final Markdown md = parser.add(token); // cheap, incremental
setState(() => _message = md);
});
Or transform a Stream<String> directly ā each event emits the grown document:
llmTokenStream
.toMarkdown() // Stream<String> ā Stream<Markdown>
.listen((md) => setState(() => _message = md));
- Exact, never approximate. The result of
add()/currentis always identical, block for block, toMarkdown.fromString(everythingReceivedSoFar). Blocks whose type still depends on input that hasn't arrived ā an unterminated code fence, a table header still missing its delimiter row, a list that may continue ā stay in the live tail and are re-evaluated, so they never freeze into the wrong shape. - Same options as batch. Pass a configured decoder to match
Markdown.fromString:StreamingMarkdownParser(decoder: const MarkdownDecoder(inlineMath: true))(or.toMarkdown(decoder: ...)). - Fast. Replaying a whole message token-by-token is 3ā14Ć faster than
the full-reparse approach, and the gap grows with message length
(
benchmark/streaming_benchmark.dart). Rendering keeps up: a repaint lays out only the blocks that changed, so a chunk of an 8 KB document takes about 0.3 ms on an iPad Pro and 0.7 ms on a Galaxy S22 Ultra (numbers). - Reusable.
parser.reset()clears state for the next message;parser.sourceis the raw text accumulated so far,parser.currentthe parsed model without adding anything.
Pair it with selection: feed each grown model to controller.putDocument(id, md)
and the active selection stays anchored as the message streams in (see the
Chat tab in example/).
š Syntax Highlighting #
Fenced code blocks can be syntax-highlighted with a tree-shakeable,
dependency-free highlighter ā 65+ languages and ready-made GitHub light/dark
themes. Highlighting is opt-in and off by default (code renders as plain
monospace): assign a SyntaxHighlighter to MarkdownThemeData.highlighter.
import 'package:flutter_md/highlight.dart'; // engine + MarkdownHighlighter
import 'package:flutter_md/highlight/dart.dart'; // one import per language
import 'package:flutter_md/highlight/sql.dart';
import 'package:flutter_md/highlight/themes.dart'; // HighlightThemes
final theme = MarkdownThemeData.mergeTheme(
Theme.of(context),
highlighter: MarkdownHighlighter(
// You assemble the map, so only these grammars are compiled in.
languages: {
'dart': HighlightDart.grammar,
'sql': HighlightSql.grammar,
},
theme: isDark ? HighlightThemes.githubDark : HighlightThemes.githubLight,
),
);
MarkdownWidget(markdown: doc, theme: theme);
- Only the languages you import ship. Each language is its own library
exposing a single grammar, referenced through lazy thunks with no central
registry/
Map/enum. Importinghighlight/dart.dartnever references any other language, so unused grammars are removed by tree-shaking ā a Dart-only app adds ~0 beyond the engine, and all 65 languages together add ~62 KB (gzipped) only if you deliberately bundle them all. - Selection- and copy-safe. The highlighter only partitions the source into colored spans; the concatenated text is byte-identical, so cross-block selection, offsets and "Copy as Markdown" stay aligned.
- Themes. Use
HighlightThemes.githubDark/githubLight(diffs included: inserted / deleted lines in green / red), or implementCodeHighlightTheme(aswitchfrom token type āTextStyle) for your own palette. Itsbackgroundis applied to the code-block surface. - Prism semantics. Rules run in grammar order and
greedyrules match across earlier tokens, as in Prism ā so"a # b"stays a string and"#{name}"interpolation is highlighted. - Demos & tooling.
import 'package:flutter_md/highlight/all.dart';exposesallHighlightLanguages, a ready map of every grammar keyed by tag and alias (js,ts,sh, ā¦). It references everything, so unused languages can no longer be tree-shaken away ā use it for demos, not production. - Robust. Unknown languages fall back to plain text; a pattern Dart's regex engine rejects disables just that one rule instead of breaking the language.
The grammars are generated by tool/highlight_codegen (see its README to
regenerate or add languages). See the runnable Highlight tab in example/.
Syntax grammars are adapted from Prism (MIT License).
// Custom theme: map token types to styles.
final class MyCodeTheme implements CodeHighlightTheme {
const MyCodeTheme();
@override
Color? get background => const Color(0xFF1E1E1E);
@override
Color? get foreground => const Color(0xFFD4D4D4);
@override
TextStyle? styleFor(String tokenType) => switch (tokenType) {
'comment' => const TextStyle(color: Color(0xFF6A9955)),
'keyword' => const TextStyle(color: Color(0xFF569CD6)),
'string' || 'string-literal' => const TextStyle(color: Color(0xFFCE9178)),
_ => null,
};
}
šØ Customization #
Theme Configuration #
MarkdownTheme(
data: MarkdownThemeData(
textStyle: TextStyle(fontSize: 16.0, color: Colors.black87),
h1Style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
h2Style: TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.bold,
color: Colors.blueGrey,
),
quoteStyle: TextStyle(
fontSize: 14.0,
fontStyle: FontStyle.italic,
color: Colors.grey[600],
),
// Customize link text styling (merged on top of linkColor)
linkStyle: const TextStyle(
decoration: TextDecoration.underline,
),
// Monospace face for inline `code` AND fenced blocks. Leave it unset to
// get a family the host platform can actually resolve: `monospace` on
// Android / Linux / web, Menlo on iOS + macOS, Consolas on Windows
// (CoreText and DirectWrite do not know the CSS generic, so code would
// otherwise render in the proportional body face there).
monospaceFontFamily: 'JetBrains Mono',
// Per-type accent colors for GitHub alert blocks
alertColors: const {
MD$AlertType.warning: Color(0xFF9A6700),
},
// Handle link taps
onLinkTap: (title, url) {
print('Tapped link: $title -> $url');
// Launch URL or navigate
},
// Filter blocks (e.g., hide code blocks)
blockFilter: (block) => block is! MD$Code,
// Filter spans (e.g., exclude images or certain styles)
spanFilter: (span) => !span.style.contains(MD$Style.image),
),
child: MarkdownWidget(
markdown: yourMarkdown,
),
)
Or you can use the MarkdownThemeData.mergeTheme(Theme.of(context)) factory to create a theme that inherits from the application's theme.
This approach allows you to easily support both light and dark themes, and keeps your markdown styling consistent with the rest of your application.
Images & link icons #
Pictures are opt-in, so rendering untrusted Markdown never fetches anything
by itself. Install a resolver to show  images and, optionally, a
favicon in front of every link:
MarkdownThemeData(
textStyle: const TextStyle(fontSize: 14),
imageResolver: const MarkdownProviderImageResolver(
linkIcons: true, // favicon before each http(s) link
maxImageHeight: 320,
).call,
)
Create the resolver once (a const, a field or a top-level final), not
inside build: a different resolver drops and reloads every picture.
MarkdownProviderImageResolver loads through Flutter ImageProviders, so
swapping the source or adding a cache is one function:
MarkdownProviderImageResolver(
baseUri: Uri.parse('https://example.com/docs/'), // relative sources
imageProvider: (uri) => switch (uri.scheme) {
'file' => FileImage(File(uri.toFilePath())),
'http' || 'https' => CachedNetworkImageProvider(uri.toString()),
_ => null, // keep the alt text
},
faviconUri: (link) =>
Uri.parse('https://icons.duckduckgo.com/ip3/${link.host}.ico'),
).call
To control how a picture looks or loads, implement MarkdownImage ā it
reports the size of its slot in the line, paints itself into it, and calls
notifyListeners() when it changes ā and return it from your own
MarkdownImageResolver:
class Badge extends MarkdownImage {
@override
Size layout(MarkdownImageConstraints c) => Size.square(c.fontSize);
@override
void paint(Canvas canvas, Rect rect, MarkdownImageConstraints c) =>
canvas.drawCircle(rect.center, rect.shortestSide / 2,
Paint()..color = Colors.teal);
}
// A top-level function is a stable resolver.
MarkdownImage? badges(MarkdownImageRequest request) =>
request.kind == MarkdownImageKind.linkIcon ? Badge() : null;
MarkdownThemeData(
textStyle: const TextStyle(fontSize: 14),
imageResolver: badges,
)
Return a new MarkdownImage from every call (the renderer owns and disposes
it), or null to keep the alt text. A resolver that throws is reported via
FlutterError.reportError and that image shows its alt text.
Selection treats a picture as its alt text (a link icon as nothing), so copied
text is the same with or without pictures. Building text yourself with
paragraphFromMarkdownSpans outside a MarkdownWidget? Lay it out and paint
it with layoutMarkdownText / paintMarkdownText, and dispose the
MarkdownImageSpan.images you get ā no widget owns them.
Custom Block Painters #
For advanced customization, you can provide custom block painters:
MarkdownThemeData(
builder: (block, theme) {
if (block is MD$Code && block.language == 'dart') {
// Return custom painter for Dart code blocks
return CustomDartCodePainter(block: block, theme: theme);
}
return null; // Use default painter
},
)
Performance Optimization #
A document too long to parse within a frame can be parsed in slices, pausing for the UI between them:
final markdown = await Markdown.fromStringAsync(veryLongText);
For large markdown documents or frequently changing content:
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final Markdown _markdown;
@override
void initState() {
super.initState();
// Parse markdown once during initialization
_markdown = Markdown.fromString(yourMarkdownString);
}
@override
Widget build(BuildContext context) {
return MarkdownWidget(markdown: _markdown);
}
}
š Performance #
flutter_md is built for speed: a single-pass, lookup-table parser (regex-free
hot path with a plain text fast path) and a custom render object that lays the
whole document into one cached ui.Picture instead of a deep tree of per-block
widgets.
Head-to-head against flutter_markdown and gpt_markdown with identical
styles, on an iPad Pro (M2), a Galaxy S22 Ultra and a MacBook Pro (M3 Max) ā devices,
versions, full tables and method in docs/benchmarks.md:
- Parsing ā ~27Ć faster than the
markdownpackage that backsflutter_markdown(gpt_markdownhas no standalone parser), sustaining ~75 MB/s on mixed documents. - Rendering ā on a large document the first frame builds ~2Ć faster on
real devices (~6Ć and ~7.5Ć in the headless
flutter testbenchmark). Small chat bubbles are roughly even. - Streaming ā a chunk of a growing 8 KB document costs 0.3 ms at the median on the iPad and 0.7 ms on the phone, because unchanged blocks keep their laid-out text.
- Scrolling ā no frame over budget on any of the three devices, and the lowest UI-thread build cost of the three libraries (99th percentile 3.3 ms vs 6.3 / 5.6 ms on the iPad).
# In-repo parser micro-benchmarks:
dart run benchmark/parser_benchmark.dart # multi-scenario, vs. `markdown`
dart run benchmark/compare.dart --save # low-noise before/after tool
# Head-to-head vs flutter_markdown & gpt_markdown (parser + render):
cd benchmark_compare && flutter pub get
dart run benchmark/parser_benchmark.dart
flutter test test/render_benchmark_test.dart
š§ Advanced Features #
Custom Styles #
// Access individual style components
final span = MD$Span(
text: 'Custom text',
style: MD$Style.bold | MD$Style.italic, // Combine styles
);
// Check for specific styles
if (span.style.contains(MD$Style.link)) {
// Handle link styling
}
Block Filtering #
MarkdownThemeData(
blockFilter: (block) {
// Only show paragraphs and headers
return block is MD$Paragraph || block is MD$Heading;
},
)
Span Filtering #
MarkdownThemeData(
spanFilter: (span) {
// Exclude images and spoilers
return !span.style.contains(MD$Style.image) &&
!span.style.contains(MD$Style.spoiler);
},
)
š± Platform Support #
- ā Android
- ā iOS
- ā Web
- ā Windows
- ā macOS
- ā Linux
š¤ Contributing #
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Development Setup #
git clone https://github.com/DoctorinaAI/md.git md
cd md
flutter pub get
flutter test
Running the Example #
cd example
flutter run
š License #
This project is licensed under the MIT License - see the LICENSE file for details.
š Links #
š” Why Choose md? #
Unlike other Markdown packages that rely on HTML rendering or web views, flutter_md is built specifically for Flutter using custom render objects. This provides:
- Better Performance: No HTML parsing or web view overhead
- Native Feel: Fully integrated with Flutter's rendering pipeline
- Customization: Complete control over styling and behavior
- Reliability: Consistent rendering across all platforms
- Small Size: Minimal package size with no external dependencies
Perfect for chat applications, documentation viewers, note-taking apps, and any Flutter application that needs to display rich formatted text from AI assistants or user input.
