Young Devs Bin
๐Ÿ“ Today I Learned

OkHttp Blocking Calls Crash on Android Main Thread

ยท2 min readยท#today-i-learned#android#kotlin#coroutines#okhttp#llm
  • OkHttpClient.newCall(request).execute() is a blocking call โ€” calling it inside viewModelScope.launch { } runs on Dispatchers.Main by default, which throws NetworkOnMainThreadException on Android, caught as a generic exception and shown as "network error"
  • Fix: wrap every OkHttp call in withContext(Dispatchers.IO) { ... } โ€” the = withContext(Dispatchers.IO) { syntax works cleanly as an expression body for suspend functions
  • return is not allowed inside a withContext { } lambda โ€” the last expression is the return value; replace early return "Error: $msg" with if (...) "Error: $msg" else "..." branches
  • suspend fun foo(): String = withContext(Dispatchers.IO) { ... } is the idiomatic pattern for wrapping legacy blocking APIs in coroutines
  • Gemini API keys starting with AQ... are OAuth access tokens, not API keys โ€” the permanent API key (needed for server-to-server calls) starts with AIza and is issued at aistudio.google.com โ†’ Get API key
  • ChatGPT API quota errors on a fresh key almost always mean the OpenAI account has no billing credits โ€” the API is entirely separate from the ChatGPT Plus subscription
  • Android Keystore AES-GCM encryption doesn't need any extra library โ€” KeyGenerator.getInstance(KEY_ALGORITHM_AES, "AndroidKeyStore") + KeyGenParameterSpec is all you need; no SQLCipher or Tink required for field-level encryption in Room
  • When encrypting for Room storage, store the IV and ciphertext together as "$ivBase64:$ciphertextBase64" โ€” GCM IVs are 12 bytes, always unique per encryption, and must be stored alongside the ciphertext for decryption
  • groupedNotes in a folder-based UI that filters by categoryId != null will silently drop notes saved without a folder โ€” if the app requires folder selection, enforce it in the UI rather than handling the null case at render time
  • LLM responses don't always honor "respond only in JSON" โ€” always extract the first {...} substring from the raw response before parsing, with a plain-text fallback if no valid JSON is found