MainActivity.java — the 5,411-line god-object
Generated 2026-05-11 · Costco Android
The file that runs everything
Costco/src/main/java/com/costco/app/android/ui/main/MainActivity.java is a single Java class containing 5,411 lines with 100+ public methods across 20+ unrelated responsibilities. It is the single largest file in the app module and the locus of more concrete findings than any other class — concentrated crash risks, lifecycle issues, deep-link handling, navigation, WebView orchestration, biometric flows, and analytics initialization all flow through this one file.
What's in it (sampled responsibilities)
| Concern | Indicative methods | Should live in |
|---|---|---|
| Tab / bottom-nav coordination | setupBottomNav, handleTabReselected, switchToTab | BottomTabCoordinator |
| Deep-link routing | 11+ intent filters → routing dispatch within onNewIntent + helpers | DeepLinkRouter |
| WebView orchestration | getMainWebViewFragment, refreshWebView, loadWebViewUrl | MainWebViewController |
| Biometric & passkey gates | handleBiometricResult, requestPasskey | AuthGate use case |
| App-link verification | SecureKeyMembershipAccountVerification handling | MembershipVerificationFlow |
| Analytics + Adobe init | Adobe Marketing Cloud bootstrap calls | AnalyticsInitializer |
| Geofence registration | Permission flow + geofence client setup | GeofenceCoordinator |
| Pharmacy embedded WebView | showPharmacyWebView, intent routing | PharmacyFlow |
| Misc Handler.postDelayed UI tweaks | ~6 inline Handler().postDelayed sites | — |
Findings concentrated here
The static review identified 10+ findings rooted in this file:
- F058 —
items.get(0)on shortcut items (line 1266) — IndexOutOfBoundsException risk - F061 —
intent.getExtras().getString()chained without null check (line 4489) - F063 —
data.getStringExtra(NEW_TAB)compared without null check (line 2685) - F064 —
intent.getStringExtra(EXTRA_ROADSHOW_STRING)unguarded (line 3756) - F081, F082, F146-F151 — six Handler-leak sites (lines 1864, 2863, 3901, 4545, 5070, 5075)
- F080 —
Intent ACTION_VIEWwith dynamic URL on line 3891 — deep-link safety concern
Code excerpt — concentrated risk
// MainActivity.java — illustrative composite of patterns observed
// Line 1864: Handler with no Looper, no cleanup
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// captures Activity strongly; runs after activity destroyed if user backs out
refreshSomething();
}
}, 1000);
// Line 3901: 3-second deferred work — survives orientation change
new Handler().postDelayed(() -> {
showOnboardingPrompt();
}, 3000);
// Line 4489: NPE chain (getExtras() may be null)
String tab = getIntent().getExtras().getString(KEY_TAB);
// Line 1266: index 0 on possibly-empty list
ShortcutInfo first = items.get(0);
// Line 3891: Dynamic URL handed to ACTION_VIEW
startActivity(new Intent(ACTION_VIEW, Uri.parse(inboxUrl)));
Refactor strategy — incremental, not rewrite
A 5,411-line file cannot be safely rewritten in one PR. Extract concerns one at a time, each behind an interface, with the existing tests as the safety net. Recommended sequence:
- Sprint 1 — Extract
DeepLinkRouter(the largest cluster). MainActivity holds onlyrouter.handle(intent). New router has its own unit tests. - Sprint 2 — Extract
BottomTabCoordinator+MainWebViewController. Replace direct fragment manipulation with delegate calls. - Sprint 3 — Extract
HandlerCleanupHost— a tiny helper that owns allHandler.postDelayedcalls and clears them inonDestroy. Closes the 6 leak findings in one PR. - Sprint 4 — Migrate
MainActivity.javatoMainActivity.kt. With most concerns extracted, the conversion is straightforward and unlocks Kotlin idioms (sealed classes, scope functions, nullable types). - Sprint 5 — Move analytics + geofence + biometric initialization to
androidx.startup.Initializer. Reduces cold-start work and eliminatesonCreatebloat.
Refactor recipe — extracting deep-link routing
// Before — in MainActivity
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action) && intent.getData() != null) {
Uri data = intent.getData();
if (data.getHost() != null && data.getHost().contains("costco.ca")) {
// 200 lines of dispatch logic ...
}
}
}
// After — extracted
class DeepLinkRouter @Inject constructor(
private val authGate: AuthGate,
private val tabCoordinator: BottomTabCoordinator,
private val webViewController: MainWebViewController,
private val analytics: AnalyticsClient
) {
fun handle(intent: Intent): RoutingResult {
val data = intent.data ?: return RoutingResult.NoOp
if (!isCostcoHost(data.host)) return RoutingResult.External
return when {
data.path?.contains("/SecureKeyMembership") == true ->
authGate.handleMembershipVerification(data)
data.path?.contains("/warehouse/locator") == true ->
tabCoordinator.switchToWarehouse(data)
else -> webViewController.loadUrl(data.toString())
}
}
private fun isCostcoHost(host: String?): Boolean =
host != null && ALLOWED_HOSTS.any { host.endsWith(it) }
companion object {
private val ALLOWED_HOSTS = listOf("costco.com", "costco.ca", "m.costco.com", "m.costco.ca")
}
}
// MainActivity becomes:
@Inject lateinit var router: DeepLinkRouter
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
when (router.handle(intent)) {
RoutingResult.NoOp -> { /* no-op */ }
RoutingResult.External -> { /* let system handle */ }
is RoutingResult.Handled -> { /* tab/webview already updated */ }
}
}
Verification (how to confirm the refactor lands cleanly)
- Behavior parity test — generate a
DeepLinkParityTestthat runs both old and new routing paths against the 11+ intent filters in the manifest, asserting the same destination is reached for each URL. Run for one full release before deleting old code. - Crashlytics gate — the 6 Handler-leak findings should produce zero new crashes after Sprint 3. Watch for 2 weeks; if no
IllegalStateException: Activity has been destroyedspikes appear, the change is safe. - Cold-start macrobenchmark — measure
application(_:didFinishLaunchingWithOptions:)-equivalent time before and after Sprint 5. Expect 100-200ms improvement. - LoC budget — set a hard ceiling:
MainActivity.javamust be ≤ 1000 lines after Sprint 3, ≤ 500 after Sprint 5.
Why fixing this matters more than other files
Refactoring MainActivity.java unlocks the entire app module. New engineers stop being intimidated. Code review on changes becomes possible (rather than rubber-stamping). Test coverage rises because the extracted pieces are testable. And five separate categories of finding (crash, lifecycle, deprecated, performance, code quality) all see their counts drop simultaneously. This is the single highest-leverage refactor in the Android codebase.