flutter_interactive_text 0.1.1
flutter_interactive_text: ^0.1.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 application-specific 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
22font 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.1.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
when the application should own navigation or 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.
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. Defaults use the current
ColorScheme, so they react to light, dark, and custom application themes.
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 from the host application:
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, so applications are not limited to an
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.