voice_guidance 0.6.2 copy "voice_guidance: ^0.6.2" to clipboard
voice_guidance: ^0.6.2 copied to clipboard

Platform-agnostic voice guidance for navigation apps. Works on mobile (flutter_tts), Linux (spd-say), and silently in CI.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:navigation_safety/navigation_safety.dart';
import 'package:routing_engine/routing_engine.dart';
import 'package:voice_guidance/voice_guidance.dart';

/// Local adapter mirroring lib/adapters/navigation_route_adapter.dart.
/// See that file for rationale; the production adapter is internal to
/// the main app and not exported from any package public barrel.
extension _RouteResultToNavigation on RouteResult {
  NavigationRoute toNavigationRoute() => NavigationRoute(
    shape: shape,
    maneuvers: maneuvers
        .map(
          (m) => NavigationManeuver(
            index: m.index,
            instruction: m.instruction,
            type: m.type,
            lengthKm: m.lengthKm,
            timeSeconds: m.timeSeconds,
            position: m.position,
          ),
        )
        .toList(),
    totalDistanceKm: totalDistanceKm,
    totalTimeSeconds: totalTimeSeconds,
    summary: summary,
  );
}

final _exampleRoute = RouteResult(
  shape: const [LatLng(35.1709, 136.9066), LatLng(34.9551, 137.1771)],
  maneuvers: const [
    RouteManeuver(
      index: 0,
      instruction: 'Depart Sakae Station',
      type: 'depart',
      lengthKm: 40,
      timeSeconds: 2400,
      position: LatLng(35.1709, 136.9066),
    ),
    RouteManeuver(
      index: 1,
      instruction: 'In 500 metres, turn right onto Route 1',
      type: 'turn',
      lengthKm: 20,
      timeSeconds: 1200,
      position: LatLng(35.0800, 137.0500),
    ),
    RouteManeuver(
      index: 2,
      instruction: 'Arrive at Higashiokazaki Station',
      type: 'arrive',
      lengthKm: 0,
      timeSeconds: 0,
      position: LatLng(34.9551, 137.1771),
    ),
  ],
  totalDistanceKm: 40.0,
  totalTimeSeconds: 2400,
  summary: '40 km, 40 min',
  engineInfo: const EngineInfo(name: 'mock'),
);

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'voice_guidance example',
      home: MultiBlocProvider(
        providers: [
          BlocProvider(
            create: (_) => NavigationBloc()
              ..add(
                NavigationStarted(route: _exampleRoute.toNavigationRoute()),
              ),
          ),
        ],
        child: const _ExampleScreen(),
      ),
    );
  }
}

class _ExampleScreen extends StatefulWidget {
  const _ExampleScreen();

  @override
  State<_ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<_ExampleScreen> {
  late final VoiceGuidanceBloc _voiceBloc;

  @override
  void initState() {
    super.initState();
    // NoOpTtsEngine is silent — safe for all environments (CI, Linux, test).
    // Replace with DefaultTtsEngine() on a real device.
    _voiceBloc = VoiceGuidanceBloc(
      ttsEngine: NoOpTtsEngine(),
      navigationStateStream: context.read<NavigationBloc>().stream,
    );
  }

  @override
  void dispose() {
    _voiceBloc.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider.value(
      value: _voiceBloc,
      child: Scaffold(
        appBar: AppBar(title: const Text('voice_guidance example')),
        body: BlocBuilder<VoiceGuidanceBloc, VoiceGuidanceState>(
          bloc: _voiceBloc,
          builder: (context, voiceState) {
            return Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _StatusCard(voiceState: voiceState),
                  const SizedBox(height: 24),
                  _ControlRow(voiceBloc: _voiceBloc, voiceState: voiceState),
                  const SizedBox(height: 24),
                  const _ManeuverAnnounceSection(),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  final VoiceGuidanceState voiceState;

  const _StatusCard({required this.voiceState});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Status: ${voiceState.status.name}',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            if (voiceState.lastSpokenText != null) ...[
              const SizedBox(height: 8),
              Text('Last spoken: "${voiceState.lastSpokenText}"'),
            ],
            if (voiceState.lastManeuverIndex != null) ...[
              const SizedBox(height: 4),
              Text('Last maneuver index: ${voiceState.lastManeuverIndex}'),
            ],
          ],
        ),
      ),
    );
  }
}

class _ControlRow extends StatelessWidget {
  final VoiceGuidanceBloc voiceBloc;
  final VoiceGuidanceState voiceState;

  const _ControlRow({required this.voiceBloc, required this.voiceState});

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 12,
      children: [
        FilledButton.icon(
          onPressed: voiceState.isMuted
              ? () => voiceBloc.add(const VoiceEnabled())
              : null,
          icon: const Icon(Icons.volume_up),
          label: const Text('Enable'),
        ),
        OutlinedButton.icon(
          onPressed: !voiceState.isMuted
              ? () => voiceBloc.add(const VoiceDisabled())
              : null,
          icon: const Icon(Icons.volume_off),
          label: const Text('Mute'),
        ),
      ],
    );
  }
}

class _ManeuverAnnounceSection extends StatelessWidget {
  const _ManeuverAnnounceSection();

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Manual announcements',
          style: Theme.of(context).textTheme.titleSmall,
        ),
        const SizedBox(height: 12),
        Wrap(
          spacing: 12,
          runSpacing: 12,
          children: [
            FilledButton(
              onPressed: () => context.read<VoiceGuidanceBloc>().add(
                const ManeuverAnnounced(text: 'In 300 metres, turn right'),
              ),
              child: const Text('Announce maneuver'),
            ),
            FilledButton(
              onPressed: () => context.read<VoiceGuidanceBloc>().add(
                const HazardAnnounced(
                  message: 'Icy road conditions ahead',
                  severity: AlertSeverity.warning,
                ),
              ),
              child: const Text('Announce hazard'),
            ),
          ],
        ),
      ],
    );
  }
}
0
likes
150
points
164
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Platform-agnostic voice guidance for navigation apps. Works on mobile (flutter_tts), Linux (spd-say), and silently in CI.

Repository (GitHub)
View/report issues
Contributing

Topics

#tts #voice #navigation #accessibility #flutter

License

BSD-3-Clause (license)

Dependencies

equatable, flutter, flutter_bloc, flutter_tts, navigation_safety, navigation_safety_core, routing_engine

More

Packages that depend on voice_guidance