Overview
Anti-patterns are categorised by severity: Critical (produce security vulnerabilities, data loss, or production crashes), High (produce significant technical debt and degraded quality), and Medium (reduce maintainability and team velocity without immediate production impact). Critical anti-patterns are blocking code review findings — no PR that introduces a critical anti-pattern is merged.
Critical Anti-Patterns
Credentials in Plaintext Storage
Symptom: Session tokens, API keys, or passwords stored in SharedPreferences, UserDefaults, or a plaintext SQLite column.
Root Cause: Developer unaware of platform secure storage APIs. Speed priority over security.
Consequence: Any party with filesystem access (rooted/jailbroken device, backup extraction, physical access) reads the credential.
Correct approach: Android Keystore + EncryptedSharedPreferences. iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly.
⚠ Plaintext Credential Storage — sharedPrefs.putString("token", accessToken) on Android or UserDefaults.standard.set(accessToken, forKey: "token") on iOS. P1 security finding. Blocking code review gate.
CORRECT: EncryptedSharedPreferences.create(...) on Android. KeychainSwift().set(accessToken, forKey: "token") with appropriate accessibility attribute on iOS.
Network Call in ViewModel
Symptom: ViewModel contains Retrofit or URLSession import. apiService.getAccount() called directly from ViewModel.
Root Cause: Architect specified clean architecture verbally but did not enforce it through static analysis or code review.
Consequence: Business logic mixed with network I/O. ViewModel untestable without mocking the entire network stack. Security audit cannot identify the data layer boundary.
Correct approach: Network calls are in Repository implementations only. ViewModel calls Use Cases. Use Cases call Repository interfaces.
⚠ Network Call in ViewModel — viewModelScope.launch { val account = apiService.getAccount(id) ... } in a ViewModel class.
CORRECT: viewModelScope.launch { val result = getAccountUseCase(id); _state.update { it.copy(account = result.getOrNull()) } }. The Use Case handles the Repository call.
Implicit Flow for OAuth
Symptom: OAuth authorization URL contains response_type=token. Access token appears in the redirect URL fragment.
Root Cause: OAuth Implicit Flow was the recommended mobile pattern before 2017. Many tutorials still reference it.
Consequence: Access token transmitted in URL fragment is visible in server logs, browser history, and referrer headers. Attack surface for token interception.
Correct approach: OAuth 2.0 Authorization Code + PKCE. response_type=code. Access token received only in the token endpoint response body.
High-Severity Anti-Patterns
⚠ God ViewModel — ViewModel with 600+ lines handling networking, navigation, business logic, analytics, error handling, and UI state. Impossible to test, constant merge conflicts for large teams.
CORRECT: Maximum 200 lines. Business logic in Use Cases. Navigation events in a dedicated NavigationEvent sealed class. Analytics in a separate event observer. Each concern in its own class.
⚠ Mutable State Exposed from ViewModel — val accounts: MutableList<Account> as a public property. Any class can add, remove, or reorder accounts.
CORRECT: Expose only immutable state. val uiState: StateFlow<AccountsUiState>. Internal mutable state is private val _uiState: MutableStateFlow<AccountsUiState>.
⚠ Hardcoded Strings for Configuration — API base URL, feature flags, or environment identifiers hardcoded as string literals in source code. Different teams maintain different source files for different environments.
CORRECT: Android BuildConfig fields injected from Gradle build variants. iOS xcconfig files with environment-specific values. CI injects values from encrypted secrets at build time.
⚠ Synchronous Network on Background Thread in Loop — Fetching 500 records by making 500 sequential API calls in a for loop on a background thread. Each call adds 200ms on 4G — 100 seconds total.
CORRECT: Batch endpoint returns all 500 records in a single API call. If a batch endpoint is not available, concurrent requests using async/await with withContext(Dispatchers.IO).
Medium-Severity Anti-Patterns
⚠ Context Passed as Use Case Parameter — GetUserUseCase(context: Context) — the domain layer has a dependency on the Android platform.
CORRECT: Domain layer imports no Android classes. Values that require Context (resources, file paths) are resolved at the Repository or data layer and passed as primitive types to the domain.
⚠ Magic Numbers in Domain Logic — if (account.balance > 50000) { applyPremiumTier() } — business threshold embedded as a literal in source code.
CORRECT: Named constants in the domain model: const val PREMIUM_TIER_THRESHOLD = 50_000. Document the business rule in a comment. Ideally, fetch the threshold from configuration to enable adjustment without a release.
Anti-Patterns to Avoid
⚠ 1. Credentials in Plaintext Storage
Session tokens or API keys written to SharedPreferences or UserDefaults, readable on any rooted or jailbroken device. A P1 finding that exposes every user's session.
Hover to see the fix ↻
↺ Correct Approach
Android Keystore + EncryptedSharedPreferences; iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Hardware-backed, device-bound, never embedded in the binary.
⚠ 2. Network Call in the ViewModel
apiService.get() invoked directly from a ViewModel, collapsing the data layer into presentation. Untestable without mocking the network stack and a standing security-boundary violation.
Hover to see the fix ↻
↺ Correct Approach
ViewModel → Use Case → Repository interface → data source. Each layer is independently testable and the network boundary stays in the data layer.
Flowchart
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%%
flowchart TD
subgraph CRITICAL["🔴 Critical — Blocking Code Review Gate"]
AP1["<b>Credentials in Plaintext Storage</b><br/>SharedPreferences / UserDefaults<br/>Readable on rooted devices<br/><i>P1 Security Finding</i>"]
AP2["<b>Network Call in ViewModel</b><br/>apiService.get() from ViewModel<br/>Security boundary violated<br/><i>Architecture Violation</i>"]
AP3["<b>OAuth Implicit Flow</b><br/>client_secret in binary<br/>Extractable in < 60 seconds<br/><i>Token Compromise Risk</i>"]
end
subgraph HIGH["🟠 High — Mandatory Refactor"]
AP4["<b>God ViewModel</b><br/>> 200 lines<br/>Networking + business logic<br/>+ navigation + analytics"]
AP5["<b>Mutable State Exposed</b><br/>public var list = mutableListOf()<br/>No single source of truth"]
AP6["<b>Hardcoded Configuration</b><br/>API URLs · Feature flags in code<br/>Environment management failure"]
end
subgraph CORRECT["✅ Correct Approaches"]
C1["<b>Keystore + EncryptedSharedPrefs</b><br/>iOS Keychain<br/>Hardware-backed storage"]
C2["<b>Repository → UseCase → ViewModel</b><br/>Layer boundary enforced<br/>Each layer independently testable"]
C3["<b>OAuth 2.0 + PKCE</b><br/>AppAuth library<br/>No client_secret required"]
end
AP1 -->|"Fix"| C1
AP2 -->|"Fix"| C2
AP3 -->|"Fix"| C3
style CRITICAL fill:#FEE2E2,stroke:#DC2626,stroke-width:2px
style HIGH fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style CORRECT fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
style AP1 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d
style AP2 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d
style AP3 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d
style AP4 fill:#FDE68A,stroke:#D97706,color:#78350F
style AP5 fill:#FDE68A,stroke:#D97706,color:#78350F
style AP6 fill:#FDE68A,stroke:#D97706,color:#78350F
style C1 fill:#BBF7D0,stroke:#16A34A,color:#14532D
style C2 fill:#BBF7D0,stroke:#16A34A,color:#14532D
style C3 fill:#BBF7D0,stroke:#16A34A,color:#14532D
References
- Fowler, Martin — Refactoring: Improving the Design of Existing Code. Addison-Wesley, 2018.
- Google — Android Common Anti-patterns. developer.android.com/topic/performance/vitals
- OWASP — Mobile Application Security Verification Standard. owasp.org/www-project-mobile-app-security
- Nygard, Michael — Release It! Pragmatic Bookshelf, 2018.
Mobile Engineering Reference
← Mobile Development