Overview › Fix Recipes (Android)

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.

RECIPE

R-A-01 · Replace !! force-unwrap with safe-call

Force-unwrap is the most common preventable NPE in Kotlin code.

Before — anti-pattern
val name = user!!.name  // crashes if user is null
After — recommended
val name = user?.name ?: return
// or:
val name = user?.name ?: ""  // typed default
RECIPE

R-A-02 · Replace unsafe `as` cast with `as?`

Safe cast returns null on type mismatch; explicit short-circuit.

Before — anti-pattern
val activity = context as MainActivity  // ClassCastException risk
After — recommended
val activity = context as? MainActivity ?: return
RECIPE

R-A-03 · Guard getActivity() with isAdded()

Fragment.getActivity() returns null after detach; common crash in async callbacks.

Before — anti-pattern
// Inside Fragment
if (someCondition) {
    getActivity().finish();
}
After — recommended
// Inside Fragment
if (someCondition && isAdded()) {
    Activity activity = getActivity();
    if (activity != null) activity.finish();
}
RECIPE

R-A-04 · Replace Handler.postDelayed with lifecycleScope

Coroutines tied to lifecycle cancel automatically; no Handler-leak class.

Before — anti-pattern
Handler().postDelayed({
    refresh()
}, 3000)
After — recommended
viewLifecycleOwner.lifecycleScope.launch {
    delay(3000)
    refresh()
}
// Cancellation is automatic on lifecycle end
RECIPE

R-A-05 · Use viewLifecycleOwner for Fragment observe()

Observing with `this` outlives the view; use viewLifecycleOwner so observer is removed in onDestroyView.

Before — anti-pattern
viewModel.uiState.observe(this) { state ->
    render(state)
}
After — recommended
viewModel.uiState.observe(viewLifecycleOwner) { state ->
    render(state)
}
RECIPE

R-A-06 · Bound list access with firstOrNull()

Avoids IndexOutOfBoundsException; explicit null path.

Before — anti-pattern
val first = warehouses.get(0)  // crashes if list is empty
After — recommended
val first = warehouses.firstOrNull() ?: return
render(first)
RECIPE

R-A-07 · Validate intent extras

getExtras() can be null; chain with safe-call.

Before — anti-pattern
val tab = intent.getExtras().getString(KEY_TAB)
After — recommended
val tab = intent.extras?.getString(KEY_TAB) ?: DEFAULT_TAB
RECIPE

R-A-08 · Lock down WebView

Default WebView settings are permissive; default-deny instead.

Before — anti-pattern
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(JSBridge(), "Android")
After — recommended
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.
RECIPE

R-A-09 · Replace Log with Timber

Timber tag-aware (auto-uses class name); ReleaseTree filters/scrubs in production.

Before — anti-pattern
Log.d("MainActivity", "User clicked button")
Log.e("MainActivity", "Failed: " + e.message)
After — recommended
Timber.d("User clicked button")
Timber.e(e, "Failed")
RECIPE

R-A-10 · Replace startActivityForResult with ActivityResultContract

startActivityForResult is deprecated; ActivityResultContract is type-safe.

Before — anti-pattern
// In Activity
startActivityForResult(intent, REQUEST_FOO)

// onActivityResult
override fun onActivityResult(...) { ... }
After — recommended
private val launcher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) { ... }
}

// Then later:
launcher.launch(intent)
RECIPE

R-A-11 · Use design tokens, not hardcoded colors / dp

Tokens enable theming, dark mode, and brand consistency.

Before — anti-pattern
Box(modifier = Modifier
    .background(Color(0xFFE31837))
    .padding(16.dp)
)
After — recommended
Box(modifier = Modifier
    .background(Pallet.brand.primary)
    .padding(PalletSpace.md)
)
RECIPE

R-A-12 · Hoist state in Composables

Hoisted state makes the Composable testable, previewable, and reusable.

Before — anti-pattern
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) { Text("$count") }
}
After — recommended
@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++ })
RECIPE

R-A-13 · Add contentDescription to every Image/Icon

WCAG 1.1.1; without descriptions, TalkBack announces nothing.

Before — anti-pattern
Image(painter = painterResource(R.drawable.ic_cart), contentDescription = null)
After — recommended
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
RECIPE

R-A-14 · Defer Application init via androidx.startup

Defers heavy init until first use; cold-start improves measurably.

Before — anti-pattern
// CostcoApplication.kt
override fun onCreate() {
    super.onCreate()
    Adobe.start(this)
    Geofence.start(this)
    // ... 18 more
}
After — recommended
// 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>
RECIPE

R-A-15 · Use sealed Result types for repository APIs

Forces caller to handle every error path; better than try/catch.

Before — anti-pattern
suspend fun fetchUser(): User  // throws on failure
After — recommended
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>
RECIPE

R-A-16 · Cancel coroutine work in onDestroyView

Custom CoroutineScope outlives the view; lifecycle scopes cancel correctly.

Before — anti-pattern
// In Fragment
CoroutineScope(Dispatchers.IO).launch { /* … */ }
After — recommended
viewLifecycleOwner.lifecycleScope.launch { /* … */ }
// or use repeatOnLifecycle for collecting Flows:
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.state.collect { render(it) }
    }
}
RECIPE

R-A-17 · Stop FLAG_SECURE on sensitive screens

Blocks screen capture and recents-screen snapshot for membership/payment screens.

Before — anti-pattern
// Membership card / payment screens
// (no FLAG_SECURE)
After — recommended
// Activity.onCreate
window.setFlags(
    WindowManager.LayoutParams.FLAG_SECURE,
    WindowManager.LayoutParams.FLAG_SECURE
)
RECIPE

R-A-18 · Encrypt sensitive DataStore keys

App-sandbox is good but root/backup extraction can read plain DataStore values.

Before — anti-pattern
dataStore.edit { it[KEY_EMAIL] = email }  // plain text
After — recommended
// 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) }
RECIPE

R-A-19 · Generate baseline profile

AOT-compiles the critical path; cold start improves on supported devices.

Before — anti-pattern
# No baseline profile
After — recommended
# 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.
RECIPE

R-A-20 · Snapshot test design system at fontScale 2.0

Catches text clipping under accessibility font scaling — WCAG 1.4.4.

Before — anti-pattern
# No font-scale snapshot tests
After — recommended
@Test
fun bigFontScale_doesNotClip() {
    composeTestRule.setContent {
        CompositionLocalProvider(LocalDensity provides Density(2.0f, fontScale = 2.0f)) {
            CostcoTheme { MyScreen() }
        }
    }
    composeTestRule.onNodeWithTag("submit").assertIsDisplayed()
}
Costco Android · Code Review Report · Generated 2026-05-07 · 626 machine-curated findings