Code Smell is a surface-level characteristic in code that signals a potential problem in the design or architecture of an application. The term was coined by Kent Beck and popularized by Martin Fowler in the book “Refactoring: Improving the Design of Existing Code”. According to Martin Fowler, a code smell does not necessarily mean a bug, but almost always indicates the need for refactoring to improve maintainability.
Key Takeaways
Code Smell is a metaphor for symptoms in source code that with high probability indicate deeper problems. The term itself has no formal definition — it is a heuristic based on developers’ experience. Martin Fowler and Kent Beck first systematized 22 smells in 1999 in the book “Refactoring”, and most of them remain relevant decades later.
It is important to understand the difference between Code Smell and a bug. A smell is not an error: the code compiles, works, and produces correct results. The problem is that such code is hard to read, change, and test. Over time, the cost of each change increases, and confidence in the correctness of refactoring decreases. Static analysis tools (SonarQube, Detekt, SwiftLint) automatically detect many smells.
The heuristic nature of Code Smell means that not every long method needs to be split, and not every large class requires refactoring. The decision is made by the developer, evaluating the context: frequency of changes, module criticality, development plans. Experienced engineers sense a smell intuitively — the code “smells bad” even though all formal rules are followed.
Fowler identified 22 smells grouped into several categories. For mobile development, the most relevant are structural smells, object-oriented design smells, and specific problems related to platform constraints. Let us examine each group with real-world examples.
Long Method is the most common smell in mobile applications. A registration form screen often contains a single setupUI method that is 200+ lines long, creating all Views, setting up constraints, subscribing to events, and handling errors. Solution: break it down into methods by logical blocks — configureEmailField, configurePasswordField, setupConstraints, bindViewModel.
Large Class — an Activity or ViewController responsible for display, navigation, business logic, and network interactions all at once. Such a class violates the Single Responsibility Principle and contains dozens of fields and methods. In Android, this is often a Fragment with 1000+ lines containing logic from different screens. Solution: extract a presenter/ViewModel, move network code to a repository, and navigation to a coordinator.
Duplicate Code — copying identical blocks in different parts of the application. A typical example: two screens displaying a product card — in the catalog and in favorites. If the display logic is copied, fixing a bug in one place will not fix it in another. Solution: extract common logic into a reusable component or extension.
Feature Envy — a method of one class intensively uses data from another class. In Android, this manifests when a ViewModel directly accesses fields of a User model instead of calling a model method. Signal: if a method can be moved to the class whose data it uses — move it. Switch Statements (condition chains) — a switch construct or if-else chain checking the object type. Instead, use polymorphism or the strategy pattern.
Data Class — a class that only stores data but contains no behavior. Data classes in Kotlin or structs in Swift are not inherently a smell. The problem arises when business logic working with that data is scattered across the codebase instead of being encapsulated. Refused Bequest — a subclass does not use most of the parent’s methods and overrides them with empty stubs. A sign of incorrect inheritance: replace inheritance with composition.
God Activity / God Fragment — an Activity or Fragment that knows everything: lifecycle, data, navigation, Permissions, DI. This is the most expensive class to maintain in an application. Solution: architectural patterns MVVM, MVI, or Clean Architecture separate responsibilities. Giant ViewController — the iOS equivalent, where a UIViewController contains all the screen logic and often exceeds 500 lines.
Hardcoded Resources — strings, colors, sizes, API URLs embedded directly into code. In Android, this violates the R resource system; in iOS, NSLocalizedString and Asset Catalog. Fix: move all strings to strings.xml or Localizable.strings, URLs to a config file, sizes to dimens. Leaking Context — holding a reference to an Activity or ViewController longer than the component itself lives. Leads to memory leaks and crashes. Solution: weak references, Jetpack Lifecycle, RxSwift DisposeBag.
| Smell | Where It Occurs | Solution |
|---|---|---|
| Long Method | Android/iOS | Extract Method, split up |
| Large Class | Activity, ViewController | MVVM, VIPER, Clean Arch |
| Duplicate Code | Any screens | Shared Component, DRY |
| Feature Envy | ViewModel, Presenter | Move Method |
| Leaking Context | Android | Lifecycle-aware components |
Code review is the most reliable way to detect smells. The human eye notices unnatural constructs that automated analyzers miss. Code review effectiveness improves when the team uses a checklist of typical smells. It is recommended to review no more than 200–400 lines of code per session — after this threshold, attention drops and smells start slipping through.
Static analysis automates the search for structural smells. For Android, the standard tools are Detekt (Kotlin) and Android Lint; for iOS, SwiftLint and SonarQube. These tools find long methods, large classes, duplicate code, and many other issues. It is important to tune rules for the project — default configurations are often too strict or, on the contrary, miss critical smells.
Code metrics provide objective criteria: Cyclomatic Complexity (threshold >10 requires attention), Lines of Code per Method (threshold >30), Depth of Inheritance (>3 — reason to think). Tools like CodeMetrics (Xcode) and the Gradle Metrics Plugin build graphs of metric changes over time. If a method’s complexity grew from 5 to 15 after the last commit — that is a signal for refactoring.
// Example: method with Cyclomatic complexity = 7 (above threshold 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 lines */ }
else if (order.status == Status.PAID) { /* 15 lines */ }
else if (order.status == Status.SHIPPED) { /* 20 lines */ }
else if (order.status == Status.DELIVERED) { /* 8 lines */ }
else if (order.status == Status.CANCELLED) { /* 5 lines */ }
else { throw IllegalStateException() }
}
// Fix: polymorphism instead of switch
interface OrderHandler {
fun handle(order: Order)
}
Automated smell detection does not replace code review: static analyzers only find structural problems but do not catch semantic smells (Feature Envy, Inappropriate Intimacy). A combination of automated tools and human review yields the best results. Configure your CI/CD pipeline so that builds fail when complexity or method length thresholds are exceeded.
Refactoring is the primary method for eliminating code smells. Fowler describes dozens of refactoring techniques, each applicable to a specific smell. Extract Method — for long methods, Extract Class — for large classes, Move Method — for Feature Envy. It is important to perform refactoring in small steps, keeping the code working after each change.
Tests before refactoring are mandatory. If the code is not covered by unit tests, refactoring turns into rewriting with unknown results. For legacy code without tests, use Characterization Tests — write tests that capture the current behavior, then refactor. Testing gives confidence that business logic has not broken after refactoring.
Gradualness is the key to successfully fixing smells in mobile development. Do not try to rewrite a God Activity entirely. First extract the navigation layer, then the data layer, then the display logic. Each step should be accompanied by a commit and test run. Use feature toggles to enable the refactoring for a subset of users and roll back if problems arise.
IDE tools automate many refactoring techniques. Android Studio and IntelliJ IDEA offer built-in refactorings: Extract Method, Extract Interface, Pull Members Up, Encapsulate Fields. Xcode (starting with version 14) improved refactoring support for Swift. Using automatic refactorings reduces the risk of errors compared to manual code copying.
Mobile development adds its own specific smells related to platform constraints. In Android, this includes Context leaks, unclosed Cursors, and improper Lifecycle usage. In iOS, it is retain cycles through closures, incorrect Auto Layout handling, and giant ViewControllers. These smells not only impair maintainability but also directly affect application performance and stability.
Callback Hell is a characteristic smell for code working with asynchronous operations. Nested callbacks make code unreadable and hard to debug. Solution: coroutines (Kotlin), async/await (Swift 5.5+), RxJava/RxSwift, or Combine. According to Google I/O 2023, projects that migrated from callback style to coroutines reduced bug count by 30% and accelerated the addition of new features.
Platform Coupling — tight coupling of business logic to platform components. Testing such logic requires launching an emulator, which slows the feedback loop. Fix: Clean Architecture separates code into Domain (pure Kotlin/Swift with no platform dependencies) and Data/UI (with platform dependencies) layers. Business logic is tested on JVM without an emulator.
Frequently Asked Questions
No — Code Smell is not an error. Code with a smell works correctly, but it is hard to maintain, change, and test. A bug is incorrect behavior; a smell is a warning about potential future problems.
22 smells in the second edition of “Refactoring” (2019). Among them are Long Method, Large Class, Primitive Obsession, Data Clumps, Switch Statements, Speculative Generality, and others. The community has added dozens of new smells for modern paradigms and platforms.
A combination yields the best result: Detekt (Android/Kotlin), SwiftLint (iOS), SonarQube (both) for automated analysis, and code review for semantic smells. No single tool finds 100% of problems — human experience remains decisive.
Yes, if the code changes infrequently or will be completely rewritten soon. However, accumulating smells turns into technical debt: each new change becomes increasingly difficult, and the cost of fixing grows exponentially.
Yes — declarative frameworks have spawned new smells: giant @State blocks, improper handling of repeated renders, excessive recomposition, and lack of Extraction into separate Views. For SwiftUI, a typical smell is Massive View with dozens of @State variables.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also