[go: up one dir, main page]

flutter_interactive_text 0.2.3 copy "flutter_interactive_text: ^0.2.3" to clipboard
flutter_interactive_text: ^0.2.3 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 20 font size for complete emoji graphemes in both mixed and emoji-only text.
  • Theme-aware defaults and a TextStyle override for every built-in type.
  • Typed tap callbacks and automatic URL, email, and phone launching.
  • Optional domain allowlist for user-generated links.
  • Optional per-type value allowlists for mentions, commands, and custom tokens.
  • 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.3

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.dev)',
  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.

Allowed values #

Use allowedValues when a recognized type is valid only for a known set of values. A disallowed value remains ordinary text: it receives no token style, tap recognizer, or callback.

InteractiveText(
  'Ask @maria or @unknown, then try /help.',
  types: const [
    InteractiveTextType.mention,
    InteractiveTextType.command,
  ],
  allowedValues: {
    InteractiveTextType.mention: const ['maria'],
    InteractiveTextType.command: const ['help'],
  },
)

The trigger prefix is optional in allowlists for mentions, hashtags, cashtags, and commands, and those built-in values are matched case-insensitively. Other types use trimmed, exact matching. A null map, a missing type, or an empty collection leaves that type unrestricted. The same option is available on InteractiveTextParser; a per-call value passed to parse overrides the parser-level policy.

Use an explicit observable policy when valid values arrive asynchronously. A restricted type with no values remains ordinary, non-interactive text until a result is added:

final allowedValues = InteractiveTextAllowedValuesController(
  restrictedTypes: {
    InteractiveTextType.mention,
  },
);

InteractiveText(
  message,
  allowedValuesListenable: allowedValues,
  onTap: handleTap,
)

final profiles = await directory.search(query);
allowedValues.addAll(
  InteractiveTextType.mention,
  profiles.map((profile) => profile.alias),
);

The widget listens to the controller and invalidates its cached matches, styles, and recognizers when the policy changes. Use replace or remove when membership changes. Backend work stays in the application layer rather than inside parsing or widget builds.

Styling #

Every built-in type has an independent TextStyle. Actionable tokens use one stable light/dark palette, independent of the primary, secondary, or tertiary brand colors. Matches without an available action inherit the surrounding foreground. Detection alone does not imply that a token is tappable.

Tokens Light Dark
Actionable links, contact details, social tokens and references #4150F7 #708DFF
Dates, times, amounts and other metadata without actions Inherited Inherited

Blue follows the familiar link convention. Actionable links and contact details are underlined; social references use semibold weight. Code, spoilers and formatting retain the surrounding foreground. Check contrast on custom backgrounds; set InteractiveTextStyles(actionColor: ...) to adapt all action colors to a colored or media surface. Per-type styles and styleResolver take precedence, even for noninteractive tokens. The resolver exposes details.isInteractive. InteractiveTextStyles.unstyled() disables package defaults entirely.

Emoji default to font size 20 and line-height multiplier 0.6 in mixed and emoji-only text. Both remain configurable through emoji and emojiOnly.

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;
  },
)

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 without a recognizer and inherit the surrounding foreground by default. Explicit styles still apply.

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.
  • interactiveTypes can explicitly include or exclude actionable types.
  • Emoji and formatting-only ranges are not tappable by default.
  • Set autoLaunch: false to render styles without automatic actions.

Performance #

Each mounted widget parses only when its text, parser, or value allowlist 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.

1
likes
160
points
489
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A performant Flutter widget for detecting, styling, interacting with, and optionally expanding rich text patterns.

Homepage

Topics

#rich-text #links #mentions #hashtags #widget

License

MIT (license)

Dependencies

characters, flutter, url_launcher

More

Packages that depend on flutter_interactive_text