DEV Community

Cover image for Oof – look, that's a legacy codebase!
Agustín Tomas Larghi
Agustín Tomas Larghi

Posted on

Oof – look, that's a legacy codebase!

If you are an engineer who has been in the industry for over nine or ten years, you will likely have experienced projects that are not "in such great shape".

You know what I'm talking about. Those projects where the first engineer decided to implement an MVP architecture based on some random article they skimmed from Medium about using one Activity and custom views for each screen.

Then, when the Jenga tower was shaking on the brink of collapse, the engineer figured it was time to "go looking for a new adventure" thus, a new developer had to be hired!

The new dev figured that the previous one had no idea what they were doing, so they decided to flip things over – this time we go with a custom BottomSheetDialogFragment for each screen. Surely this time it'll work!

It didn't.

And yet again, this engineer had to go looking for new challenges. Repeat this 5 times over a period of 5 years and you no longer have a piece of software — you have a ticking bomb. You have a Coke that will explode in your hand as soon as you grab it from the shelves and leave you with a face full of shrapnel.

Now it is your turn. I will share a few things I have learned to help you deal with these situations. Hopefully, some of these tips might come in handy.

Remove unused resources

Issue

You go through the codebase and more often than not see pieces of code that are completely grayed out, unused. There is no such thing as keeping code "just in case", you either use the code or you don't – keeping leftovers there is just going to confuse people, and it is probably a symptom of a deeper issue.

Solution

Android Studio has a "Remove unused resources" tool under Refactor

Image description

You can preview what the removal of the unused resources would look like and exclude some of those deletions if you don't feel comfortable enough with some of the things Android Studio picks.

PR templates

Issue

So, the QA engineers complain that every fix your team submits is like a butterfly effect: it creates 3 new different bugs, or they say the fix/feature isn't even working at all. This is what I call late-stage "I don't give a flying fig" syndrome. The code base is so fragile and so prone to bugs that no one even cares to test stuff, or even run those changes locally.

Solution

Set a PR template for the team. It is not necessary to use a super-long annoying-to-fill form, just put two items to complete every time an engineer wants to submit a PR. A short description of what's in there, and a video walkthrough of the engineer explaining the changes and showing how the feature works.

Image description

You know Slack lets you record and download videos?

Image description

You can use that tool to record videos, then download them, and drag-and-drop them into the PR description. It's a piece of cake, and won't take more than 5 minutes. But if your engineers can't even find the time to whip up a 5-minute video to explain their work, well, that's a whole other cake.

Kotlin basics

Issue

You go through the code and see things like mutable data types being used on re-assignable (mutable) variables

var someLiveData = MutableLiveData<Something>()

or

var someCollection = mutableListOf<Something>()

or maybe you see data class structures abusing var properties

data class Something(
  var prop1: SomethingElse?,
  var prop2: SomethingElse?
  // ...
)
Enter fullscreen mode Exit fullscreen mode

This is usually a symptom of engineers who haven't fully migrated from Java to Kotlin. It is important to fully understand concepts like mutability and nullability.

Solution

This is a symptom of a lack of Kotlin basics. I'd like to defer to a super-helpful article I read some time ago:

https://www.javacodegeeks.com/starting-with-kotlin-cheatsheet

Tech Soup

Issue

Alrighty, in different corners of the app, you have screens that are either using ViewBinding, Kotlin Synthetics, Butterknife (because why not?), or Composables.

Some screens have the ViewModels communicate back to the UI using either LiveData, Flow, or Kotlin Channels, and some are using DataBinding.

Every engineer that passed through this project had their own unique genius way of doing things... because the guy before them was a complete idiot, obviously.

Solution

Try to find a balance between what the team thinks the best practices are and what you already have there in the codebase. Schedule either weekly or bi-weekly meetings with the engineering team, go layer by layer through the architecture and decide what the best practices are going to be from now on. For example:

"Okay, we use Retrofit for the API calls, do we wrap the responses on a Response wrapper to carry over the status response or we don't care about it?"

"Okay, how do we have ViewModels communicate to the UI from now on? We have all these options, what do we use and why?"

"Okay, do we use Jetpack Compost or do we use something else as a UI framework?"

