extra_field 0.1.0 copy "extra_field: ^0.1.0" to clipboard
extra_field: ^0.1.0 copied to clipboard

A rich composer input field for Flutter — inline trigger detection (#, @, $), action buttons, sticker panels, attachment previews, async validators, and a send button. Zero external dependencies.

ExtraField #

A rich composer input field for Flutter — the kind of text input you see in WhatsApp, Telegram, Twitter/X, and Facebook. Inline trigger detection, action buttons, sticker panels, attachment previews, async validators, and a send button — all with zero external dependencies.

pub package License: MIT


Preview #

Chat Composer Custom Builders
Chat Composer Custom Builders
Social Post Validators
Social Post Validators

Features #

  • Inline Triggers — Type #, @, $, or any character to activate autocomplete suggestions with styled inline text. Supports both static lists and async search with debounce.
  • Action Buttons — Media picker, camera, sticker panel, location picker, or fully custom actions. All via callbacks — zero platform dependencies.
  • Sticker Picker — Tabbed grid panel with multiple packs, selection limits, and custom sticker builders.
  • Attachment Previews — Horizontal scrollable bar showing media thumbnails, location chips, and sticker previews with remove buttons.
  • Async Validators — Validate the composed value with parallel async validators. Supports auto-validate on change with debounce.
  • Send Button — Built-in or fully custom via sendButtonBuilder.
  • Full Customization — Every UI element is replaceable via builder callbacks, theme data, and user-provided strings.
  • RTL / Localization — Full RTL support. All strings configurable via ExtraFieldStrings.
  • Programmatic Control — External ExtraFieldController for reading/writing text, attachments, location, stickers, panels, and validation state.
  • SerializationtoMap() / fromMap() on all models and the composed value for easy persistence.

Getting Started #

Installation #

Add to your pubspec.yaml:

dependencies:
  extra_field: ^0.1.0

Then run:

flutter pub get

Import #

import 'package:extra_field/extra_field.dart';

Usage #

Minimal Example #

ExtraFieldWidget(
  config: ExtraFieldConfig(
    hintText: 'Type a message...',
    onSend: (value) {
      print(value.text);
    },
  ),
)

Chat Composer (WhatsApp / Telegram style) #

ExtraFieldWidget(
  config: ExtraFieldConfig(
    hintText: 'Type a message...',
    maxLines: 5,

    // Inline triggers
    triggers: [
      TriggerConfig(
        trigger: '#',
        suggestions: [
          TriggerMatch(trigger: '#', label: 'Flutter', id: '1'),
          TriggerMatch(trigger: '#', label: 'Dart', id: '2'),
        ],
        style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w600),
      ),
      TriggerConfig(
        trigger: '@',
        onSearch: (query) async {
          // Your async search logic
          return await api.searchUsers(query);
        },
        debounce: Duration(milliseconds: 300),
        style: TextStyle(color: Colors.purple, fontWeight: FontWeight.w600),
      ),
    ],

    // Action buttons
    actions: [
      ExtraFieldAction.media(
        icon: Icons.attach_file,
        mediaTypes: [MediaType.image, MediaType.video],
        maxMediaItems: 5,
        onPickMedia: (type) async {
          // Your media picker logic — return list or null
          return await pickMedia(type);
        },
      ),
      ExtraFieldAction.stickers(
        icon: Icons.emoji_emotions,
        packs: [emojiPack, animalPack],
        maxStickers: 3,
      ),
      ExtraFieldAction.location(
        icon: Icons.location_on,
        onPickLocation: () async {
          // Your location picker logic — return LocationData or null
          return await pickLocation();
        },
      ),
      ExtraFieldAction.custom(
        id: 'quick_replies',
        icon: Icons.flash_on,
        panelBuilder: (context, onClose) {
          return MyQuickRepliesPanel(onClose: onClose);
        },
      ),
    ],

    // Send
    onSend: (value) {
      print('Text: ${value.text}');
      print('Triggers: ${value.triggers}');
      print('Attachments: ${value.attachments}');
      print('Location: ${value.location}');
      print('Stickers: ${value.stickers}');
    },
  ),
)

Social Post (Twitter/X style) #

ExtraFieldWidget(
  initialValue: ExtraFieldValue(text: 'Hello '),
  config: ExtraFieldConfig(
    hintText: "What's happening?",
    maxLength: 280,
    maxLines: 8,
    minLines: 3,

    // Custom theme
    theme: ExtraFieldThemeData(
      primaryColor: Colors.blue,
      borderRadius: BorderRadius.circular(16),
      padding: EdgeInsets.all(16),
    ),

    // Custom send button
    sendButtonBuilder: (context, onSend) {
      return FilledButton(onPressed: onSend, child: Text('Post'));
    },

    triggers: [
      TriggerConfig(trigger: '#', suggestions: hashtags),
      TriggerConfig(trigger: '@', onSearch: searchUsers),
      TriggerConfig(
        trigger: '\$',
        suggestions: variables,
        style: TextStyle(color: Colors.green, fontStyle: FontStyle.italic),
      ),
    ],

    onSend: (value) => postToTimeline(value),
  ),
)

