Skip to main content

Compose input, focus, keyboard, forms, and View interoperability

Controlled input​

@Composable
fun EmailField(
value: String,
onValueChange: (String) -> Unit,
error: EmailError?,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = { Text(stringResource(R.string.email)) },
isError = error != null,
supportingText = { error?.let { Text(stringResource(it.messageRes)) } },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
)
}

Parent owns value; component reports edits. Domain validation should not depend only on keyboard type because users/paste/accessibility can provide arbitrary input.

Draft state and validation​

  • Immediate local validation for obvious format/required constraints.
  • Debounced remote validation for username/promo when needed.
  • Final server/domain validation on submit.
  • Preserve draft according to process-death/product privacy requirement.

Do not show “invalid email” before user has meaningfully interacted unless UX specifies it.

Focus​

val passwordRequester = remember { FocusRequester() }

TextField(
value = email,
onValueChange = onEmail,
keyboardActions = KeyboardActions(
onNext = { passwordRequester.requestFocus() }
)
)

TextField(
value = password,
onValueChange = onPassword,
modifier = Modifier.focusRequester(passwordRequester)
)

Request focus in response to event/effect, not unconditionally during composition. Test hardware keyboard, D-pad, TalkBack, and IME actions.

Keyboard/IME and insets​

Use IME actions (Next, Done, Search) matching form meaning. Keep focused field/action visible with IME insets/bring-into-view behavior. Do not hardcode keyboard height.

Gestures​

Prefer high-level clickable, toggleable, selectable, scrollable components because they add semantics, focus, keyboard, ripple/interaction handling. Use pointerInput for genuinely custom gestures and handle cancellation/multiple pointers/conflicts.

ComposeView inside Fragment​

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View =
ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent { AppTheme { ProfileRoute() } }
}

Composition disposal must align with Fragment view lifecycle. Wrong strategy can lose state or retain composition beyond dead View tree.

AndroidView inside Compose​

AndroidView(
factory = { context -> MapView(context).apply { initialize() } },
update = { map -> map.showLocation(location) },
onRelease = { map -> map.release() }
)
  • Construct View in factory, not on every recomposition.
  • update synchronizes current state and can run repeatedly.
  • Integrate lifecycle callbacks/resource cleanup.
  • Avoid retaining Activity/old state.
  • Ensure accessibility and nested input/scroll interoperability.

Migration boundary​

Migrate vertical slices/components, keeping one source of truth. Do not make View and Compose sides each own a mutable copy synchronized by callbacks—this creates loops/races.

ViewModel/state holder
├─ View renderer/events
└─ Compose renderer/events

Platform resources​

Use stringResource, painterResource, dimensionResource where appropriate. Resolve current configuration inside UI; do not cache old Context strings in ViewModel.

Interview-ready answer

Compose input should be controlled by one source of truth and emit events. Keyboard options improve UX but do not validate untrusted input. Focus/IME are event- and lifecycle-owned, with hardware/accessibility testing. For interoperability, ComposeView disposal follows the View lifecycle; AndroidView constructs once in factory, synchronizes idempotently in update, and releases resources. Hybrid UI shares one state holder rather than synchronizing two local states.

Exercises​

  1. Design signup validation timing and process restoration.
  2. Explain why focus request inside composition can loop/repeat.
  3. Wrap a lifecycle-heavy camera View in Compose safely.