Working Effectively with (Android) Legacy Code

How many times have you heard yourself (or a teammate) say one of the following?

  • How are we going to add this new feature when the code is a mess?
  • We can’t change this file — it’s too risky!
  • How do I test this class when it depends on X, Y, and Z?
  • There is not enough time to make the changes you want!
  • What does this code even do!?
  • I feel overwhelmed and it’s never going to get any better.

Android isn’t new anymore. Most apps are not greenfield projects. Many of us find ourselves in the position of working with code we did not author and which we don’t fully understand.

Legacy Code

But what is legacy code? First, let’s consider a strict definition.

Legacy code is code that we’ve gotten from someone else.

In the classic 2005 book, Working Effectively with Legacy Code, Michael C. Feathers offers a different definition.

Legacy code is code without tests.

Why? Without tests, there is no way to know if your code is getting better or worse.

Modern Android

Android development has come a long way since I started in 2009. Today we have support libraries, MVP, MVVM, MVI, dependency injection, unit tests, multidex, Espresso, RxJava, OkHttp, Architecture Components, Data Binding, Kotlin, Jetpack, etc, etc, etc.

But what if your app doesn’t currently use any of these things? What if it was written in 2009? What if you can’t take advantage of the latest and greatest frameworks due to restrictions on external dependencies? What if the original author of the code is long gone, and now you are tasked with fixing all the bugs, delivering new features on time, and keeping the project alive?

Change Management

For many of us, our first instinct when inheriting a project is to clean it up immediately, so the code is “the way it should be.” This is a bad idea.

All change has risk. If code is functioning properly, there is no reason to change it. Especially if that code is not well tested. However, when it comes time to fix a bug, or add a new feature, change becomes necessary.

But how can we improve and evolve a code base with poor architecture and no unit tests? By finding the seams. In Android, this often means where to draw the line between the operating system and your library or app.

Like a surgeon, you must decide where to make the incision. Then you can isolate the component that needs to change, extract it into its own method or class, and tests its behavior in isolation.

This can be messy work. Oftentimes things will get worse before they get better. You might have to duplicate code, relax scope, violate the single responsibility principle, or make other poor design trade-offs.

Over time, you can start to grow areas of high-quality code in legacy code bases, but the steps you must take to get there can often make the code around it uglier in the process (at least for a while).

In the coming posts, we will explore specific techniques for improving Android legacy code including:

  • Introducing a new architecture pattern
  • Migrating to a new framework
  • Breaking dependencies
  • Backwards compatibility
  • Enforcing code styles and standards
  • Leaving code better than you found it
  • And more…

Have an idea for something else you would like to see covered? Drop your suggestion in the comments section below.

This post is part of a series on working with legacy code on Android. It explores ways we can navigate, maintain, improve, and evolve legacy code using clean architecture, refactoring, dependency breaking techniques, and testing.

If you found this article helpful, please give it some applause 👏 to help others find it. You can also leave a comment below. Thanks!