External Controller #

final controller = ExtraFieldController();

// Pass to widget
ExtraFieldWidget(controller: controller, config: config)

// Programmatic control
controller.text = 'Hello #Flutter @john!';
controller.addAttachment(MediaItem(type: MediaType.image, name: 'photo.jpg'));
controller.setLocation(LocationData(latitude: 24.7, longitude: 46.6, name: 'Riyadh'));
controller.addSticker(Sticker(id: 's1', url: '', emoji: '\u{1F600}'));
controller.openPanel('stickers');

// Read composed value
final value = controller.value;       // ExtraFieldValue
final map = controller.toMap();        // Map<String, dynamic>

// Load a saved value
controller.loadValue(savedValue);

// Reset everything
controller.reset();

// Don't forget to dispose
controller.dispose();

Async Validators #

ExtraFieldConfig(
  validators: [
    AsyncValidator(
      fieldId: 'text',
      validate: (value) async {
        if (value.text.trim().isEmpty) return 'Text is required';
        return null; // valid
      },
    ),
    AsyncValidator(
      fieldId: 'profanity',
      validate: (value) async {
        final result = await api.checkProfanity(value.text);
        return result.hasProfanity ? 'Contains blocked content' : null;
      },
      validateOnChange: true,          // auto-validate as user types
      debounce: Duration(milliseconds: 500),
    ),
    AsyncValidator(
      fieldId: 'media',
      validate: (value) async {
        if (value.attachments.isEmpty) return 'Attach at least 1 image';
        return null;
      },
    ),
  ],
)

// Manual validation
final result = await controller.validate();
if (result.isValid) {
  // send
} else {
  print(result.errors); // {'profanity': 'Contains blocked content'}
}

Custom Builders #

Every UI element can be replaced:

