Taking Over an Existing Flutter App: How I Stabilize an Inherited Codebase
My step-by-step process for inheriting a Flutter app from another developer — auditing, upgrading dependencies, fixing crashes first and shipping improvements without breaking what works.

A lot of my freelance work doesn’t start from a blank project. It starts with a message like: “Our developer left, the app is crashing, and we need to add a feature by next month.”
SafeMom was one of those projects: an existing consumer app that needed its critical bugs fixed, its stability improved, and new monetization added. Over the years I’ve developed a routine for these situations. It’s less about clever code and more about not making things worse while making them better.
Step 1: Get it building before changing anything
Before I touch a single line, I make sure I can build and run the app exactly as it is today:
- Clone the repo and check which Flutter version it was built with. Look at
pubspec.lock, theenvironmentconstraint inpubspec.yaml, any.fvmrcfile, or the CI configuration. - Use FVM to install that exact version for the project. Upgrading Flutter on day one is how you end up debugging twenty unrelated errors at once.
- Get the signing keys, API keys, Firebase config files and store console access from the client. Missing signing keys is the most common blocker — without the original Android upload key, you can’t publish an update (Play App Signing’s key reset process helps, but it takes time).
- Build a release version for both platforms and install it on a real device.
Only when I can reproduce the current production app do I start changing it.
Step 2: Audit before you fix
I spend the first day or two reading, not writing. What I’m looking for:
- Crash reports. If Firebase Crashlytics or Sentry is set up, the top crashes are the first priority list. If nothing is set up, adding crash reporting is my first change.
- Store reviews. Recent one- and two-star reviews tell you what users actually experience, which isn’t always what the client thinks is broken.
- Structure. Where does business logic live? How is state managed? Are there tests? Is there any consistency, or did three developers each do it their own way?
- Dependencies. Run
flutter pub outdatedand note packages that are discontinued, several major versions behind, or unmaintained. - Platform config.
minSdkVersion,targetSdkVersion, iOS deployment target, Gradle and Kotlin versions. Google Play requires a recent target SDK for updates, so an old one is a deadline you can’t ignore.
I write this up for the client in plain language: what’s healthy, what’s risky, and what I recommend fixing first. It sets expectations and avoids surprises later.
Step 3: Fix crashes first
New features are what the client asked for. But crashes are what’s costing them users and ratings right now. I fix the top crashes before anything else, and the fixes are often small:
- A null value from the API that the code assumed would always be there.
setState()called after a widget was disposed, because an async call finished after the user left the screen. Amountedcheck fixes it.- A
BuildContextused across an async gap to show a dialog or navigate. - An unhandled exception in a
Futurethat nobody awaited.
Future<void> _loadProfile() async {
final profile = await api.fetchProfile();
if (!mounted) return; // the user may have left the screen while we waited
setState(() => _profile = profile);
}
Shipping a stability release early also builds trust with the client. They see ratings stabilize before the “real” work even starts.
Step 4: Upgrade in small, safe steps
Outdated dependencies and old Flutter versions need upgrading eventually, but not all at once. My order:
- Upgrade Flutter one step at a time, following the migration notes for each release, and run the app after each step.
- Upgrade packages one at a time, starting with the ones blocking something (for example, a package that doesn’t support the required Android target SDK).
- Replace discontinued packages with maintained alternatives, behind a small wrapper so the swap touches as few files as possible.
- Run
flutter analyzeand fix warnings as I go — they often point straight at real bugs.
Each step gets its own commit, so if something breaks, it’s obvious which change caused it.
Step 5: Add a safety net where it matters
Inherited apps rarely have tests, and writing a full test suite for someone else’s code isn’t a good use of the client’s budget. Instead, I add tests where I’m about to make changes:
- Unit tests around the logic I’m modifying, written before I modify it, so I know I haven’t changed its behavior by accident.
- A few widget or integration tests for the most important flows: sign in, the core feature, purchase.
Over time, the parts of the codebase that change most end up with the best coverage — which is exactly where you want it.
Step 6: Improve structure gradually
It’s tempting to announce “this needs a rewrite”. Sometimes it does, but usually it doesn’t, and the client can’t pause the business while you rebuild. I follow the boy scout rule: leave each area a bit cleaner than I found it.
- When I work on a feature, I move its business logic out of widgets into a testable class.
- New features follow a clean, consistent structure, and old code migrates toward it when it’s touched.
- I don’t refactor code I’m not otherwise changing. Working code with no tests is best left alone until there’s a reason.
Step 7: Then build the new features
With a stable, building, up-to-date app, adding features is much less risky. On SafeMom, that meant integrating ad SDKs (including Facebook and TikTok) and an Adapty paywall for premium features. Because the groundwork was done, those integrations could go out confidently, and I could clearly tell a bug in the new code from a pre-existing one.
Communication is half the job
Technical steps aside, what makes these projects go well is communication:
- Explain what you found in terms of business risk, not code quality. “This could cause the app to be removed from Google Play in August” lands better than “the target SDK is outdated”.
- Give options with costs. Quick fix now vs. proper fix later, and what each means.
- Ship small and often. Frequent releases with visible progress beat a big-bang update after two months of silence.
Takeaways
- Reproduce the current build exactly before changing anything; pin the original Flutter version.
- Audit crashes, reviews, dependencies and platform config — and write it up for the client.
- Fix crashes first; they’re usually small and hugely valuable.
- Upgrade Flutter and packages one step at a time.
- Add tests where you’re about to change code, and improve structure gradually.
If you’ve inherited a Flutter app that needs rescuing, or your developer has moved on and you need someone to pick it up, get in touch.
Comments
Questions, corrections or your own experience — leave a comment below (GitHub sign-in).