native_haptics_and_audio 2.0.0 copy "native_haptics_and_audio: ^2.0.0" to clipboard
native_haptics_and_audio: ^2.0.0 copied to clipboard

Ultra-low latency native haptic and audio feedback designed specifically for high-speed performance on iOS and Android.

example/lib/main.dart

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

void main() {
  runApp(const ExampleApp());
}

/// An audio asset owned by this app rather than the plugin.
const CustomSound kCustomDemo = CustomSound('assets/custom_demo.m4a');

/// The sound pinned at startup, standing in for a latency-critical hot path.
const NativeSound kPinnedSound = NativeSound.scannerBeep;

/// Kept deliberately small so LRU eviction is visible while tapping around.
const int kMaxCachedSounds = 5;

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'native_haptics_and_audio',
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final NativeHapticsAndAudioRepository _repo = NativeHapticsAndAudioRepository.instance;

  bool _ready = false;
  double _volume = 1.0;
  double _rate = 1.0;
  String? _lastPlayed;

  @override
  void initState() {
    super.initState();
    _boot();
  }

  Future<void> _boot() async {
    await _repo.initialize(maxCachedSounds: kMaxCachedSounds, respectSilentSwitch: false);

    // Pin the hot sound so it is never evicted, no matter what else plays.
    await _repo.preload(kPinnedSound);

    if (!mounted) return;
    setState(() => _ready = true);
  }

  @override
  void dispose() {
    _repo.release();
    super.dispose();
  }

  Future<void> _play(Sound sound, String label) async {
    await _repo.play(sound, volume: _volume, rate: _rate);
    if (!mounted) return;
    setState(() => _lastPlayed = label);
  }

  Future<void> _haptic(HapticPattern pattern) => _repo.playHaptic(pattern);

  Future<void> _unloadAll() async {
    await _repo.unloadAll();
    if (!mounted) return;
    setState(() => _lastPlayed = null);
  }

  Future<void> _repin() async {
    await _repo.preload(kPinnedSound);
    if (!mounted) return;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('native_haptics_and_audio'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_sweep_outlined),
            tooltip: 'Unload all sounds',
            onPressed: _unloadAll,
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _MemoryPanel(
            pinned: _repo.pinnedAssets,
            cached: _repo.cachedAssets,
            maxCachedSounds: _repo.maxCachedSounds,
            lastPlayed: _lastPlayed,
            onRepin: _repin,
          ),
          const SizedBox(height: 24),
          const _SectionHeader(
            title: 'Playback',
            subtitle: 'Applies to every sound below',
          ),
          _SliderTile(
            label: 'Volume',
            value: _volume,
            min: 0,
            max: 1,
            onChanged: (double v) => setState(() => _volume = v),
          ),
          _SliderTile(
            label: 'Rate',
            value: _rate,
            min: 0.5,
            max: 2,
            onChanged: (double v) => setState(() => _rate = v),
          ),
          const SizedBox(height: 24),
          _SectionHeader(
            title: 'Built-in sounds',
            subtitle:
                '${NativeSound.values.length} bundled clips. '
                'Tapping an unloaded sound loads it on demand.',
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: NativeSound.values.map((NativeSound sound) {
              final bool pinned = sound == kPinnedSound;
              final bool loaded = _repo.isLoaded(sound);
              return FilledButton.tonalIcon(
                onPressed: () => _play(sound, sound.name),
                icon: Icon(
                  pinned
                      ? Icons.push_pin
                      : loaded
                      ? Icons.memory
                      : Icons.download_outlined,
                  size: 18,
                ),
                label: Text(sound.name),
              );
            }).toList(),
          ),
          const SizedBox(height: 24),
          const _SectionHeader(
            title: 'Custom sound',
            subtitle: 'An asset owned by this app, loaded through the same path.',
          ),
          const SizedBox(height: 8),
          FilledButton.icon(
            onPressed: () => _play(kCustomDemo, 'custom_demo'),
            icon: const Icon(Icons.audio_file_outlined),
            label: const Text('assets/custom_demo.m4a'),
          ),
          const SizedBox(height: 24),
          const _SectionHeader(title: 'Haptics'),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: HapticPattern.values.map((HapticPattern pattern) {
              return OutlinedButton.icon(
                onPressed: () => _haptic(pattern),
                icon: const Icon(Icons.vibration, size: 18),
                label: Text(pattern.name),
              );
            }).toList(),
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

/// Live view of what the plugin is holding in native RAM.
class _MemoryPanel extends StatelessWidget {
  const _MemoryPanel({
    required this.pinned,
    required this.cached,
    required this.maxCachedSounds,
    required this.lastPlayed,
    required this.onRepin,
  });

  final Set<String> pinned;
  final List<String> cached;
  final int maxCachedSounds;
  final String? lastPlayed;
  final VoidCallback onRepin;

  /// Trims the asset key down to the bare filename for display.
  static String _short(String assetKey) => assetKey.split('/').last.replaceAll('.m4a', '');

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Card(
      margin: EdgeInsets.zero,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text('Native memory', style: theme.textTheme.titleMedium),
                ),
                if (pinned.isEmpty) TextButton(onPressed: onRepin, child: const Text('Re-pin')),
              ],
            ),
            const SizedBox(height: 12),
            _Tier(
              icon: Icons.push_pin,
              label: 'Pinned',
              detail: 'never evicted',
              items: pinned.map(_short).toList(),
              emptyText: 'nothing pinned',
            ),
            const SizedBox(height: 12),
            _Tier(
              icon: Icons.memory,
              label: 'Cached (${cached.length}/$maxCachedSounds)',
              detail: 'least recently used first',
              items: cached.map(_short).toList(),
              emptyText: 'cache empty',
            ),
            if (lastPlayed != null) ...<Widget>[
              const Divider(height: 24),
              Text(
                'Last played: $lastPlayed',
                style: theme.textTheme.bodySmall,
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _Tier extends StatelessWidget {
  const _Tier({
    required this.icon,
    required this.label,
    required this.detail,
    required this.items,
    required this.emptyText,
  });

  final IconData icon;
  final String label;
  final String detail;
  final List<String> items;
  final String emptyText;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Icon(icon, size: 16, color: theme.colorScheme.primary),
            const SizedBox(width: 6),
            Text(label, style: theme.textTheme.labelLarge),
            const SizedBox(width: 6),
            Text(
              detail,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ],
        ),
        const SizedBox(height: 6),
        if (items.isEmpty)
          Text(
            emptyText,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
              fontStyle: FontStyle.italic,
            ),
          )
        else
          Wrap(
            spacing: 6,
            runSpacing: 6,
            children: items
                .map(
                  (String name) => Chip(
                    label: Text(name),
                    visualDensity: VisualDensity.compact,
                    materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  ),
                )
                .toList(),
          ),
      ],
    );
  }
}

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({required this.title, this.subtitle});

  final String title;
  final String? subtitle;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(title, style: theme.textTheme.titleMedium),
        if (subtitle != null)
          Padding(
            padding: const EdgeInsets.only(top: 2),
            child: Text(
              subtitle!,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
      ],
    );
  }
}

class _SliderTile extends StatelessWidget {
  const _SliderTile({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        SizedBox(width: 56, child: Text(label)),
        Expanded(
          child: Slider(
            value: value,
            min: min,
            max: max,
            onChanged: onChanged,
          ),
        ),
        SizedBox(
          width: 44,
          child: Text(value.toStringAsFixed(2), textAlign: TextAlign.end),
        ),
      ],
    );
  }
}
3
likes
160
points
140
downloads

Documentation

API reference

Publisher

verified publisherandresmontano.dev

Weekly Downloads

Ultra-low latency native haptic and audio feedback designed specifically for high-speed performance on iOS and Android.

Repository (GitHub)
View/report issues

Topics

#haptics #audio #pos #barcode-scanner #native

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on native_haptics_and_audio

Packages that implement native_haptics_and_audio