DEVIOUS BY DC · COMPANION APP BLOG · SV 1.0.0 · POZNAŃ · PL
BLOG · 07

The keyboard ate the app.

CATEGORY ENGINEERING READ TIME 8 MIN PUBLISHED 29 JUL 2026

A beta tester told us the on-screen keyboard was covering the field they were typing in. We fixed it, shipped v1.0.2, and told them so. The next report said: still broken. They were right, we were wrong, and the reason turned out to be a platform change that silently breaks almost every React Native app that upgrades to Expo SDK 54 — so here is the full story, including the part that cost us a review cycle.

01 What actually changed.

For most of Android's history, apps didn't handle the keyboard at all — the OS did. With adjustResize, the window simply shrank when the keyboard appeared, every scroll view got shorter, and whatever you were typing stayed visible. Most React Native code relies on this without knowing it relies on it.

Android 15 made edge-to-edge rendering mandatory for apps targeting SDK 35: your app now draws behind the status and navigation bars, and Expo SDK 54 enforces it. The side effect nobody puts in a headline: once the app draws edge-to-edge, the automatic window-resize behaviour is effectively gone. softwareKeyboardLayoutMode: "resize" in app.json stops doing what it did. The keyboard becomes just another thing drawn on top of your app, and making room for it becomes entirely your problem — on every screen, at once.

Nothing crashes. Nothing warns. Every screen that never explicitly handled the keyboard just quietly starts typing blind.

02 Why our first fix failed.

When the first report came in, we did what most teams do: found the screen in the report, wrapped it in React Native's KeyboardAvoidingView, confirmed it worked, shipped. It did work — on that screen. Then we audited the whole app and found the real number: 33 files render a text input, and 29 of them had no keyboard handling at all. They had never needed any. Fixing the one screen someone happened to report was whack-a-mole with 28 moles left.

That's the real lesson of this bug: edge-to-edge turns keyboard handling from an opt-in nicety into an app-wide requirement. Per-screen patching can't fix a class-level problem.

03 The fix that held.

We adopted react-native-keyboard-controller (v1.18.5) — the library Expo's own keyboard-handling guide points to — and converted the entire app in one pass:

  • One provider at the root. KeyboardProvider wraps the app in the root layout. It detects edge-to-edge on its own (via react-native-is-edge-to-edge) — no props needed.
  • Screens where the input lives in a scroll view — 20 of them — swap ScrollView for the library's KeyboardAwareScrollView. It's prop-compatible; we added bottomOffset={16} for breathing room. The field you're typing in scrolls itself into view above the keyboard.
  • Modals and bottom sheets with inputs — 10 of them — wrap their sheet in the library's drop-in KeyboardAvoidingView with behavior="padding".
  • Fixed, non-scrolling screens got KeyboardAwareScrollView with contentContainerStyle={{flexGrow: 1}} rather than a padding-based wrapper — a shrinking container on a screen that can't scroll just clips your save button off the bottom, which is arguably worse than the original bug.

04 The gotcha nobody documents.

Here's the one that will bite you later. On Android, the moment KeyboardProvider is mounted, the library sets every React Native Modal's window to SOFT_INPUT_ADJUST_NOTHING — unconditionally. That's what lets its components take control inside modals. But it also means any modal with a text input that is not wrapped in one of the library's keyboard components is now 100% dead to the keyboard: no pan, no resize, no fallback. An input that used to be merely awkward becomes completely unusable, and the failure only shows up on a device with the keyboard actually open.

We audited every modal in the app and left a comment on our shared modal wrapper stating the invariant, because this is exactly the kind of rule that outlives everyone's memory of why it exists.

05 Small things worth stealing.

  • keyboardShouldPersistTaps="handled" everywhere you converted. Without it, tapping a button while the keyboard is open spends the first tap dismissing the keyboard. Once fields scroll into view next to buttons, this papercut gets very visible.
  • The library ships a jest mock (react-native-keyboard-controller/jest) — register it once in a global setup file. Its native module has no JS fallback and will throw the moment a test imports it.
  • This is a native module. An OTA/JS-only update cannot deliver the fix, and existing dev clients hard-crash at boot until rebuilt. Plan a store build, and warn your team before they pull the branch.
  • Verify the claim before believing the fix. Our test suite stayed green through the broken state and the fixed state alike — the jest mock reduces every keyboard component to a plain view. Green tests are evidence of nothing here; only a device with a keyboard is.

Devious by DC is an end-to-end-encrypted app for D/s relationships — tasks, rules, contracts, journals, chat. The keyboard fix described here shipped in v1.0.3; the honest ledger of everything else we've broken and fixed is on the build log.

© MMXXVI · DEVIOUS CREATIONS SP. Z O.O. · POZNAŃ · PL DEVIOUSby DC· SV 1.0.0