flutter_interactive_text 0.2.1
flutter_interactive_text: ^0.2.1 copied to clipboard
A performant Flutter widget for detecting, styling, interacting with, and optionally expanding rich text patterns.
flutter_interactive_text #
A performant, theme-aware Flutter widget for detecting, styling, and handling
interactive text. The same InteractiveText widget can render ordinary,
limited, selectable, or expandable content without requiring an editing
controller or a separate text model.
Features #
- One widget for static and expandable interactive text.
- Built-in URLs, deep links, email addresses, phone numbers, mentions, hashtags, cashtags, emoji, commands, references, dates, times, currency, percentages, UUIDs, IPv4 addresses, variables, and lightweight Markdown.
- Stable
20font size for complete emoji graphemes in both mixed and emoji-only text. - Theme-aware defaults and a
TextStyleoverride for every built-in type. - Typed tap callbacks and automatic URL, email, and phone launching.
- Optional domain allowlist for user-generated links.
- Custom regex, trigger, emoji, or function-based detectors.
- Grapheme-safe expand and collapse behavior.
- Optional selectable text.
- Cached parsing, spans, recognizers, and line measurements for list usage.
Installation #
Add the package to pubspec.yaml:
dependencies:
flutter_interactive_text: ^0.2.1
Basic usage #
The default configuration recognizes common social and messaging content:
import 'package:flutter_interactive_text/flutter_interactive_text.dart';
InteractiveText(
'Write to hello@example.com or visit example.org. Hi @maria #Flutter ππ½',
)
URLs, email addresses, and phone numbers launch automatically. Supply onTap
to customize navigation or handle another action:
InteractiveText(
'Explore #Flutter with @maria',
onTap: (match) {
if (match.type == InteractiveTextType.hashtag) {
openSearch(match.text);
} else if (match.type == InteractiveTextType.mention) {
openProfile(match.text.substring(1));
}
},
)
Expandable text #
Expansion is opt-in and remains part of the same widget:
InteractiveText(
longDescription,
expansion: InteractiveTextExpansion(
collapsedLines: 3,
expandText: 'More',
collapseText: 'Less',
onChanged: (expanded) => analytics.track(expanded),
),
)
The collapsed value is measured with the effective theme, locale, text scaler, strut, alignment, and available width. Truncation only occurs at complete Unicode grapheme boundaries.
Markdown display #
Enable the Markdown group to render lightweight inline formatting:
InteractiveText(
'**Important** _note_ ~~old~~ ||spoiler|| `code` [Flutter](https://flutter.cn)',
types: InteractiveTextGroups.combine([
InteractiveTextGroups.chat,
InteractiveTextGroups.markdown,
]),
)
Recognized formatting delimiters are hidden by default. Bold accepts **text**
and __text__; italic accepts _text_ and *text*. Markdown links display only
their label while keeping the destination for navigation and domain checks.
Inline code stays literal inside its backticks. Escaped and incomplete syntax
is not treated as formatting. Meaningful token prefixes such as @, #, $,
and template braces are retained. Formatting-like punctuation inside detected
URLs, email addresses, handles, hashtags, and variables stays literal.
showMarkdownSyntax: true restores the source-style presentation. Tap callbacks
always receive the original match.text and source match.range; use
match.displayText or match.contentRange for the visible portion. Selectable
text copies the rendered text, and expansion measures visible spans, not hidden
markers or link destinations. Formatting remains lightweight and follows the
parser's non-overlapping priority rules, not a full CommonMark/block renderer.
Choose detected types #
InteractiveTextGroups.common is enabled by default. More focused and advanced
presets are available:
InteractiveText(
message,
types: InteractiveTextGroups.chat,
)
InteractiveText(
assistantResponse,
types: InteractiveTextGroups.assistantChat,
)
InteractiveText(
technicalDetails,
types: InteractiveTextGroups.combine([
InteractiveTextGroups.links,
InteractiveTextGroups.identifiers,
InteractiveTextGroups.markdown,
]),
)
Available groups include links, contact, social, chat, common,
commerce, temporal, identifiers, markdown, assistantChat, and all.
Styling #
Every built-in type has an independent TextStyle. Default token colors use a
stable light/dark palette rather than the primary, secondary, or
tertiary brand colors. Surfaces and ordinary/code text still use ColorScheme.
| Tokens | Light | Dark |
|---|---|---|
| Links, email, phone, mentions, hashtags, references | #1565C0 |
#90CAF9 |
| Cashtags, currency, percentages | #2E7D32 |
#A5D6A7 |
| Dates, times | #00695C |
#80CBC4 |
| Commands, template variables | #6A1B9A |
#CE93D8 |
Blue follows the familiar link convention.
Other category colors are package defaults, not universal standards. Links and
contact details are underlined; mentions and hashtags use additional weight.
Check contrast on custom backgrounds. All colors,
decorations, and font sizes remain overridable; InteractiveTextStyles.unstyled
disables package defaults entirely.
InteractiveText(
content,
style: Theme.of(context).textTheme.bodyLarge,
styles: const InteractiveTextStyles(
mention: TextStyle(fontWeight: FontWeight.w700),
hashtag: TextStyle(color: Colors.teal),
emoji: TextStyle(fontSize: 24),
emojiOnly: TextStyle(fontSize: 24),
expansion: TextStyle(fontWeight: FontWeight.w700),
),
)
Use styleResolver when the style depends on match metadata or source value:
InteractiveText(
content,
styleResolver: (details) {
if (details.match.type == InteractiveTextType.mention &&
details.match.text == '@admin') {
return details.suggestedStyle.copyWith(color: Colors.red);
}
return details.suggestedStyle;
},
)
Link policy #
The package does not define a domain allowlist. A null or empty
allowedDomains collection permits every web host. To make only selected
domains interactive, provide their host names through allowedDomains:
InteractiveText(
userGeneratedContent,
allowedDomains: const ['example.com', 'example.org'],
)
Subdomains are included. Disallowed URLs remain visible and styled but do not receive a recognizer.
Custom detection #
InteractiveTextType is a value object rather than a closed enum. Define a
type and detector, then provide an optional custom style:
const orderType = InteractiveTextType('order');
final orderDetector = RegexInteractiveTextDetector(
type: orderType,
id: 'order',
pattern: RegExp(r'ORDER-\d+'),
priority: 100,
);
InteractiveText(
'Track ORDER-2048',
detectors: [orderDetector],
onTap: (match) => openOrder(match.text),
styles: InteractiveTextStyles(
custom: {
orderType: const TextStyle(
color: Colors.indigo,
fontWeight: FontWeight.w700,
),
},
),
)
For non-regex requirements, use FunctionInteractiveTextDetector. Trigger
syntax can use TriggerInteractiveTextDetector, and its character and boundary
policies are fully configurable.
Interaction rules #
- Without
onTap, supported links and contact values launch automatically. - With
onTap, the callback owns the action for non-formatting matches. interactiveTypescan explicitly include or exclude actionable types.- Emoji and formatting-only ranges are not tappable by default.
- Set
autoLaunch: falseto render styles without automatic actions.
Performance #
Each mounted widget parses only when its text or parser changes. It reuses tap
recognizers and caches built spans and collapsed measurements across ordinary
rebuilds. Built-in regular expressions are also shared. For repeated custom
rules, create one InteractiveTextParser and pass it to every list item.
See example/lib/main.dart for a runnable example.