April 29, 2026 · Gökhan Oğuz

The Android Shakedown

The simulator lies. Not on purpose, but it lies. Today was Sesla's first real session on a physical Android phone — a Galaxy S921B running Android 16 — and within fifteen minutes I had four bugs that the iOS simulator and the test suite had cheerfully signed off on.

4

Real Bugs Caught

1030

Tests Passing

0

Caught By Tests

The crash that wasn't a crash

First launch on the Galaxy: app opens, splash flashes, app dies. No red Flutter screen, no Dart exception, just a clean exit. The kind of failure you don't get to debug, you get to interrogate.

adb logcat eventually coughed up the truth:

Failed to load dynamic library 'libsqlite3.so'

The pubspec was on sqlite3_flutter_libs: ^0.6.0+eol. That +eol tag turns out to be load-bearing. The package author marked the 0.6 line as end-of-life and stopped shipping the Android native libraries with it. flutter pub get happily resolves it, everything compiles, and then the database opens to a void.

Pinning to ^0.5.40 brought back the .so file and the app booted. Lesson logged: a tag suffix with the word "eol" in it can break your build without producing a single compile error.

The keyboard that wouldn't leave

Onboarding, page two: enter the child's name and age. Two text fields, one Next button. On the simulator this works fine. On a real phone with a real soft keyboard, three problems showed up at once.

The keyboard pushed the layout 52 pixels past the bottom edge. The keyboard's own Next key advanced the page wizard instead of moving focus to the age field. And after a hot restart the keyboard was still sitting there from the previous session, attached to a focus tree that no longer existed.

Three small surgeries. Wrap the page in a SingleChildScrollView. Give the name field textInputAction.next and a real FocusNode on the age field so submitting the name moves focus instead of trying to advance pages. And in the onboarding initState, fire one shot of:

SystemChannels.textInput.invokeMethod<void>('TextInput.hide')

to tell the native IME that whatever it thought was happening, isn't. The Flutter focus tree had no idea the keyboard was even up because, from its perspective, no field was focused. The native side held the state independently.

The locale that lagged a profile

Sesla supports multiple user profiles per device, each with its own primary language. I onboarded as English, then created a Turkish profile on top. The symbol grid switched to Turkish immediately. The bottom nav still said Home / Search symbols / Profiles / Settings. The sentence strip placeholder still read "Tap symbols to build a sentence." The category tiles were Turkish. The chrome was English.

Turns out the language was being read from two different places. The symbol grid asked the active user's primaryLang directly, which updates the moment the user switches. The chrome asked localeProvider, which is a separate, persisted setting. Switching profiles never touched it.

Two fixes. The profile switch and create paths now also call localeProvider.setLocale(). And — for any user who had already drifted into the inconsistent state, including me — bootstrap now reconciles the saved locale against the active user's language on every cold start. If they disagree, the active user wins. The chrome catches up on the next launch without anyone having to re-switch profiles.

Six tiles where five would do

Last one. The Turkish "Temel İletişim" board was seeded with six columns; its English twin was seeded with five. On a 1080-wide phone the sixth column squeezed every tile into something a kid would struggle to hit. The asset fix was a one character change. The cleanup wasn't.

Five symbols in that board lived at col: 5. Dropping the column count from six to five orphaned them, and on the next clean install the seeder threw a foreign key violation trying to insert children with no parent row. The board loader caught the exception and silently seeded zero boards. The user got an empty home screen.

A small Python pass over the JSON dropped the five stranded entries, and the bench is back to 1030 green tests. While I was in the file I also shortened the bottom nav label from Sembol ara / Search symbols to Ara / Search. Two-word nav labels never survive a real device.

The simulator runs your code. The phone runs your code on top of every decision a hundred Android engineers made about IMEs, native libraries, window insets, and platform channels. The phone is right.

What today proved

The four bugs are different in kind, but they share a shape: each one needed something the simulator can't fake. A real native library loader. A real soft keyboard with its own state machine. A real multi-profile flow tested end to end. A real screen at a real density.

A 1030-test suite isn't going to find any of these. Real-device QA isn't a step you do once before submission. It's the only ground truth you have, and from now on every meaningful change gets a pass on the Galaxy before it counts as done.

Tomorrow

More device passes. Tablet next. Then iOS on hardware, not just the simulator. And I want to look at the screenshot-driven QA loop itself — something that takes a screenshot of every screen in both languages and diffs them when anything changes. The phone catches things, but a kid notices things even faster.