Skip to content

feat: expose hasOnDeviceSupport so callers can check before requesting onDevice recognition - #675

Merged
sowens-csd merged 2 commits into
csdcorp:mainfrom
tompohare:feat/on-device-support-probe
Sep 14, 2026
Merged

sowens-csd merged 2 commits into
csdcorp:mainfrom
tompohare:feat/on-device-support-probe

Conversation

@tompohare

Copy link
Copy Markdown
Contributor

Follow-up to #674, as offered there. Independent of it — this branches from main and 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:

  • Android checks SpeechRecognizer.isOnDeviceRecognitionAvailable in createRecognizer and falls through to the network recognizer when it is absent.
  • iOS returns an onDeviceError and the listen session does not run.

So onDevice: true is 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 supportsOnDeviceRecognition into onDeviceStatus during setupSpeechRecognition; Android calls isOnDeviceRecognitionAvailable inside createRecognizer. Neither is surfaced. This exposes it.

What is here

Platform interfacehasOnDeviceSupport({String? localeId}), defaulting to UnimplementedError like the other methods, with a MethodChannel implementation 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 SFSpeechRecognizer for the requested locale rather than reading the cached onDeviceStatus. Two reasons, and I would rather state them than leave the choice looking arbitrary: 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 one. A query also should not mutate recognizer as a side effect, hence the local instance.

AndroidisOnDeviceRecognitionAvailable, 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 test and flutter analyze clean in speech_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.hasOnDeviceSupport getter. The plugin resolves speech_to_text_platform_interface from pub (^2.4.0), not by path, so a facade getter would not compile in CI until 2.5.0 is published — the speech_to_text job 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_overrides if you would rather carry it in one change — your call on how you prefer to sequence a cross-package addition in this repo.

…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 sowens-csd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very nice PR, thanks for the detailed explanation and comments. Sorry it's taken me a while to review.

@sowens-csd
sowens-csd merged commit d660d8c into csdcorp:main Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants