Fix Recipes (Android)
Generated 2026-05-07 · Costco Android
Android fix recipes — top 20
Engineering playbook of the most-applicable patterns in the codebase. Each recipe captures one decision: when to apply, the anti-pattern, the recommended pattern, and why. Use this as the team's house style.
R-A-01 · Replace !! force-unwrap with safe-call
Force-unwrap is the most common preventable NPE in Kotlin code.
val name = user!!.name // crashes if user is null
val name = user?.name ?: return
// or:
val name = user?.name ?: "" // typed default
R-A-02 · Replace unsafe `as` cast with `as?`
Safe cast returns null on type mismatch; explicit short-circuit.
val activity = context as MainActivity // ClassCastException risk
val activity = context as? MainActivity ?: return
R-A-03 · Guard getActivity() with isAdded()
Fragment.getActivity() returns null after detach; common crash in async callbacks.
// Inside Fragment
if (someCondition) {
getActivity().finish();
}
// Inside Fragment
if (someCondition && isAdded()) {
Activity activity = getActivity();
if (activity != null) activity.finish();
}
R-A-04 · Replace Handler.postDelayed with lifecycleScope
Coroutines tied to lifecycle cancel automatically; no Handler-leak class.
Handler().postDelayed({
refresh()
}, 3000)
viewLifecycleOwner.lifecycleScope.launch {
delay(3000)
refresh()
}
// Cancellation is automatic on lifecycle end
R-A-05 · Use viewLifecycleOwner for Fragment observe()
Observing with `this` outlives the view; use viewLifecycleOwner so observer is removed in onDestroyView.
viewModel.uiState.observe(this) { state ->
render(state)
}
viewModel.uiState.observe(viewLifecycleOwner) { state ->
render(state)
}
R-A-06 · Bound list access with firstOrNull()
Avoids IndexOutOfBoundsException; explicit null path.
val first = warehouses.get(0) // crashes if list is empty
val first = warehouses.firstOrNull() ?: return
render(first)
R-A-07 · Validate intent extras
getExtras() can be null; chain with safe-call.
val tab = intent.getExtras().getString(KEY_TAB)
val tab = intent.extras?.getString(KEY_TAB) ?: DEFAULT_TAB
R-A-08 · Lock down WebView
Default WebView settings are permissive; default-deny instead.
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(JSBridge(), "Android")
with(webView.settings) {
javaScriptEnabled = true // only if needed
allowFileAccess = false
allowContentAccess = false
allowFileAccessFromFileURLs = false
allowUniversalAccessFromFileURLs = false
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
}
// JS bridge must use @JavascriptInterface annotation;
// validate caller URL before each method.
R-A-09 · Replace Log with Timber
Timber tag-aware (auto-uses class name); ReleaseTree filters/scrubs in production.
Log.d("MainActivity", "User clicked button")
Log.e("MainActivity", "Failed: " + e.message)
Timber.d("User clicked button")
Timber.e(e, "Failed")
R-A-10 · Replace startActivityForResult with ActivityResultContract
startActivityForResult is deprecated; ActivityResultContract is type-safe.
// In Activity
startActivityForResult(intent, REQUEST_FOO)
// onActivityResult
override fun onActivityResult(...) { ... }
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) { ... }
}
// Then later:
launcher.launch(intent)
R-A-11 · Use design tokens, not hardcoded colors / dp
Tokens enable theming, dark mode, and brand consistency.
Box(modifier = Modifier
.background(Color(0xFFE31837))
.padding(16.dp)
)
Box(modifier = Modifier
.background(Pallet.brand.primary)
.padding(PalletSpace.md)
)
R-A-12 · Hoist state in Composables
Hoisted state makes the Composable testable, previewable, and reusable.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}
// Caller owns the state:
var count by remember { mutableStateOf(0) }
Counter(count, onIncrement = { count++ })
R-A-13 · Add contentDescription to every Image/Icon
WCAG 1.1.1; without descriptions, TalkBack announces nothing.
Image(painter = painterResource(R.drawable.ic_cart), contentDescription = null)
Image(
painter = painterResource(R.drawable.ic_cart),
contentDescription = stringResource(R.string.a11y_cart)
)
// For decorative images, the explicit null + comment is acceptable:
// contentDescription = null // decorative
R-A-14 · Defer Application init via androidx.startup
Defers heavy init until first use; cold-start improves measurably.
// CostcoApplication.kt
override fun onCreate() {
super.onCreate()
Adobe.start(this)
Geofence.start(this)
// ... 18 more
}
// Each becomes an Initializer:
class AdobeInitializer : Initializer<Adobe> {
override fun create(context: Context): Adobe = Adobe.start(context)
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
// Manifest: <provider androidx.startup.InitializationProvider>
R-A-15 · Use sealed Result types for repository APIs
Forces caller to handle every error path; better than try/catch.
suspend fun fetchUser(): User // throws on failure
sealed interface NetworkResult<out T> {
data class Success<T>(val value: T) : NetworkResult<T>
data class HttpError(val code: Int, val body: String?) : NetworkResult<Nothing>
data class NetworkError(val cause: Throwable) : NetworkResult<Nothing>
}
suspend fun fetchUser(): NetworkResult<User>
R-A-16 · Cancel coroutine work in onDestroyView
Custom CoroutineScope outlives the view; lifecycle scopes cancel correctly.
// In Fragment
CoroutineScope(Dispatchers.IO).launch { /* … */ }
viewLifecycleOwner.lifecycleScope.launch { /* … */ }
// or use repeatOnLifecycle for collecting Flows:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { render(it) }
}
}
R-A-17 · Stop FLAG_SECURE on sensitive screens
Blocks screen capture and recents-screen snapshot for membership/payment screens.
// Membership card / payment screens
// (no FLAG_SECURE)
// Activity.onCreate
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
R-A-18 · Encrypt sensitive DataStore keys
App-sandbox is good but root/backup extraction can read plain DataStore values.
dataStore.edit { it[KEY_EMAIL] = email } // plain text
// EncryptedDataStore wrapping standard DataStore
// or Jetpack Security:
val encryptedPrefs = EncryptedSharedPreferences.create(
context, "sensitive", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit { putString("email", email) }
R-A-19 · Generate baseline profile
AOT-compiles the critical path; cold start improves on supported devices.
# No baseline profile
# Add :macrobenchmark module; write a journey:
@Test
fun homeToCart() = baselineProfileRule.collect("com.costco.costco") {
pressHome()
startActivityAndWait()
device.findObject(By.text("Cart")).click()
device.wait(Until.hasObject(By.text("Subtotal")), 5000)
}
# Output: Costco/src/main/baseline-prof.txt — ships in APK.
R-A-20 · Snapshot test design system at fontScale 2.0
Catches text clipping under accessibility font scaling — WCAG 1.4.4.
# No font-scale snapshot tests
@Test
fun bigFontScale_doesNotClip() {
composeTestRule.setContent {
CompositionLocalProvider(LocalDensity provides Density(2.0f, fontScale = 2.0f)) {
CostcoTheme { MyScreen() }
}
}
composeTestRule.onNodeWithTag("submit").assertIsDisplayed()
}