Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This article describes how to speech-enable your Android EHR app by using Jetpack Compose. For more information, see the Compose UI sample app. The following integration patterns are supported:
Compose UI with
AndroidViewBindingand an XML layout.Compose UI with
AndroidView-wrapped controls.Pure Compose UI with custom
VuiController.
Choose a pattern based on whether the app's speech-enabled fields are existing XML views, Android views hosted by Compose, or native Compose fields.
Sample app flow
The examples in this article reference sample code in our GitHub repo.
SpeechKitComposeUISample is the launcher activity. It displays a pure Compose sign-in screen, opens the SpeechKit session, and launches ComposeUIReportsActivity.
ComposeUIReportsActivity displays the three supported reports in a Material 3 LazyColumn and launches:
ComposeUIAndroidViewBindingActivityComposeUIAndroidViewWrapperActivityPureComposeCustomVCReportActivity
All report activities are internal to the app (android:exported="false").
AndroidViewBinding and XML layout
Use this pattern when you need to host an existing XML form in a Compose screen.
Reference files:
reports/ComposeUIAndroidViewBindingActivity.ktres/layout/activity_compose_ui_android_view_binding_report.xml
Implementation requirements:
Use
com.nuance.speechanywhere.VuiControlleras the XML root.Place every speech-enabled
EditTextorTextInputEditTextcontrol in that controller hierarchy.Inflate the layout with
AndroidViewBinding.Keep a reference to the controller and call
close("")when navigating back and during activity destruction.Make the content scrollable and apply IME insets so the keyboard and SpeechBar don't cover the active field.
Sample code:
AndroidViewBinding(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
factory = ActivityComposeUiAndroidViewBindingReportBinding::inflate,
update = {
onControllerAvailable(vuiController)
val scrollView = reportScrollView
val contentPadding = (16 * scrollView.resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(scrollView) { view, insets ->
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
view.setPadding(
contentPadding,
contentPadding,
contentPadding,
contentPadding + imeBottom,
)
insets
}
ViewCompat.requestApplyInsets(scrollView)
},
)
The XML layout uses a ScrollView with android:fillViewport="true", android:clipToPadding="false", and vertical scrollbars. The report description uses textMultiLine and a minimum of three lines. Both fields use flagNoExtractUi.
<com.nuance.speechanywhere.VuiController
android:id="@+id/vuiController"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/reportScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:padding="16dp"
android:scrollbars="vertical">
<!-- Speech-enabled TextInputEditText controls -->
</ScrollView>
</com.nuance.speechanywhere.VuiController>
AndroidView wrapper
Use this pattern for a Compose-first screen that still uses classic Android EditText controls. VuiControllerView hosts the Compose content under one SDK VuiController and discovers the wrapped fields.
Reference file:
/composeuisample/reports/ComposeUIAndroidViewWrapperActivity.kt
SpeechKit APIs:
com.nuance.speechanywhere.compose.VuiControllerViewcom.nuance.speechanywhere.compose.VuiControllerStatecom.nuance.speechanywhere.compose.rememberVuiControllerState
Implementation requirements:
Create controller state with
rememberVuiControllerState.Wrap all speech-enabled fields with
VuiControllerView.Add classic fields with
AndroidViewand assign stable resource IDs.Preserve values with
rememberSaveableand synchronize Compose and View state infactoryandupdate.Use multiline input and
EditorInfo.IME_FLAG_NO_EXTRACT_UIfor the sample report fields.Place fields in an
imePadding()and vertically scrollable container.Retain the controller callback and close the controller on back navigation and destruction.
Sample code:
val state = rememberVuiControllerState(
language = "en-US",
topic = VuiController.NUSA_TOPIC_GENERAL_MEDICINE,
)
VuiControllerView(
modifier = Modifier.fillMaxSize(),
language = state.language,
topic = state.topic,
onControllerChanged = onControllerChanged,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.imePadding()
.verticalScroll(rememberScrollState()),
) {
AndroidView(
factory = { context ->
EditText(context).apply {
id = R.id.compose_wrapper_patient_name
imeOptions = EditorInfo.IME_ACTION_NONE or
EditorInfo.IME_FLAG_NO_EXTRACT_UI
minLines = 3
maxLines = 6
}
},
)
}
}
VuiControllerView assigns an ID only when a wrapped EditText has no ID. It synchronizes after composition, after attachment, and on lifecycle resume to avoid missing hosted views. Its ComposeView uses DisposeOnViewTreeLifecycleDestroyed, and the lifecycle effect releases the controller when appropriate.
Pure Compose UI with custom VuiController
Use this pattern for native Compose OutlinedTextField controls. The SDK ComposeFieldAdapter bridges Compose TextFieldValue state to custom.ITextControl. The custom controller itself is hosted in an AndroidView.
Reference file:
/composeuisample/reports/PureComposeCustomVCReportActivity.kt
SpeechKit APIs:
com.nuance.speechanywhere.compose.ComposeFieldAdaptercom.nuance.speechanywhere.compose.rememberComposeFieldAdaptercom.nuance.speechanywhere.compose.rememberComposeFieldSetupcom.nuance.speechanywhere.custom.VuiController
Implementation requirements:
Create saveable field state with
rememberComposeFieldSetup.Create one stable
ComposeFieldAdapterper field withrememberComposeFieldAdapter.Track the active control ID and call
controlGotFocuswhen focus changes.Host
custom.VuiControllerinAndroidViewand set the language and topic.Synchronize adapters after controller creation and again on
ON_RESUME.Use an
imePadding()and vertically scrollable field container.Close the controller on back navigation and destruction.
Sample code:
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
VuiController(context).apply {
setLanguage("en-US")
setTopic(VuiController.NUSA_TOPIC_GENERAL_MEDICINE)
}
},
update = { controller ->
if (synchronizedController !== controller) {
synchronizedController = controller
controller.post { controller.synchronize(controls) }
}
when (activeControlId) {
firstControlId -> controller.controlGotFocus(firstControl)
secondControlId -> controller.controlGotFocus(secondControl)
}
}
)
ComposeFieldAdapter bridges text, selection, replacement, and focus operations between Compose state and SpeechKit.
Add SpeechBar and keyboard UI
The current sample includes the following UI handling to keep speech controls and editable content usable when the soft keyboard or SpeechBar is visible:
The three report activities use
android:windowSoftInputMode="adjustResize".The three report activities handle
orientation|screenSize|keyboard|keyboardHiddenconfiguration changes.The XML report opts into edge-to-edge layout with
WindowCompat.setDecorFitsSystemWindows(window, false)and applies the current IME bottom inset to itsScrollViewpadding.The XML
ScrollViewsetsclipToPadding="false", allowing content to scroll through the inset-padded area.The launcher, wrapped-view report, and pure Compose report use
imePadding()plus vertical scrolling.Wrapped Android fields and pure Compose fields are multiline (
minLines = 3,maxLines = 6) so dictated text remains readable.XML and wrapped Android fields disable extract UI to avoid a full-screen keyboard editor.
Report controllers are explicitly closed on system back, toolbar back, and activity destruction so a stale SpeechBar isn't retained after leaving a report.
android.permission.HIDE_OVERLAY_WINDOWSremains declared for SDK overlay behavior.
Manifest configuration:
<activity
android:name="com.nuance.dm_speechkit.composeuisample.reports.ComposeUIAndroidViewBindingActivity"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
android:windowSoftInputMode="adjustResize"
android:exported="false" />
Apply the same configuration to the AndroidView wrapper and pure Compose report activities.
Integrator checklist
Select the XML, mixed Android View and Compose, or pure Compose pattern.
Keep View-based fields inside one
VuiControllerhierarchy.Use stable IDs for wrapped Android views.
Preserve text and selection state across recomposition.
Forward focus changes for pure Compose controls.
Set controller language and topic explicitly.
Synchronize hosted or custom controls after attachment and lifecycle resume.
Use scrolling and IME insets so fields remain visible with the keyboard and SpeechBar.
Close the controller whenever the report activity exits.
Register report activities as non-exported with
adjustResizeand the documented configuration changes.Test dictation, field navigation, rotation, keyboard behavior, SpeechBar visibility, back navigation, and activity resume.