Floaty Chatheads
Floating bubble overlays for Flutter -- like Facebook Messenger chatheads, but for any widget.
Ever used Facebook Messenger on Android and seen that little circular bubble floating on top of everything? That is a "chathead." This plugin lets you build the same thing in Flutter -- a draggable bubble that floats above your app (or even above other apps on Android) and expands into any Flutter widget you want: a mini player, a chat window, quick-action buttons, a dashboard, or anything else you can dream up.
| Android | iOS |
|---|---|
|
|
Why Floaty Chatheads?
| What you get | Why it matters |
|---|---|
| One API, two platforms | Write your overlay once -- it works on Android and iOS with the same Dart code |
| Any Flutter widget | The expanded panel is a full Flutter engine, so you can use any widget, package, or animation |
| Spring-physics drag | The bubble snaps to screen edges with smooth, natural-feeling spring animations |
| Survives app death (Android) | Your overlay stays alive even if the user kills the main app |
| Zero-boilerplate helpers | Show a chathead in 3 lines; build an overlay widget in 1 line |
| Built-in messaging | Send data back and forth between your main app and the overlay in real time |
| Theming & accessibility | Full TalkBack / VoiceOver support, customizable colors, and a debug inspector |
| Production-ready | 267 tests, 100% coverage on handwritten code, MIT licensed |
How It Works (the big picture)
Your Main App Floating Overlay
───────────── ────────────────
┌──────────────────┐ ┌──────────┐
│ │ showChatHead() │ Bubble │ ← draggable, snaps
│ FloatyChatheads ├──────────────────────► │ (icon) │ to screen edges
│ │ └────┬─────┘
│ shareData({}) ◄──── bidirectional ────► │ tap
│ onData.listen() │ messaging ▼
│ │ ┌──────────┐
└──────────────────┘ │ Content │ ← any Flutter widget
│ Panel │ you define
└──────────┘
- You call
showChatHead()from your main app. This launches a floating bubble on the screen. - The bubble is draggable. Users can drag it around; it snaps to screen edges with spring physics.
- Tapping the bubble expands a content panel -- this is a completely separate Flutter widget running in its own engine (think of it as a mini-app inside the overlay).
- Your main app and the overlay can talk to each other using
shareData()/onDatastreams, or the higher-level helpers likeFloatyStateChannel,FloatyActionRouter, andFloatyProxyClient. - Dragging the bubble to the close target (bottom of the screen) dismisses the overlay.
Key concept -- separate engine: The overlay runs in its own Flutter engine, not inside your app's widget tree. This means it has its own
main()function (called an "entry point") and its own state. The plugin handles all the wiring between the two engines for you.
Quick Start (5 minutes)
1. Install
dependencies:
floaty_chatheads: ^1.2.1
flutter pub get
Requirements: Dart ^3.4.0, Flutter >=3.22.0, Android 6.0+ (API 23) / iOS 13.0+
2. Platform setup
Android -- add these permissions to android/app/src/main/AndroidManifest.xml
(inside the <manifest> tag, before <application>):
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
iOS -- nothing to do! No permissions needed.
3. Add your bubble icon to assets
Put an image (e.g. chatheadIcon.png) in your assets/ folder and declare it
in pubspec.yaml:
flutter:
assets:
- assets/chatheadIcon.png
- assets/close.png # optional close-target icon (Android)
- assets/closeBg.png # optional close-target background (Android)
iOS note: Icon assets are optional on iOS because the overlay is rendered entirely by your Flutter widget. On Android they are used for the native bubble bitmap and close-target drawable.
4. Create the overlay widget
The overlay runs in its own Flutter engine, so it needs a top-level function
marked with @pragma('vm:entry-point'). The easiest way is the one-liner helper:
// file: lib/overlay_main.dart
import 'package:floaty_chatheads/floaty_chatheads.dart';
import 'package:flutter/material.dart';
@pragma('vm:entry-point')
void overlayMain() => FloatyOverlayApp.run(
const Center(
child: Material(
elevation: 8,
borderRadius: BorderRadius.all(Radius.circular(16)),
child: Padding(
padding: EdgeInsets.all(24),
child: Text('Hello from the overlay!'),
),
),
),
);
FloatyOverlayApp.run() takes care of WidgetsFlutterBinding, wraps your
widget in a MaterialApp, and calls FloatyOverlay.setUp() so messaging works
out of the box.
5. Show the chathead
import 'package:floaty_chatheads/floaty_chatheads.dart';
Future<void> launchChatHead() async {
// 1. Ask for permission (no-op on iOS).
final granted = await FloatyChatheads.checkPermission();
if (!granted) await FloatyChatheads.requestPermission();
// 2. Show the chathead!
await FloatyChatheads.showChatHead(
entryPoint: 'overlayMain',
assets: const ChatHeadAssets(
icon: IconSource.asset('assets/chatheadIcon.png'),
closeIcon: IconSource.asset('assets/close.png'),
closeBackground: IconSource.asset('assets/closeBg.png'),
),
notification: const NotificationConfig(title: 'My Chathead'),
sizePreset: ContentSizePreset.card,
);
}
That is it! Run flutter run, call launchChatHead(), and you will see a
floating bubble on the screen.
6. Send data between app and overlay
// In your main app -- send data to the overlay:
FloatyChatheads.shareData({'message': 'Hello overlay!'});
// In your main app -- listen for data from the overlay:
FloatyChatheads.onData.listen((data) {
print('Overlay said: $data');
});
// In the overlay widget -- send data to the main app:
FloatyOverlay.shareData({'action': 'button_pressed'});
// In the overlay widget -- listen for data from the main app:
FloatyOverlay.onData.listen((data) {
print('Main app said: $data');
});
Choose Your Level
Floaty Chatheads is designed so you can start simple and add complexity only when you need it. Here is a roadmap:
| Level | What to use | Good for |
|---|---|---|
| Beginner | showChatHead() + shareData() / onData |
Simple overlays, quick prototypes |
| Intermediate | FloatyOverlayApp.run(), FloatyScope, FloatyLauncher, FloatyController |
Less boilerplate, lifecycle management |
| Advanced | FloatyStateChannel, FloatyActionRouter, FloatyProxyHost/Client, FloatyHostKit |
Complex state sync, RPC, offline queueing |
All levels use the same underlying plugin -- the helpers just reduce boilerplate.
Platform Comparison
The plugin exposes a single Dart API for both platforms, but the underlying implementations differ due to OS-level constraints:
| Capability | Android | iOS |
|---|---|---|
| Overlay scope | System-wide (above all apps) | App-level (above your app only) |
| Permissions | SYSTEM_ALERT_WINDOW required |
None required |
| Bubble rendering | Native View with bitmap icon |
Flutter widget |
| Edge snapping | Spring physics with configurable margin | Spring physics with configurable margin |
| Entrance animations | Pop, slide, fade | Pop, slide, fade |
| Badge counter | Native drawn badge | Native drawn badge |
| Theming | Badge, border, shadow, close tint, palette | Badge, border, shadow, palette |
| Size presets | compact, card, halfScreen, fullScreen |
compact, card, halfScreen, fullScreen |
| Debug inspector | FPS, spring HUD, bounds, message log | getDebugInfo() |
| Accessibility | Full TalkBack support | VoiceOver labels and screen changes |
| Dragging | Spring physics touch handler | UIPanGestureRecognizer |
| Position persistence | Supported | Supported (UserDefaults) |
| Foreground service | Persists across app switches | N/A (app-level window) |
| Overlay survival | Survives app death (service-owned engine) | N/A |
| Bidirectional messaging | Yes | Yes |
| Multi-bubble | Messenger-style row | Via Flutter API callback |
| State channel | FloatyStateChannel<T> bidirectional sync |
FloatyStateChannel<T> bidirectional sync |
| Action routing | FloatyActionRouter with offline queueing |
FloatyActionRouter (no queueing) |
| Proxy RPC | FloatyProxyHost / FloatyProxyClient |
FloatyProxyHost / FloatyProxyClient |
| Min platform version | Android 6.0+ (API 23) | iOS 13.0+ |
Note: On iOS,
checkPermission()andrequestPermission()always returntruesince no special permission is needed.
Features at a Glance
| Category | What it does |
|---|---|
| Chathead Bubbles | Draggable bubble with spring-based edge snapping, entrance animations (pop, slide, fade), and multi-bubble support (Messenger-style row) |
| Content Panel | Any Flutter widget rendered in a separate engine -- like a mini-app inside the overlay |
| Bidirectional Messaging | shareData / onData streams for real-time two-way communication |
| Badge Counter | Numeric badge on the bubble, updatable from either side |
| Theming | Badge colors, bubble border, shadow, close-target tint, and a full color palette forwarded to the overlay |
| Size Presets | Named sizes (compact, card, halfScreen, fullScreen) instead of raw pixel values |
| Debug Inspector | Native overlay with FPS counter, spring HUD, view bounds, and Pigeon message log (Android) |
| Optional Logging | All native logcat output is silent by default; set debugMode: true to enable verbose logs during development |
| Accessibility | TalkBack (Android) and VoiceOver (iOS) with content descriptions, live-region announcements, and focus management |
| Lifecycle Events | Streams for onTapped, onClosed, onExpanded, onCollapsed, onDragStart, onDragEnd |
| Permission Gate | FloatyPermissionGate widget that shows a fallback until overlay permission is granted |
| Programmatic Control | expandChatHead(), collapseChatHead(), addChatHead(), removeChatHead(), resizeContent() |
| Foreground Service | Overlay persists across app switches (Android) |
| Overlay Survival | Overlay survives app death -- service-owned engine, connection detection, action queueing, proxy RPC, and automatic reconnection (Android) |
| State Channel | FloatyStateChannel<T> for type-safe bidirectional state sync with JSON serialization |
| Action Routing | FloatyActionRouter for typed action dispatch with handler registration and offline queueing |
| Proxy Services | FloatyProxyHost + FloatyProxyClient for request/response RPC between main app and overlay |
Convenience Helpers
The plugin ships helpers at different levels of abstraction. Pick the one that fits your use case -- you can always switch later.
| Need | Use | Complexity |
|---|---|---|
| Bootstrap an overlay entry point | FloatyOverlayApp.run() / .runScoped() |
Beginner |
| Launch with auto permission handling | FloatyLauncher |
Beginner |
| Lifecycle-aware show/close tied to a widget | FloatyControllerWidget / FloatyController |
Beginner |
| Auto-wired overlay context with all streams | FloatyScope / FloatyOverlayScope |
Intermediate |
| Display incoming overlay data reactively (main app) | FloatyDataBuilder<T> |
Intermediate |
| Build an overlay with zero lifecycle code | FloatyOverlayBuilder<T> |
Intermediate |
| Type-safe messaging with custom models | FloatyMessenger<T> |
Intermediate |
| Bidirectional state sync (JSON-based) | FloatyStateChannel<T> |
Advanced |
| Typed action dispatch with offline queueing | FloatyActionRouter |
Advanced |
| Overlay-to-app request/response RPC | FloatyProxyHost / FloatyProxyClient |
Advanced |
| All-in-one communication bundle | FloatyHostKit / FloatyOverlayKit |
Advanced |
FloatyOverlayApp -- one-liner overlay bootstrap
Replaces 5 lines of entry-point boilerplate with one call:
@pragma('vm:entry-point')
void overlayMain() => FloatyOverlayApp.run(const MyOverlayWidget());
Handles ensureInitialized(), FloatyOverlay.setUp(), and wraps your
widget in a MaterialApp. Accepts optional theme and navigatorObservers.
Use runScoped() to additionally wrap the child in a FloatyScope, so
FloatyScope.of(context) works everywhere inside the overlay:
@pragma('vm:entry-point')
void overlayMain() => FloatyOverlayApp.runScoped(const MyOverlayWidget());
FloatyLauncher -- one-call launch with auto permissions
Combines permission check + request + show into a single Future<bool>:
final shown = await FloatyLauncher.show(
entryPoint: 'overlayMain',
assets: const ChatHeadAssets(
icon: IconSource.asset('assets/icon.png'),
),
sizePreset: ContentSizePreset.card,
);
Also provides FloatyLauncher.toggle() to show/close with one call.
FloatyController -- lifecycle-aware declarative control
A ChangeNotifier-based controller that manages the chathead lifecycle.
Automatically handles show/close tied to widget lifecycle:
FloatyControllerWidget(
entryPoint: 'overlayMain',
assets: const ChatHeadAssets(
icon: IconSource.asset('assets/icon.png'),
),
sizePreset: ContentSizePreset.card,
onData: (data) => print('Got: $data'),
child: MyPageContent(),
)
Use the builder parameter for reactive re-rendering based on controller state:
FloatyControllerWidget(
entryPoint: 'overlayMain',
assets: const ChatHeadAssets(
icon: IconSource.asset('assets/icon.png'),
),
builder: (context, controller) => ElevatedButton(
onPressed: controller.toggle,
child: Text(controller.isActive ? 'Close' : 'Show'),
),
)
Or use the controller directly for fine-grained control:
final controller = FloatyController(
entryPoint: 'overlayMain',
assets: const ChatHeadAssets(
icon: IconSource.asset('assets/icon.png'),
),
onError: (e, st) => debugPrint('Error: $e'),
);
await controller.show();
await controller.toggle();
await controller.sendData({'action': 'refresh'});
FloatyScope -- auto-wired overlay context
An InheritedWidget that subscribes to all overlay streams and rebuilds
when any event fires. No manual stream wiring needed:
@pragma('vm:entry-point')
void overlayMain() => FloatyOverlayApp.runScoped(const MyOverlay());
// Inside MyOverlay:
final scope = FloatyScope.of(context);
Text('Last message: ${scope.lastMessage}');
Text('Palette primary: ${scope.palette?.primary}');
Exposes: lastMessage, messages, lastTappedId, lastClosedId,
lastExpandedId, lastCollapsedId, lastDragStart, lastDragEnd,
and palette.
FloatyDataBuilder<T> -- main-app reactive builder
Subscribes to FloatyChatheads.onData and reduces incoming messages into
typed state. No manual StreamSubscription, setState, or dispose:
FloatyDataBuilder<int>(
initialData: 0,
onData: (count, raw) => raw is Map ? raw['count'] as int : count,
builder: (context, count) => Text('$count'),
)
FloatyOverlayBuilder<T> -- overlay-side zero-boilerplate builder
Handles setUp(), stream subscriptions, mounted guards, and dispose()
automatically. Turns overlay widgets into stateless declarations:
class MyOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FloatyOverlayBuilder<int>(
initialState: 0,
onData: (count, data) =>
data is Map ? data['count'] as int : count,
onTapped: (count, id) => count + 1,
builder: (context, count) => Text('$count'),
);
}
}
FloatyMessenger<T> -- type-safe messaging
Eliminates raw Object? casting with a serializer/deserializer pair:
// Main app side:
final messenger = FloatyMessenger<ChatMessage>(
serialize: (msg) => msg.toJson(),
deserialize: ChatMessage.fromJson,
);
messenger.send(ChatMessage(text: 'Hello!'));
messenger.messages.listen((ChatMessage msg) => print(msg.text));
// Overlay side:
final messenger = FloatyMessenger<ChatMessage>.overlay(
serialize: (msg) => msg.toJson(),
deserialize: ChatMessage.fromJson,
);
FloatyStateChannel<T> -- type-safe bidirectional state sync
Synchronises a shared state object between the main app and overlay with automatic JSON serialization:
// Main app side:
final channel = FloatyStateChannel<MyState>(
toJson: (s) => s.toJson(),
fromJson: MyState.fromJson,
initialState: MyState(),
);
await channel.setState(MyState(counter: 42));
channel.onStateChanged.listen((state) => print(state.counter));
FloatyActionRouter -- typed action dispatch with queueing
Register named action handlers and dispatch typed FloatyAction objects.
On the overlay side, actions are automatically queued when the main app is
disconnected and flushed on reconnection:
// Main app side -- register handlers:
final router = FloatyActionRouter();
router.on<IncrementAction>(
'increment',
fromJson: IncrementAction.fromJson,
handler: (action) => counter += action.amount,
);
// Overlay side -- dispatch (queues if disconnected):
final router = FloatyActionRouter.overlay();
router.dispatch(IncrementAction(amount: 5));
FloatyProxyHost / FloatyProxyClient -- overlay-to-app RPC
Expose named services from the main app that the overlay can call:
// Main app side -- register a service:
final host = FloatyProxyHost();
host.register('time', (method, params) {
return {'iso': DateTime.now().toIso8601String()};
});
// Overlay side -- call the service:
final client = FloatyProxyClient();
final result = await client.call('time', 'now',
fallback: () => {'iso': 'offline'},
);
FloatyHostKit / FloatyOverlayKit -- all-in-one communication bundle
Bundles FloatyStateChannel, FloatyActionRouter, and FloatyProxyHost/Client
into a single object. One kit instance replaces three separate setups:
// Main app side:
final kit = FloatyHostKit<MyState>(
stateToJson: (s) => s.toJson(),
stateFromJson: MyState.fromJson,
initialState: MyState(),
);
kit.router.on<IncrementAction>('increment', ...);
kit.proxy.register('time', ...);
await kit.state.setState(MyState(counter: 42));
FloatyOverlayScope<S> -- zero-boilerplate typed overlay scope
Combines FloatyOverlayKit with automatic stream management in a single
widget. Handles setUp(), subscriptions, state sync, connection tracking,
and dispose(). Access the kit from descendants via FloatyOverlayScope.of:
@pragma('vm:entry-point')
void overlayMain() => FloatyOverlayApp.run(
FloatyOverlayScope<MyState>(
stateToJson: (s) => s.toJson(),
stateFromJson: MyState.fromJson,
initialState: MyState(),
builder: (context, kit, state, connected) {
return Text('Count: ${state.counter}');
},
),
);
FloatyConnectionState -- main app connection tracking
Overlay-side utility that tracks whether the main app is connected. Useful for showing connection indicators and adapting UI:
// Check current state:
if (FloatyConnectionState.isMainAppConnected) { ... }
// React to changes:
FloatyConnectionState.onConnectionChanged.listen((connected) {
setState(() => _online = connected);
});
Pre-built Overlay Widgets
Drop-in widgets for common overlay use cases. No custom UI needed.
FloatyMiniPlayer
A media player overlay with play/pause, next/previous, progress bar, and album art support:
@pragma('vm:entry-point')
void playerOverlay() => FloatyOverlayApp.run(
FloatyMiniPlayer(
title: 'Now Playing',
subtitle: 'Artist Name',
isPlaying: true,
progress: 0.4,
onPlayPause: () => FloatyOverlay.shareData({'action': 'toggle'}),
onClose: FloatyOverlay.closeOverlay,
),
);
FloatyNotificationCard
A toast/notification-style card with icon, title, body, and action buttons:
@pragma('vm:entry-point')
void notifOverlay() => FloatyOverlayApp.run(
FloatyNotificationCard(
title: 'New Message',
body: 'You have 3 unread messages',
icon: Icons.message,
actions: [
FloatyNotificationAction(label: 'View', onPressed: () {}),
FloatyNotificationAction(label: 'Dismiss', onPressed: FloatyOverlay.closeOverlay),
],
),
);
Theming
Pass a ChatHeadTheme to showChatHead() to customise the native bubble appearance
and deliver a color palette to your overlay widget:
await FloatyChatheads.showChatHead(
// ...
theme: ChatHeadTheme(
badgeColor: Colors.deepPurple.toARGB32(),
badgeTextColor: Colors.white.toARGB32(),
bubbleBorderColor: Colors.deepPurpleAccent.toARGB32(),
bubbleBorderWidth: 2,
bubbleShadowColor: Colors.black54.toARGB32(),
closeTintColor: Colors.redAccent.toARGB32(),
overlayPalette: {
'primary': Colors.deepPurple.toARGB32(),
'secondary': Colors.amber.toARGB32(),
'surface': Colors.white.toARGB32(),
},
),
);
In the overlay, consume the palette via the stream or the static getter:
FloatyOverlay.setUp();
// Static access.
final primary = FloatyOverlay.palette?.primary;
// Reactive updates.
FloatyOverlay.onPaletteChanged.listen((palette) {
setState(() => _bg = palette.surface ?? Colors.white);
});
Size Presets
Instead of specifying raw pixel dimensions, use a ContentSizePreset:
await FloatyChatheads.showChatHead(
// ...
sizePreset: ContentSizePreset.halfScreen,
);
| Preset | Width | Height | Good for |
|---|---|---|---|
compact |
160 dp | 200 dp | Small controls, mini player |
card |
300 dp | 400 dp | Chat windows, notification cards |
halfScreen |
Full width | Half screen | Dashboards, maps |
fullScreen |
Full width | Full height | Full app-like overlays |
Debug Inspector & Logging (Android)
The plugin includes two debug tools to help you during development:
Debug overlay
Enable the native debug overlay to visualize what is happening under the hood:
await FloatyChatheads.showChatHead(
// ...
debugMode: true,
);
The inspector renders directly on the Android WindowManager layer (does not
intercept touches) and displays:
- FPS counter -- frames per second measured via
Choreographer - Spring velocity HUD -- current velocity of the bubble's spring animation
- State info -- toggled, captured, head count
- Green bounds rectangles around each chathead bubble
- Blue bounds rectangle around the content panel
Query debug telemetry from Dart:
final info = await FloatyOverlay.getDebugInfo();
Optional native logs
All native logcat output (tagged FloatyDebug) is silent by default -- your
production builds will not have any log noise from the plugin. When you need to
debug native behavior, set debugMode: true and the plugin will emit detailed
logs covering engine lifecycle, service creation, content panel dimensions, and
window management. Turn it off again by omitting the flag or setting it to
false.
Accessibility
All native views ship with built-in TalkBack (Android) and VoiceOver (iOS) support:
- ChatHead bubbles: content description updates with badge count (e.g. "Chat bubble, 3 new messages"), custom accessibility actions for expand/collapse and dismiss
- Close target: live-region announcement when visible, descriptive label
- Content panel: receives focus on expand, announces hide on collapse
- Motion tracker: hidden from accessibility tree
- Debug overlay: excluded from accessibility
No extra Dart code is required -- accessibility is handled at the native layer.
For overlay-side semantics, use Flutter's standard Semantics widget (works on
both platforms).
Overlay Survival (Android)
On Android, the overlay can survive app death. When the user force-stops or swipes away the main app, the overlay remains visible and functional:
- The foreground service owns the Flutter engine, so the overlay keeps running after the main app's process is killed.
FloatyConnectionStatedetects the disconnect and firesonConnectionChangedso the overlay can show an offline indicator.FloatyActionRouterqueues dispatched actions while disconnected and flushes them in order when the main app reconnects.FloatyProxyClientreturns a fallback value (or throwsFloatyProxyDisconnectedException) instead of timing out.- When the main app restarts, the plugin automatically reconnects to the existing overlay -- queued actions flush, state re-syncs, and proxy calls resume.
iOS: Overlay survival does not apply on iOS since the overlay uses an app-level
UIWindowthat is tied to the app's process.
Configuration Options
showChatHead parameters
| Parameter | Type | Default | Description | Platform |
|---|---|---|---|---|
entryPoint |
String |
'overlayMain' |
Top-level Dart function annotated with @pragma('vm:entry-point') |
Both |
contentWidth |
int? |
null |
Content panel width in dp (Android) / pt (iOS) | Both |
contentHeight |
int? |
null |
Content panel height in dp / pt | Both |
assets |
ChatHeadAssets? |
null |
Grouped icon configuration (icon, closeIcon, closeBackground) | Android |
notification |
NotificationConfig? |
null |
Grouped notification configuration (title, icon, visibility) | Android |
snap |
SnapConfig? |
null |
Grouped snap configuration (edge, margin, persistPosition) | Android |
flag |
OverlayFlag |
.defaultFlag |
Window behavior flag (e.g. click-through) | Both |
enableDrag |
bool |
true |
Whether the bubble can be dragged | Both |
entranceAnimation |
EntranceAnimation |
.none |
How the bubble appears (pop, slideInFromLeft, slideInFromRight, fadeIn) |
Android |
theme |
ChatHeadTheme? |
null |
Colors, borders, shadows, and overlay palette | Android |
sizePreset |
ContentSizePreset? |
null |
Named size (overrides contentWidth / contentHeight) |
Both |
debugMode |
bool |
false |
Enable debug inspector and verbose native logs | Android |
iOS Behavior Details
On iOS the overlay is a UIWindow at windowLevel = .alert + 1:
- The window appears above your app but below system UI (status bar, Control Center)
- It stays visible while your app is in the foreground but does not persist when the app is backgrounded
- Dragging uses
UIPanGestureRecognizerwith bounds clamping and a 0.2 s settle animation - Default position: top-right corner, 16 pt from the right edge, 80 pt from the top
Since iOS does not have an equivalent to Android's SYSTEM_ALERT_WINDOW, the
overlay cannot float above other apps.
Examples
The example app ships with 14 demo screens accessible from a gallery page:
| # | Example | What it demonstrates |
|---|---|---|
| 1 | Basic | Show / close / send data -- the simplest starting point |
| 2 | Messenger Chat | Two-way messaging between app and overlay |
| 3 | Mini Player | Media controls with play/pause state sync |
| 4 | Quick Actions | Click-through FAB overlay for quick shortcuts |
| 5 | Notification Counter | Badge counter updated reactively |
| 6 | Timer / Stopwatch | Dynamic resize and lap tracking |
| 7 | Multi-Chathead | Multiple Messenger-style bubbles in a row |
| 8 | Dashboard | Near-fullscreen scrollable notes overlay |
| 9 | Messenger Fullscreen | Bubble at top, full chat panel below |
| 10 | Features Showcase | Badge, expand/collapse, lifecycle events |
| 11 | Themed Chathead | Custom colors, border ring, palette delivery |
| 12 | Accessibility | TalkBack labels and large touch targets |
| 13 | Interactive Map | OSM map with action routing, state sync, proxy |
| 14 | Overlay Survival | Kill app -- overlay survives, queues actions, reconnects |
Run the example:
cd floaty_chatheads/example
flutter run
Tip for beginners: Start with Example 1 (Basic) -- it is the simplest way to see how
showChatHead()andshareData()work together.
Test Suite & Coverage
The plugin ships with 267 unit and widget tests across all 4 packages:
| Package | Tests | Status |
|---|---|---|
floaty_chatheads |
233 | All passing |
floaty_chatheads_platform_interface |
33 | All passing |
floaty_chatheads_android |
1 | All passing |
| Total | 267 | All passing |
Coverage (handwritten code, excluding generated files)
| File | Coverage |
|---|---|
floaty_chatheads.dart |
100% |
floaty_controller.dart |
100% |
floaty_data_builder.dart |
100% |
floaty_launcher.dart |
100% |
floaty_messenger.dart |
100% |
floaty_overlay.dart |
100% |
floaty_overlay_app.dart |
100% |
floaty_overlay_builder.dart |
100% |
floaty_permission_gate.dart |
100% |
floaty_scope.dart |
100% |
floaty_connection_state.dart |
100% |
floaty_state_channel.dart |
100% |
floaty_action_router.dart |
100% |
floaty_proxy.dart |
100% |
floaty_mini_player.dart |
100% |
floaty_notification_card.dart |
100% |
testing.dart |
100% |
| Overall | 100% |
Run tests locally:
# Main package
cd floaty_chatheads && flutter test
# With coverage
flutter test --coverage
# Platform interface
cd floaty_chatheads_platform_interface && flutter test
Testing Utilities
Import the testing utilities for unit testing overlay-dependent code:
import 'package:floaty_chatheads/testing.dart';
FakeFloatyPlatform
Drop-in replacement for the platform instance. Tracks all method calls:
final fake = FakeFloatyPlatform();
FloatyChatheadsPlatform.instance = fake;
await FloatyChatheads.showChatHead(entryPoint: 'test');
expect(fake.showChatHeadCalled, isTrue);
expect(fake.lastConfig?.entryPoint, equals('test'));
// Control permission behavior:
fake.permissionGranted = false;
expect(await FloatyChatheads.checkPermission(), isFalse);
FakeOverlayDataSource
Simulates overlay events for testing overlay-side widgets:
final fake = FakeOverlayDataSource();
fake.emitData({'action': 'refresh'});
fake.emitTapped('default');
Quickstart
See example/lib/quickstart.dart
for a complete, minimal integration using all the helpers (~120 lines total).
API Reference
FloatyChatheads (main app side)
| Method | Description | Android | iOS |
|---|---|---|---|
checkPermission() |
Returns true if overlay permission is granted |
Yes | Always true |
requestPermission() |
Opens system settings for overlay permission | Yes | Always true |
showChatHead({...}) |
Launches the chathead with full configuration | Yes | Yes |
closeChatHead() |
Closes the chathead and stops the service | Yes | Yes |
isActive() |
Whether the overlay is currently visible | Yes | Yes |
addChatHead({id, iconAsset}) |
Adds another bubble to the group | Yes | Yes |
removeChatHead(id) |
Removes a bubble by ID | Yes | Yes |
updateBadge(count) |
Sets the badge number (0 hides it) | Yes | -- |
expandChatHead() |
Programmatically expands the content panel | Yes | -- |
collapseChatHead() |
Programmatically collapses the content panel | Yes | -- |
shareData(data) |
Sends data to the overlay | Yes | Yes |
onData |
Stream of messages from the overlay | Yes | Yes |
dispose() |
Tears down the message channel | Yes | Yes |
FloatyConnectionState (overlay side)
| Member | Description |
|---|---|
setUp() |
Registers the connection-state handler (called automatically by FloatyOverlay.setUp()) |
isMainAppConnected |
Whether the main app is currently connected (bool) |
onConnectionChanged |
Stream<bool> that fires on connect/disconnect transitions |
dispose() |
Tears down the handler and stream |
FloatyStateChannel<T> (both sides)
| Member | Description |
|---|---|
FloatyStateChannel(...) |
Main-app constructor with toJson, fromJson, initialState |
FloatyStateChannel.overlay(...) |
Overlay constructor |
setState(T state) |
Sends the full state to the other side |
updateState(Map<String, dynamic>) |
Sends a partial update (merged on the receiver) |
onStateChanged |
Stream<T> of incoming state updates |
currentState |
The latest state value |
dispose() |
Tears down handler and stream |
FloatyActionRouter (both sides)
| Member | Description |
|---|---|
FloatyActionRouter() |
Main-app constructor |
FloatyActionRouter.overlay(...) |
Overlay constructor with optional maxQueueSize, overflowStrategy |
on<A>(type, fromJson, handler) |
Registers a handler for a named action type |
dispatch(FloatyAction) |
Sends an action (queues if overlay is disconnected) |
queueLength |
Number of actions currently queued |
dispose() |
Tears down handler, stream, and connection subscription |
FloatyProxyHost / FloatyProxyClient
| Member | Description |
|---|---|
FloatyProxyHost() |
Main-app side -- exposes services to the overlay |
register(service, handler) |
Registers a named service with a (method, params) => result handler |
FloatyProxyClient() |
Overlay side -- calls services on the main app |
call(service, method, {params, timeout, fallback}) |
Calls a service; returns fallback or throws FloatyProxyDisconnectedException if offline |
dispose() |
Tears down handler and pending completers |
FloatyOverlay (overlay side)
| Method | Description | Android | iOS |
|---|---|---|---|
setUp() |
Initializes the overlay message handler (call once) | Yes | Yes |
onData |
Stream of messages from the main app | Yes | Yes |
onTapped / onClosed / onExpanded / onCollapsed |
Lifecycle event streams | Yes | Yes |
onDragStart / onDragEnd |
Drag event streams with position info | Yes | Yes |
onPaletteChanged |
Stream of palette updates from the host | Yes | -- |
palette |
Current OverlayColorPalette (nullable) |
Yes | -- |
resizeContent(w, h) |
Resizes the content panel from inside the overlay | Yes | Yes |
updateFlag(flag) |
Changes the window flag (e.g. click-through) | Yes | Yes |
updateBadge(count) |
Updates the badge from the overlay side | Yes | -- |
closeOverlay() |
Closes the overlay from inside | Yes | Yes |
getOverlayPosition() |
Returns the current overlay position | Yes | Yes |
getDebugInfo() |
Returns debug telemetry (debug mode only) | Yes | -- |
shareData(data) |
Sends data to the main app | Yes | Yes |
dispose() |
Tears down handlers | Yes | Yes |
Architecture
floaty_chatheads/ # Main package (what you import)
lib/src/
floaty_chatheads.dart # FloatyChatheads static class
floaty_overlay.dart # FloatyOverlay + OverlayColorPalette
floaty_data_builder.dart # Main-app reactive builder (reducer pattern)
floaty_overlay_builder.dart # Overlay zero-boilerplate builder
floaty_overlay_app.dart # One-liner overlay bootstrap (run / runScoped)
floaty_controller.dart # Lifecycle-aware controller (child / builder)
floaty_permission_gate.dart # Permission gate widget
floaty_connection_state.dart # Main-app connection tracking (overlay side)
floaty_state_channel.dart # Bidirectional type-safe state sync
floaty_action_router.dart # Typed action dispatch + offline queueing
floaty_proxy.dart # Overlay-to-app RPC (host + client)
floaty_chatheads_platform_interface/ # Shared contract (you don't touch this)
lib/src/models/
chat_head_config.dart # Configuration model
chat_head_theme.dart # Theming model
content_size_preset.dart # Size preset enum
floaty_chatheads_android/ # Android native code
android/src/main/kotlin/
FloatyChatheadsPlugin.kt # Plugin entry point
floating_chathead/
ChatHead.kt # Bubble view (badge, border, a11y)
ChatHeads.kt # Layout manager + spring physics
Close.kt # Close-target view
DebugOverlayView.kt # Debug inspector overlay
services/
FloatyContentJobService.kt # Foreground service + Pigeon host
utils/
Managment.kt # Shared state (theme, debug, springs)
floaty_chatheads_ios/ # iOS native code
ios/.../Sources/
FloatyCheaheadsPlugin.swift # UIWindow overlay + pan gesture
FloatyChatheadsApi.g.swift # Pigeon-generated Swift bindings
Communication uses Pigeon (a code-generation tool by the Flutter team) for
type-safe platform channels on both platforms, plus a BasicMessageChannel
relay for data messages between the main app and overlay.
Migration from floaty_chathead
This package (floaty_chatheads) is the successor to the original
floaty_chathead plugin.
It is a complete rewrite -- not a drop-in upgrade.
What changed
| Area | floaty_chathead (old) |
floaty_chatheads (new) |
|---|---|---|
| Architecture | Single package | Federated (main + platform_interface + android + ios) |
| Platform channels | Method channel | Pigeon-generated type-safe APIs |
| iOS support | None | Full support |
| Theming | None | Badge, border, shadow, close tint, overlay palette |
| Size presets | None | compact, card, halfScreen, fullScreen |
| Debug tooling | None | FPS counter, spring HUD, bounds, message log, optional logging |
| Accessibility | None | Full TalkBack + VoiceOver support |
| Multi-bubble | None | Messenger-style row with add/remove by ID |
| Badge counter | None | Native badge, updatable from main app or overlay |
| Expand / Collapse | None | Programmatic + accessibility actions |
| Snap behavior | Basic | Spring-based with configurable edge, margin, persistence |
| Entrance animations | None | Pop, slide-in, fade-in |
| Overlay messaging | Basic | Bidirectional streams + palette delivery |
How to migrate
- Replace
floaty_chatheadwithfloaty_chatheads: ^1.2.1in yourpubspec.yaml. - Update imports from
package:floaty_chathead/...topackage:floaty_chatheads/floaty_chatheads.dart. - Replace method calls with the new static API on
FloatyChatheadsandFloatyOverlay. - Add Android manifest permissions if not already present (see Quick Start).
- Review the Configuration Options for new parameters.
Acknowledgements
-
flutter_overlay_window by Saad Farhan -- This project draws significant inspiration from
flutter_overlay_windowfor the approach to hosting Flutter widgets inside system overlay windows. -
Facebook Rebound -- Spring physics library used for chathead snapping and drag animations on Android.
-
Very Good CLI -- Project scaffolding and federated plugin structure generated with Very Good CLI.
License
This project is licensed under the MIT License. See LICENSE for details.
Libraries
- floaty_chatheads
- Floaty Chatheads -- a Flutter federated plugin for floating chathead bubbles on Android and iOS.
- testing
- Testing utilities for floaty_chatheads.