ExtraFieldConfig(
  // Custom suggestion tiles
  triggers: [
    TriggerConfig(
      trigger: '@',
      suggestions: mentions,
      suggestionBuilder: (context, match, onSelect) {
        return ListTile(
          leading: CircleAvatar(backgroundImage: NetworkImage(match.imageUrl!)),
          title: Text('@${match.label}'),
          subtitle: Text(match.subtitle ?? ''),
          onTap: onSelect,
        );
      },
    ),
  ],

  // Custom action buttons
  actions: [
    ExtraFieldAction.media(
      icon: Icons.photo,
      onPickMedia: pickMedia,
      buttonBuilder: (context, onTap) {
        return ElevatedButton.icon(
          onPressed: onTap,
          icon: Icon(Icons.photo),
          label: Text('Photo'),
        );
      },
    ),
    ExtraFieldAction.stickers(
      icon: Icons.emoji_emotions,
      packs: packs,
      // Custom sticker item
      stickerBuilder: (context, sticker, isSelected, onTap) {
        return GestureDetector(
          onTap: onTap,
          child: AnimatedContainer(
            duration: Duration(milliseconds: 200),
            decoration: BoxDecoration(
              border: isSelected ? Border.all(color: Colors.blue, width: 2) : null,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Center(child: Text(sticker.emoji ?? '', style: TextStyle(fontSize: 28))),
          ),
        );
      },
    ),
  ],

  // Custom send button
  sendButtonBuilder: (context, onSend) {
    return GradientButton(onPressed: onSend, child: Icon(Icons.send));
  },
)

Arabic / RTL #

Directionality(
  textDirection: TextDirection.rtl,
  child: ExtraFieldWidget(
    config: ExtraFieldConfig(
      strings: ExtraFieldStrings(
        hintText: '\u0627\u0643\u062A\u0628 \u0631\u0633\u0627\u0644\u0629...',
        send: '\u0625\u0631\u0633\u0627\u0644',
        attachMedia: '\u0625\u0631\u0641\u0627\u0642 \u0648\u0633\u0627\u0626\u0637',
        image: '\u0635\u0648\u0631\u0629',
        video: '\u0641\u064A\u062F\u064A\u0648',
        audio: '\u0635\u0648\u062A',
        file: '\u0645\u0644\u0641',
        pickLocation: '\u0627\u062E\u062A\u064A\u0627\u0631 \u0627\u0644\u0645\u0648\u0642\u0639',
        stickers: '\u0645\u0644\u0635\u0642\u0627\u062A',
        remove: '\u062D\u0630\u0641',
        noSuggestions: '\u0644\u0627 \u062A\u0648\u062C\u062F \u0646\u062A\u0627\u0626\u062C',
        maxMediaReached: (max) => '\u0627\u0644\u062D\u062F \u0627\u0644\u0623\u0642\u0635\u0649 $max \u0639\u0646\u0627\u0635\u0631',
        maxStickersReached: (max) => '\u0627\u0644\u062D\u062F \u0627\u0644\u0623\u0642\u0635\u0649 $max \u0645\u0644\u0635\u0642\u0627\u062A',
      ),
      triggers: [...],
      actions: [...],
      onSend: (value) => send(value),
    ),
  ),
)

Architecture #

ExtraFieldWidget
  \u251C\u2500 AttachmentPreview       \u2190 media thumbnails, location chips, sticker previews
  \u251C\u2500 TextField               \u2190 with TriggerTextEditingController (styled spans)
  \u251C\u2500 TriggerSuggestions      \u2190 overlay above text field
  \u251C\u2500 Action Bar              \u2190 ExtraFieldAction buttons + send button
  \u2514\u2500 Expandable Panel        \u2190 StickersPanel or custom panelBuilder

Key Classes #

Class Description
ExtraFieldWidget Main composer widget. Drop into any layout.
ExtraFieldConfig All configuration: text, triggers, actions, send, validators, theme, strings.
ExtraFieldController State controller. Text, attachments, location, stickers, panels, validation.
ExtraFieldValue Immutable snapshot of composed data. Serializable via toMap()/fromMap().
TriggerConfig Defines a trigger character, search callback, styling, and suggestion UI.
TriggerMatch A detected or suggested trigger match (e.g. #Flutter, @john).
ExtraFieldAction Action button definition. Named constructors: .media(), .stickers(), .location(), .custom().
AsyncValidator Async validation rule with optional auto-validate on change and debounce.
ValidationResult Validation outcome with isValid, errors map, and errorFor(fieldId).
ExtraFieldThemeData Theme overrides for colors, padding, border radius, icon theme, and per-module themes.
ExtraFieldStrings All user-facing strings. Override for localization.

Models #

Model Description
MediaItem Media attachment with type, name, path, url, size, duration, thumbnailUrl.
MediaType Enum: image, video, audio, file.
LocationData Location with latitude, longitude, name, address, placeId, url.
Sticker Sticker with id, url, emoji, name, packId, extra.
StickerPack Pack with id, name, icon, stickers, extra.

Theming #

ExtraFieldConfig(
  theme: ExtraFieldThemeData(
    primaryColor: Colors.indigo,
    backgroundColor: Colors.white,
    surfaceColor: Colors.grey.shade100,
    borderRadius: BorderRadius.circular(12),
    padding: EdgeInsets.all(12),
    labelStyle: TextStyle(fontSize: 16),
    hintStyle: TextStyle(color: Colors.grey),
    iconTheme: IconThemeData(size: 22, color: Colors.indigo),
    stickersTheme: StickersThemeData(
      selectedBorderColor: Colors.indigo,
      tabIndicatorColor: Colors.indigo,
      spacing: 8,
    ),
  ),
)

Or provide it via InheritedWidget:

ExtraFieldTheme(
  data: myTheme,
  child: ExtraFieldWidget(config: config),
)

// Access anywhere:
final theme = ExtraFieldTheme.of(context);

Serialization #

All models and the composed value support toMap() / fromMap():

// Save
final map = controller.toMap();
await storage.save('draft', jsonEncode(map));

// Restore
final saved = jsonDecode(await storage.load('draft'));
final value = ExtraFieldValue.fromMap(saved);
controller.loadValue(value);

Example App #

The example directory contains a comprehensive demo app with 6 screens:

  1. Chat Composer — WhatsApp/Telegram style with triggers, media, stickers, location, and custom panels.
  2. Social Post — Twitter/X style with character limit, custom theme, $variable triggers, and custom send button.
  3. Arabic / RTL — Full RTL support with Arabic strings.
  4. Custom Builders — Custom suggestion tiles with avatars, pill-shaped action buttons, animated sticker selection, gradient send button.
  5. Validators — Async validation rules with auto-validate on change and manual trigger.
  6. Programmatic Control — External controller with buttons for every method and live state output.

Run it:

cd example
flutter run

Zero Dependencies #

This package has no external dependencies beyond Flutter itself. Platform-specific functionality (image picker, camera, maps, etc.) is provided by your app through callbacks:

// You provide the implementation:
ExtraFieldAction.media(
  onPickMedia: (type) async {
    // Use image_picker, file_picker, or any package you prefer
    final file = await ImagePicker().pickImage(source: ImageSource.gallery);
    return file != null ? [MediaItem(type: type, path: file.path)] : null;
  },
)

ExtraFieldAction.location(
  onPickLocation: () async {
    // Use google_maps, geolocator, or any package you prefer
    final loc = await Navigator.push(context, MaterialPageRoute(builder: (_) => MapPicker()));
    return loc != null ? LocationData(latitude: loc.lat, longitude: loc.lng) : null;
  },
)

Additional Information #

  • API Documentation: Full dartdoc on every public class, method, field, and parameter.
  • Issues & Feedback: GitHub Issues
  • License: MIT
0
likes
150
points
40
downloads
screenshot

Publisher

unverified uploader

Weekly Downloads

A rich composer input field for Flutter — inline trigger detection (#, @, $), action buttons, sticker panels, attachment previews, async validators, and a send button. Zero external dependencies.

Repository (GitHub)
View/report issues

Topics

#input #composer #chat #mentions #widget

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on extra_field