← All posts

5 min read

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.

  • Flutter
  • Maintenance
  • Freelancing
  • Code Quality
Cover illustration for Taking Over an Existing Flutter App: How I Stabilize an Inherited Codebase

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, the environment constraint in pubspec.yaml, any .fvmrc file, 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 outdated and 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. A mounted check fixes it.
  • A BuildContext used across an async gap to show a dialog or navigate.
  • An unhandled exception in a Future that 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:

  1. Upgrade Flutter one step at a time, following the migration notes for each release, and run the app after each step.
  2. 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).
  3. Replace discontinued packages with maintained alternatives, behind a small wrapper so the swap touches as few files as possible.
  4. Run flutter analyze and 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).