Skip to main content

bluesky_text_flutter pub package Dart SDK Version

bluesky_text_flutter provides Flutter widgets built on top of bluesky_text: a TextEditingController that live-highlights entities and the over-limit tail as the user types, and a viewer that renders a fetched post's facets with tappable mentions, links, and tags.

info

This package re-exports bluesky_text, so a single import gives you both the widgets and the underlying text-processing API.

Features ⭐

  • Live Highlighting - Entities (handles, links, tags) and the over-limit tail are styled on every keystroke
  • Post Viewer - Renders server-authoritative facets with tappable features
  • Typed Tap Callbacks - onMentionTap / onLinkTap / onTagTap plus a catch-all onFeatureTap
  • Customizable Styling - Per-segment style overrides for entities and overflow
  • IME-aware - Preserves the composing (underline) region while highlighting
  • Leak-free - Tap gesture recognizers are owned and disposed by the widget

Getting Started 💪

Install

tip

See the Install Package section for more details on how to install a package in your Dart and Flutter app.

flutter pub add bluesky_text_flutter

Import

import 'package:bluesky_text_flutter/bluesky_text_flutter.dart';

Composing: BlueskyTextEditingController

BlueskyTextEditingController is a TextEditingController that highlights Bluesky rich text as the user types — entities and the part of the text past the post-length limit are styled in a single pass on every keystroke. Attach it to a TextField like any controller:

import 'package:bluesky_text_flutter/bluesky_text_flutter.dart';
import 'package:flutter/material.dart';

class Composer extends StatefulWidget {
const Composer({super.key});


State<Composer> createState() => _ComposerState();
}

class _ComposerState extends State<Composer> {
final _controller = BlueskyTextEditingController();


void dispose() {
_controller.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return TextField(
controller: _controller,
maxLines: null,
// The over-limit tail is highlighted; `overflow` is null while in range.
decoration: InputDecoration(
errorText: _controller.overflow == null ? null : 'Too long',
),
);
}
}

Styling & Overrides

Styling precedence for each segment: styleBuilder (when it returns non-null) › the over-limit tail (overflowStyle) › an entity (entityStyle) › the field's base style. Defaults are the theme's primary color for entities and error color for the overflow tail.

final controller = BlueskyTextEditingController(
// Whether markdown links participate in analysis (mirrors BlueskyText).
enableMarkdown: true,

// Style for handles/links/tags.
entityStyle: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),

// Style for the text past the 300-grapheme limit.
overflowStyle: const TextStyle(backgroundColor: Color(0x33FF0000)),

// Full per-segment override; when it returns non-null it wins.
styleBuilder: (segment) =>
segment.type == null ? null : const TextStyle(letterSpacing: 0.2),
);

Read controller.overflow (a TextLengthOverflow?, null when within the limit) to drive a live character counter.

Viewing: BlueskyRichText

BlueskyRichText displays a fetched post: its text styled with the server's authoritative facets, with tappable features. Unlike re-detecting entities from text, it renders exactly what the author committed (a mention already carries its DID).

import 'package:bluesky_text_flutter/bluesky_text_flutter.dart';
import 'package:flutter/material.dart';

Widget buildPost(String text, List<PostFacet> facets) {
return BlueskyRichText(
text: text,
facets: facets,
onMentionTap: (did) => print('mention: $did'),
onLinkTap: (uri) => print('link: $uri'),
onTagTap: (tag) => print('tag: $tag'),
);
}

Parse facet JSON returned from the API with PostFacet.fromJson. For example, from a bluesky post record:

final richText = BlueskyRichText(
text: post.record.text,
facets: post.record.facets
?.map((f) => PostFacet.fromJson(f.toJson()))
.toList() ??
const [],
onMentionTap: (did) => context.go('/profile/$did'),
onLinkTap: (uri) => launchUrlString(uri),
);

Tap Callbacks & Styling

onMentionTap / onLinkTap / onTagTap are typed conveniences dispatched by feature kind; onFeatureTap is a catch-all that receives the raw FacetFeature. A provided typed callback takes precedence for its kind.

BlueskyRichText(
text: text,
facets: facets,
// Base style for the whole text.
style: const TextStyle(fontSize: 16),
// Style for mention/link/tag slices (defaults to the theme's primary color).
featureStyle: const TextStyle(color: Colors.teal),
// Catch-all: fires for any feature without a matching typed callback.
onFeatureTap: (feature) => print('${feature.type}: ${feature.value}'),
textAlign: TextAlign.start,
maxLines: 4,
overflow: TextOverflow.ellipsis,
);

The TapGestureRecognizers created for tappable features are owned by the widget and disposed automatically, so callers never leak them.

  • bluesky_text - The underlying rich-text analysis and facet generation (re-exported here)
  • bluesky - Full Bluesky client for fetching posts and creating them with facets