You go layer by layer, figure out what the available options are (please don't thrust yet another tool into the mix!), and pick one. If possible, document that somewhere, either Notion or a wiki or whatever, but do document it – agreeing on something and not writing it down is the same as doing nothing.

Navigation Soup

Issue

It's like a choose-your-own-adventure book...except in the land of app navigation! Some parts of the app are using Navigation Graphs. Some are going for the one Activity, many custom views approach. Some are full-screen DialogFragments. Others are full-height BottomSheetDialogFragments. What will you choose for your app journey?

Solution

Same as above, sync with the rest of the team and pick one solution. I'd recommend that whatever you decide, you use Fragments. Think about all the third-party libraries out there that provide some sort of UI; they all do it either through Activities or Fragments. The goal for each flow of your app (the login flow, the account creation flow, the onboarding flow, etc.) should be exactly that — expose each of those flows as if you were sharing an SDK that would be used by other developers.

Of course, this requires a bit more technical brainstorming depending on the spaghetti you have at hand – I'd recommend that if the app has a base class for each screen (those awesome noodles of wisdom) you keep using them. Hear me out:

Let's say your app works using BaseSomethingFragment, each screen extends on this base class. Internally, BaseSomethingFragment extends from BottomSheetDialogFragment and it tweaks it so it prompts at full height.

Something you could do is to tweak this base class so it has a FragmentViewContainer and it uses a Navigation Graph internally, that way you can create the rest of the screens using Fragments, and eventually move every screen into bare Fragments instead of using that base class.

Just an idea, it's not a foolproof plan, but it'll give you the chance to do things one step at a time.

Isolate new code through modules

Issue

Given that the app is one large monolithic :app module, containing all the above items, how can we begin to implement our fresh approaches and start to separate the components?

Solution

Sunset the APP module. All the apps out there can be divided into different flows. For example, when the user goes through the login screens, that's the login flow – it might be a collection of 2, or 3 screens, but that's a self-contained flow. You might also have an onboarding flow, account creation flow, etc.

Each of these flows should be contained in a feature module (just a fancy name for a library module) and each of these feature modules should be completely independent of each other so we prevent dependency-spaghetti. You can do this step by step:

First, you have your monolithic app module

Image description

Now, just to keep things tidy let's rename that module into :app-legacy just everyone knows that we aren't supposed to keep adding things in there.

Image description

We are going to create a new application module, namely :company-app and have it depend on the old :legacy-app module. We have also turned the :legacy-app module from an application module into a library module.

Image description

Most likely, your old :legacy-app module had an Application class – in situations like these, the Application class is usually highly bloated with code. So, to start fresh you can have your new :company-app application module have its own Application class that extends from the old one. You can also mark the old CompanyApp as @Deprecated to prevent further things from being added in there.

Image description

You might also have to move many plugins from the :legacy-app down into the new :company-app application module. For example, if you are using Firebase and google-service.json you might have to move these down there, as they need to be plugged into the application module.

Now you can create completely independent feature modules to either refactor existing flows or create new ones. Let's say we had an account creation flow that was rid of bugs and we want to extract it out into an isolated module so we can refactor things safely.

First, we create a new library module for this new flow.

Image description

Now, we move all the classes, all the Fragments, all the stuff specific to that flow into that new library module. If something is reused between flows, let's say you have a SharedPreferences wrapper, that stays up in the :app-legacy module.

What about if we want to navigate between feature modules? Let's say we have another feature module for another flow of the app.

Image description

Well, you can use a Navigation class. You can declare an interface up in the :app-legacy that exposes the entry points for each of the screens in each flow. Something like:

interface MyNavigationProvider {
   fun getLoginFragment(context: Context): `BaseSomethingFragment`
   fun getUserProfileDetail(context: Context): Intent
   // ...
}
Enter fullscreen mode Exit fullscreen mode

You can provide either Fragments or Intents, whatever fits your architecture best.

This interface would be implemented in the NewCompanyApp Application class, and since the :company-app module ties all the feature modules together it knows about all these classes.

If you ever need to jump from one screen in one feature module to another, the only thing you need to do is to cast the application context into the MyNavigationProvider interface that was declared up in :app-legacy

Something like:

getApplicationContext() as? MyNavigationProvider

and that's it!

Now, don't think the job is done yet. This is the first step on a long road. Eventually what I try to do is turn the :app-legacy into a :core module, with just the things that are common to all feature modules. I try to extract the UI and everything else down into feature modules.

But this, at least, should be enough to provide your team with a way to create new flows independently.

Image description

There are many things you can do after this, you can extract all your base UI into a :design-system library module, so you encapsulate the look-and-feel of your app in one single place, but I would strongly recommend you first get the basics working, then you can start thinking about adding extra stuff.

Set a coding standard

Issue

Every PR someone submits seems to have more changes due to code formatting than actual substance - it's like a one-line bugfix needs 30 changes; one for the bug and 29 for imports turning into wildcard imports or tab-formatting. Who knew code formatting could be so complicated?

Solution

Either push some of the .idea files up into the repo, or share the code style configuration across the team.

That's all

Finally, set expectations with everyone in the team, engineers and uppermanagement, lay all the things on the table so everyone is aware of what's going on and what needs to be fixed.

I'm sure most people will cry "micromanagment!" at some of my proposals, but hey, to each their own – you like to push code up and pretend it's working, that's your prerogative. That's not my thing.

Top comments (0)