feat: expose hasOnDeviceSupport so callers can check before requesting onDevice recognition - #675
Merged
Conversation
…nDevice
There is currently no way to ask whether a device can recognize speech
offline, and the two platforms disagree about what happens when you request
it anyway. Android checks `isOnDeviceRecognitionAvailable` and falls through
to the network recognizer; iOS returns an `onDeviceError` and the listen
session does not run. So `onDevice: true` is only safe to pass on Android,
and "prefer on-device, fall back to the network" cannot be expressed on iOS
at all.
The information already exists on both sides — iOS computes
`supportsOnDeviceRecognition` into `onDeviceStatus` during setup, Android
calls `isOnDeviceRecognitionAvailable` inside `createRecognizer` — it is
simply never surfaced. This exposes it.
- platform interface: `hasOnDeviceSupport({String? localeId})`, defaulting to
`UnimplementedError` like the other methods, with a method-channel
implementation that answers false rather than throwing when a platform has
no handler for it. An app that checks first then degrades to the network
recognizer behaves exactly as it would have without the check.
- iOS/macOS: builds a throwaway `SFSpeechRecognizer` for the requested locale
rather than reading the cached `onDeviceStatus`. Support is per-locale, and
`listenForSpeech` replaces `recognizer` with whatever locale the last
session used, so the cached field does not answer a question about the next
session. A query must not mutate `recognizer` either.
- Android: `isOnDeviceRecognitionAvailable`, false below API 31. The locale
argument is accepted and ignored, because Android answers once for the
device rather than per language.
The locale parameter is there because of that asymmetry: on iOS the answer is
only meaningful for the locale the session will actually use.
Five tests on the platform interface cover true, false, the null/no-handler
default, and that the locale is passed through both when given and when not.
Note this does not change the `SpeechToText` facade — the plugin resolves
`speech_to_text_platform_interface` from pub, so a `hasOnDeviceSupport`
getter there cannot compile until 2.5.0 is published. Happy to follow up
with that one-liner whenever suits.
sowens-csd
approved these changes
Sep 14, 2026
sowens-csd
left a comment
Contributor
There was a problem hiding this comment.
That's a very nice PR, thanks for the detailed explanation and comments. Sorry it's taken me a while to review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #674, as offered there. Independent of it — this branches from
mainand touches different regions of the same Swift file, so the two can merge in either order.Why
There is no way to ask whether a device can recognize speech offline, and the two platforms disagree about what happens if you request it anyway:
SpeechRecognizer.isOnDeviceRecognitionAvailableincreateRecognizerand falls through to the network recognizer when it is absent.onDeviceErrorand the listen session does not run.So
onDevice: trueis only safe to pass unconditionally on Android, and "prefer on-device, fall back to the network" — which is what most callers actually want, and what Android already does for free — cannot be expressed on iOS at all.The information exists on both sides already. iOS computes
supportsOnDeviceRecognitionintoonDeviceStatusduringsetupSpeechRecognition; Android callsisOnDeviceRecognitionAvailableinsidecreateRecognizer. Neither is surfaced. This exposes it.What is here
Platform interface —
hasOnDeviceSupport({String? localeId}), defaulting toUnimplementedErrorlike the other methods, with aMethodChannelimplementation that answers false rather than throwing when a platform has no handler. That matters for the degradation story: an app that checks first and falls back to the network recognizer then behaves exactly as it would have without the check, on old platform implementations and on web/Windows alike.iOS / macOS — builds a throwaway
SFSpeechRecognizerfor the requested locale rather than reading the cachedonDeviceStatus. Two reasons, and I would rather state them than leave the choice looking arbitrary: support is per-locale, andlistenForSpeechreplacesrecognizerwith whatever locale the last session used — so the cached field does not answer a question about the next one. A query also should not mutaterecognizeras a side effect, hence the local instance.Android —
isOnDeviceRecognitionAvailable, false below API 31. The locale argument is accepted and ignored, because Android answers once for the device rather than per language.That asymmetry is why the parameter exists at all. I have documented it on both sides rather than quietly dropping it, since a caller who assumes per-locale semantics on Android would be wrong in a way nothing would tell them.
Tests
Five on the platform interface: true, false, the null/no-handler default, and that the locale is passed through both when supplied and when omitted.
flutter testandflutter analyzeclean inspeech_to_text_platform_interface(27 tests).The native halves are not covered, for the same reason as #674 — they only answer differently on a device or simulator with no on-device support, and the result is a platform value Dart cannot synthesize. If you have a pattern for that I have missed, I am glad to add it.
The one thing this deliberately does NOT do
It does not add a
SpeechToText.hasOnDeviceSupportgetter. The plugin resolvesspeech_to_text_platform_interfacefrom pub (^2.4.0), not by path, so a facade getter would not compile in CI until 2.5.0 is published — thespeech_to_textjob would go red on a PR that is otherwise fine.So this PR bumps the interface to 2.5.0 and stops there, leaving your CI green. The facade getter is a one-liner and I am happy to open it the moment the interface is released, or to add it here with a
dependency_overridesif you would rather carry it in one change — your call on how you prefer to sequence a cross-package addition in this repo.