Mobile App Codebase Refactoring
Refactoring is commissioned not when "it would be nice"—but when adding a new feature takes three weeks instead of three days because four screens depend on a single God Object with 2000 lines. Or when Instruments shows a 30-second main thread stall opening the chat, and nobody dares touch it.
What a Codebase Needing Refactoring Looks Like
iOS: ViewController with 1500 lines containing URLSession, CoreData, business logic, and cell height calculations in one file. NotificationCenter with magic-string notification names scattered across the project. Closure hell in completion handlers, missing [weak self] = memory leaks showing as steady Allocations growth in Instruments.
Android: Activity with 800 lines, direct SharedPreferences calls in the UI layer, AsyncTask instead of coroutines (deprecated since API 30), hard dependencies between features through static singletons. LiveData and ViewModel are hooked up, but logic still lives in Activity/Fragment.
React Native / Flutter: components with business logic directly in JSX/build, state managed via setState in 200+ line components without layering, missing type safety (any everywhere in TypeScript, dynamic in Dart).
What We Do and in What Order
Refactoring without tests is rewriting with hope you didn't break anything. First step: always cover critical paths with snapshot and unit tests before changes. This fixes current behavior as the benchmark.
iOS. Extract networking into a dedicated NetworkService with async/await (Swift Concurrency). ViewController receives only ViewModel via Dependency Injection (plain init injection suffices for simple cases—no Swinject needed). CoreData operations—move to PersistenceController with NSPersistentContainer.performBackgroundTask. Combine or async/await replaces closure callbacks—code readability transforms dramatically.
Android. Migrate from AsyncTask / RxJava to Kotlin Coroutines + Flow. ViewModel + StateFlow instead of mutable LiveData. Repository pattern—UI knows nothing about data sources: Room, Retrofit, or cache. Hilt for DI—eliminates static singletons that make testing impossible.
Real case: Android workout tracking app, 2 years in production, 3-person team. Main issue: WorkoutActivity.java—1800 lines, Bluetooth sensor connection, SQLite writes, statistics calculation and UI in one class. New feature (second sensor type support) estimated at 6 weeks. After refactoring: BluetoothSensorManager, WorkoutRepository, StatisticsCalculator, WorkoutViewModel—each class under 200 lines with clear responsibility. Next similar feature: 5 days.
Managing Technical Debt Incrementally
Full refactoring "all at once" almost always backfires. Follow Boy Scout Rule: each PR improves the code it touches. For large refactoring: feature branch with screen-by-screen decomposition, run old and new code in parallel via feature flag, gradually shift traffic.
Outcome Metrics
Not "it got better," but concrete numbers:
- Build time (modularity speeds incremental builds)
- Average time adding new features (velocity from Jira/Linear)
- Crash count (Firebase Crashlytics, before and after)
- Test coverage (Istanbul/JaCoCo)
Timeline: refactoring one module / screen—1–2 weeks. Full architectural restructuring of the app—6–14 weeks depending on codebase size and test coverage.







