added local implementation of speech_to_text_dialog

This commit is contained in:
Fabian Freund
2026-01-13 12:07:32 +01:00
parent 849823f5df
commit 565595a0be
47 changed files with 1612 additions and 19 deletions
@@ -17,31 +17,60 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart'; import 'package:speech_to_text_dialog/speech_to_text_dialog.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper; import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class SpeechToTextButton extends StatelessWidget { class SpeechToTextButton extends StatefulWidget {
final Function(dynamic data) onTextReceived; final Function(String text) onTextReceived;
const SpeechToTextButton({required this.onTextReceived, super.key}); const SpeechToTextButton({required this.onTextReceived, super.key});
@override
State<SpeechToTextButton> createState() => _SpeechToTextButtonState();
}
class _SpeechToTextButtonState extends State<SpeechToTextButton> {
final _speechDialog = SpeechToTextDialog();
StreamSubscription<String>? _textSubscription;
@override
void dispose() {
_textSubscription?.cancel().ignore();
_speechDialog.dispose();
super.dispose();
}
Future<void> _showSpeechDialog(BuildContext context) async {
// Cancel any existing subscription
await _textSubscription?.cancel();
// Listen for the next text result
_textSubscription = _speechDialog.textStream.take(1).listen((text) {
if (text.isNotEmpty) {
widget.onTextReceived(text);
}
});
// Show the dialog
final isServiceAvailable = await _speechDialog.showDialog(
// locale: "en-US",
);
if (!isServiceAvailable) {
if (context.mounted) {
ui_helper.showErrorMessage(context, 'Service is not available');
}
await _textSubscription?.cancel();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
onPressed: () async { onPressed: () => _showSpeechDialog(context),
final isServiceAvailable = await SpeechToTextGoogleDialog.getInstance()
.showGoogleDialog(
onTextReceived: onTextReceived,
// locale: "en-US",
);
if (!isServiceAvailable) {
if (context.mounted) {
ui_helper.showErrorMessage(context, 'Service is not available');
}
}
},
icon: const Icon(Icons.mic), icon: const Icon(Icons.mic),
); );
} }
+2 -3
View File
@@ -77,9 +77,8 @@ dependencies:
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
smooth_page_indicator: ^2.0.1 smooth_page_indicator: ^2.0.1
socks5_proxy: ^2.1.1 socks5_proxy: ^2.1.1
speech_to_text_google_dialog: speech_to_text_dialog:
git: path: ../packages/speech_to_text_dialog
url: https://github.com/FaFre/speech_to_text_google_dialog.git
sqlite3: ^2.9.4 sqlite3: ^2.9.4
sqlite3_flutter_libs: ^0.5.41 sqlite3_flutter_libs: ^0.5.41
synchronized: ^3.4.0 synchronized: ^3.4.0
+137
View File
@@ -0,0 +1,137 @@
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
PlatformException _createConnectionError(String channelName) {
return PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
}
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
if (error == null) {
return <Object?>[result];
}
return <Object?>[error.code, error.message, error.details];
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
default:
return super.readValueOfType(type, buffer);
}
}
}
/// Host API - methods called from Flutter to native Android.
class SpeechToTextApi {
/// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// Show the speech recognition dialog.
///
/// Returns [true] if the dialog was shown successfully.
/// Returns [false] if the speech recognition service is not available.
///
/// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
Future<bool> showDialog({String? locale}) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[locale]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
}
/// Flutter API - callbacks from native Android to Flutter.
abstract class SpeechToTextEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// Called when speech recognition completes with the recognized text.
///
/// [text] contains the recognized speech text. May be empty if
/// recognition failed or was cancelled.
void onTextReceived(String text);
static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_text = (args[0] as String?);
assert(arg_text != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.');
try {
api.onTextReceived(arg_text!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
+1
View File
@@ -0,0 +1 @@
TODO: Add your license here.
+15
View File
@@ -0,0 +1,15 @@
# speech_to_text_dialog
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
@@ -0,0 +1,66 @@
group = "eu.weblibre.speech_to_text_dialog"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.2.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "eu.weblibre.speech_to_text_dialog"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 24
}
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
@@ -0,0 +1 @@
rootProject.name = 'speech_to_text_dialog'
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.weblibre.speech_to_text_dialog">
</manifest>
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.weblibre.speech_to_text_dialog
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.speech.RecognizerIntent
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.PluginRegistry
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextApi as PigeonSpeechToTextApi
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextEvents
import java.util.Locale
/// Plugin implementation for speech recognition dialog.
///
/// This plugin integrates Android's RecognizerIntent to provide
/// speech-to-text functionality to Flutter apps.
class SpeechToTextDialogPlugin : FlutterPlugin, ActivityAware,
PluginRegistry.ActivityResultListener {
companion object {
private const val REQ_CODE_SPEECH_INPUT = 120752
}
private var activity: Activity? = null
private var speechApiHost: SpeechApiHost? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
speechApiHost = SpeechApiHost(binding.binaryMessenger, this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
speechApiHost?.release()
speechApiHost = null
}
override fun onAttachedToActivity(@NonNull binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(@NonNull binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivity() {
activity = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQ_CODE_SPEECH_INPUT) {
speechApiHost?.handleActivityResult(resultCode, data)
return true
}
return false
}
/// Show the speech recognition dialog.
///
/// @param locale The locale string (e.g., "en-US", "de-DE") or null for device default.
/// @return true if the dialog was shown successfully, false otherwise.
fun showSpeechDialog(locale: String?): Boolean {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(
RecognizerIntent.EXTRA_LANGUAGE,
locale?.let { Locale(it) } ?: Locale.getDefault()
)
putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak")
}
return try {
activity?.startActivityForResult(intent, REQ_CODE_SPEECH_INPUT)
true
} catch (e: ActivityNotFoundException) {
false
}
}
}
/// Host API implementation that handles Flutter-to-Android calls.
class SpeechApiHost(
private val messenger: BinaryMessenger,
private val plugin: SpeechToTextDialogPlugin
) {
private val events: SpeechToTextEvents = SpeechToTextEvents(messenger)
init {
// Set up the API handler
PigeonSpeechToTextApi.setUp(messenger, object : PigeonSpeechToTextApi {
override fun showDialog(locale: String?): Boolean {
return plugin.showSpeechDialog(locale)
}
})
}
/// Handle the result from the speech recognition activity.
fun handleActivityResult(resultCode: Int, data: Intent?) {
sendResult(resultCode, data)
}
private fun sendResult(resultCode: Int, data: Intent?) {
val text = if (resultCode == Activity.RESULT_OK && data != null) {
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (!result.isNullOrEmpty()) {
result[0]
} else {
""
}
} else {
// User cancelled or recognition failed
""
}
events.onTextReceived(text) { }
}
fun release() {
PigeonSpeechToTextApi.setUp(messenger, null)
}
}
@@ -0,0 +1,143 @@
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package eu.weblibre.speech_to_text_dialog.pigeons
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object SpeechToTextPigeonUtils {
fun createConnectionError(channelName: String): FlutterError {
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
private open class SpeechToTextPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
super.writeValue(stream, value)
}
}
/**
* Host API - methods called from Flutter to native Android.
*
* Generated interface from Pigeon that represents a handler of messages from Flutter.
*/
interface SpeechToTextApi {
/**
* Show the speech recognition dialog.
*
* Returns [true] if the dialog was shown successfully.
* Returns [false] if the speech recognition service is not available.
*
* The [locale] parameter specifies the language locale for recognition
* (e.g., 'en-US', 'de-DE'). If null, uses the device default.
*/
fun showDialog(locale: String?): Boolean
companion object {
/** The codec used by SpeechToTextApi. */
val codec: MessageCodec<Any?> by lazy {
SpeechToTextPigeonCodec()
}
/** Sets up an instance of `SpeechToTextApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: SpeechToTextApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val localeArg = args[0] as String?
val wrapped: List<Any?> = try {
listOf(api.showDialog(localeArg))
} catch (exception: Throwable) {
SpeechToTextPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/**
* Flutter API - callbacks from native Android to Flutter.
*
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
*/
class SpeechToTextEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by SpeechToTextEvents. */
val codec: MessageCodec<Any?> by lazy {
SpeechToTextPigeonCodec()
}
}
/**
* Called when speech recognition completes with the recognized text.
*
* [text] contains the recognized speech text. May be empty if
* recognition failed or was cancelled.
*/
fun onTextReceived(textArg: String, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(textArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(SpeechToTextPigeonUtils.createConnectionError(channelName)))
}
}
}
}
@@ -0,0 +1,27 @@
package eu.weblibre.speech_to_text_dialog
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.mockito.Mockito
import kotlin.test.Test
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class SpeechToTextDialogPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = SpeechToTextDialogPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
@@ -0,0 +1,16 @@
# speech_to_text_dialog_example
Demonstrates how to use the speech_to_text_dialog plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "eu.weblibre.speech_to_text_dialog_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "eu.weblibre.speech_to_text_dialog_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="speech_to_text_dialog_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package eu.weblibre.speech_to_text_dialog_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
@@ -0,0 +1,33 @@
// This is a basic Flutter integration test.
//
// Since integration tests run in a full Flutter application, they can interact
// with the host side of a plugin implementation, unlike Dart unit tests.
//
// For more information about Flutter integration tests, please see
// https://flutter.dev/to/integration-testing
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:speech_to_text_dialog/speech_to_text_dialog.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('showDialog returns bool', (WidgetTester tester) async {
final SpeechToTextDialog plugin = SpeechToTextDialog();
final bool result = await plugin.showDialog();
// The result indicates if the dialog was shown successfully.
// In a test environment without proper activity, this may return false.
expect(result, isA<bool>());
plugin.dispose();
});
testWidgets('textStream emits String values', (WidgetTester tester) async {
final SpeechToTextDialog plugin = SpeechToTextDialog();
final Stream<String> stream = plugin.textStream;
expect(stream, isA<Stream<String>>());
plugin.dispose();
});
}
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:speech_to_text_dialog/speech_to_text_dialog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _speechToTextDialog = SpeechToTextDialog();
StreamSubscription<String>? _textSubscription;
String _recognizedText = 'Tap the button to start speaking...';
String _selectedLocale = 'en-US';
final List<String> _locales = [
'en-US',
'de-DE',
'es-ES',
'fr-FR',
'it-IT',
'pt-BR',
'ru-RU',
'zh-CN',
'ja-JP',
'ko-KR',
];
@override
void initState() {
super.initState();
// Listen for recognized text
_textSubscription = _speechToTextDialog.textStream.listen((text) {
setState(() {
if (text.isEmpty) {
_recognizedText = 'No speech detected or cancelled';
} else {
_recognizedText = text;
}
});
});
}
@override
void dispose() {
_textSubscription?.cancel();
_speechToTextDialog.dispose();
super.dispose();
}
Future<void> _startListening() async {
setState(() {
_recognizedText = 'Listening...';
});
final success = await _speechToTextDialog.showDialog(
locale: _selectedLocale,
);
if (!success) {
setState(() {
_recognizedText = 'Speech recognition not available';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Speech to Text Dialog'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.mic,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 24),
Text(
_recognizedText,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
DropdownButton<String>(
value: _selectedLocale,
hint: const Text('Select Language'),
items: _locales.map((String locale) {
return DropdownMenuItem<String>(
value: locale,
child: Text(locale),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedLocale = newValue;
});
}
},
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _startListening,
icon: const Icon(Icons.mic),
label: const Text('Start Speech Recognition'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,86 @@
name: speech_to_text_dialog_example
description: "Demonstrates how to use the speech_to_text_dialog plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none'
resolution: workspace
environment:
sdk: ^3.10.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
speech_to_text_dialog:
# When depending on this package from a real application you should use:
# speech_to_text_dialog: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:speech_to_text_dialog_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:flutter/services.dart' show BinaryMessenger;
import 'src/pigeons/speech_to_text.g.dart';
/// Speech recognition dialog using Android's RecognizerIntent.
///
/// This class provides a Flutter interface to Android's speech recognition
/// dialog, allowing users to dictate text that is then returned to the app.
///
/// Example usage:
/// ```dart
/// final speechDialog = SpeechToTextDialog();
///
/// // Listen for results
/// final subscription = speechDialog.textStream.listen((text) {
/// print('Recognized: $text');
/// });
///
/// // Show the dialog
/// final success = await speechDialog.showDialog(locale: 'en-US');
///
/// // Clean up when done
/// await subscription.cancel();
/// speechDialog.dispose();
/// ```
class SpeechToTextDialog implements SpeechToTextEvents {
/// Creates a new [SpeechToTextDialog] instance.
///
/// Optionally provide a custom [api] for testing or dependency injection.
/// If [binaryMessenger] is provided, it will be used for Pigeon communication.
SpeechToTextDialog({
SpeechToTextApi? api,
BinaryMessenger? binaryMessenger,
}) : _api = api ?? SpeechToTextApi(binaryMessenger: binaryMessenger),
_binaryMessenger = binaryMessenger {
// Set up the event handler
SpeechToTextEvents.setUp(this, binaryMessenger: binaryMessenger);
}
final SpeechToTextApi _api;
final BinaryMessenger? _binaryMessenger;
final _textStreamController = StreamController<String>.broadcast();
bool _disposed = false;
/// Stream of recognized speech text.
///
/// This stream emits the recognized text when the user completes
/// speech input. An empty string may be emitted if recognition failed
/// or the user cancelled.
Stream<String> get textStream => _textStreamController.stream;
/// Show the speech recognition dialog.
///
/// Returns [true] if the dialog was shown successfully.
/// Returns [false] if the speech recognition service is not available.
///
/// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE', 'es-ES'). If null, uses the device default locale.
///
/// Example:
/// ```dart
/// final success = await speechDialog.showDialog(locale: 'en-US');
/// if (!success) {
/// print('Speech recognition not available');
/// }
/// ```
Future<bool> showDialog({String? locale}) async {
return _api.showDialog(locale: locale);
}
@override
void onTextReceived(String text) {
if (!_disposed) {
_textStreamController.add(text);
}
}
/// Release resources used by this instance.
///
/// Call this when the speech dialog is no longer needed to avoid memory leaks.
/// After calling [dispose], this instance cannot be used again.
void dispose() {
if (!_disposed) {
_disposed = true;
SpeechToTextEvents.setUp(null, binaryMessenger: _binaryMessenger);
_textStreamController.close();
}
}
}
@@ -0,0 +1,137 @@
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
PlatformException _createConnectionError(String channelName) {
return PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
}
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
if (error == null) {
return <Object?>[result];
}
return <Object?>[error.code, error.message, error.details];
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
default:
return super.readValueOfType(type, buffer);
}
}
}
/// Host API - methods called from Flutter to native Android.
class SpeechToTextApi {
/// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// Show the speech recognition dialog.
///
/// Returns [true] if the dialog was shown successfully.
/// Returns [false] if the speech recognition service is not available.
///
/// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
Future<bool> showDialog({String? locale}) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[locale]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
}
/// Flutter API - callbacks from native Android to Flutter.
abstract class SpeechToTextEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// Called when speech recognition completes with the recognized text.
///
/// [text] contains the recognized speech text. May be empty if
/// recognition failed or was cancelled.
void onTextReceived(String text);
static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_text = (args[0] as String?);
assert(arg_text != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.');
try {
api.onTextReceived(arg_text!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:pigeon/pigeon.dart';
/// Pigeon API for speech recognition dialog.
///
/// This API provides type-safe communication between Flutter and Android
/// for showing the Google speech recognition dialog.
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/src/pigeons/speech_to_text.g.dart',
dartOptions: DartOptions(),
kotlinOut:
'android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt',
kotlinOptions: KotlinOptions(
package: 'eu.weblibre.speech_to_text_dialog.pigeons',
),
dartPackageName: 'speech_to_text_dialog',
),
)
/// Host API - methods called from Flutter to native Android.
@HostApi()
abstract class SpeechToTextApi {
/// Show the speech recognition dialog.
///
/// Returns [true] if the dialog was shown successfully.
/// Returns [false] if the speech recognition service is not available.
///
/// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
bool showDialog({String? locale});
}
/// Flutter API - callbacks from native Android to Flutter.
@FlutterApi()
abstract class SpeechToTextEvents {
/// Called when speech recognition completes with the recognized text.
///
/// [text] contains the recognized speech text. May be empty if
/// recognition failed or was cancelled.
void onTextReceived(String text);
}
@@ -0,0 +1,26 @@
name: speech_to_text_dialog
description: Speech recognition dialog using Android's RecognizerIntent with type-safe Pigeon channels.
version: 0.0.1
publish_to: 'none'
resolution: workspace
environment:
sdk: ^3.10.4
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
pigeon: ^26.1.5
flutter:
plugin:
platforms:
android:
package: eu.weblibre.speech_to_text_dialog
pluginClass: SpeechToTextDialogPlugin
+2
View File
@@ -12,6 +12,8 @@ workspace:
- packages/simple_intent_receiver/example - packages/simple_intent_receiver/example
- packages/locale_resolver - packages/locale_resolver
- packages/locale_resolver/example - packages/locale_resolver/example
- packages/speech_to_text_dialog
- packages/speech_to_text_dialog/example
dev_dependencies: dev_dependencies:
melos: ^7.3.0 melos: ^7.3.0