How we refactored our Android code from Zero to Hero

Imperfectly written codes can lead to nightmares. No therapist can help in getting rid of them but the clean code. Here, I will share my experience of how we cleaned our Android code to get the best out of it…

Mohammed Atif
Engineering@Zemoso

--

src: https://developer.android.com/images/brand/Android_Robot.svg

Imperfectly written code can not only cause issues while adding new features but also the existing bugs and errors can drastically slow down the whole development process.

Just like any other early stage fast paced startup or organisation, we too ended up in a similar situation where our code got dreadfully cluttered due to multiple and rapid changes in the features based on customer feedback, lack of experienced developers in the project and last but not the least lack of initial knowledge on Good Android Architecture.

We had Fragments and Classes that had codes ranging from 5000 LOC to 8000 LOC. Multipurpose classes and Fragments which were stabbing clean code principles again and again.

But as the saying goes, it’s never too late to fix things and few months after working on that code we decided to clean it up for adding new features. So in this article I will share my experience of we identified and fixed all the issues in our code within few months (That too without having any major impact on our sprints and deliverables) and how anyone can adapt this technique to write amazing Android Applications with clean architecture.

Do we really need refactoring?

Sometimes, the code is good enough and just requires some very minor cleanup. And other times it might be tangled enough to plan a major refactoring. Below questionnaire should help you decide whether to go ahead with the refactoring, if yes then to what extent

  1. Does your code follow any coding architecture? Does words like MVVM or MVP sounds familiar?
    Then you need to plan a major refactoring. Read up to know more about the process
  2. Null Pointer Exceptions, Illegal State Exceptions or Out Of Memory Exceptions are common in the project?
    You might have setup an architecture, but the implementation might not be good enough, so few major cleanups are to be done
  3. Does your code has cyclic dependencies?
    These are difficult to fix and need a proper planning and understanding of coding principles to cleanup
  4. Are the file sizes too big?
    These should be pretty straight forward. If all the above questions are answered as negative, then just splitting the larger files into smaller files should be a piece of cake

The Roadblocks

Decision to migrate or refactor the code is never easy and its very crucial to identify the Roadblocks beforehand to avoid any surprises during the course of migration

  1. Deadlines — Make sure the deadlines are not tight, otherwise cleanup can turn into a mess up in no time
  2. Deliverables— Its critical to understand that the dependency on this migration is minimal or the team size is adequate to address the new features and migrations quickly
  3. Sprint Time — Deliverables are definitely going down for next few sprint and it is advisable to keep all the stakeholders in loop to avoid any last minute surprises
  4. Break down of working features — One common concern for all the organisations is, what if we end up breaking something which is already working? So it is really important to target the refactor feature by feature in smaller chunks to avoid any major breakdowns later in the process.
  5. Code Size and Knowledge — It’s not the number of files that will eventually cause the major issues, it’s the product and code knowledge that will speed up the whole process. So always make sure that you know what your code contains and what it does before taking the decision of migration
  6. Whether the app is already in production — Refactoring the app when it is already in production increases the overall risk. We have to make sure that end users are not affected because of the decision taken by developers and they receive time to time updates

The Checklist

Well, since we are talking about cleaning up a mess we had to make sure we we are not making it messier unknowingly. So we decided to come up with a checklist of what needs to be done to get the codes clean and eventually keep it cleaner going forward

  1. Identifying the features — It is very important to identify all the features of the app. Ad hoc changes or removals of code can cause unrecoverable damage and thereby increase the overall time to refactor. Though messed up, our codes were still managing with no or minimal hard coupling and hence we were able to target one class at a time.
  2. Source Control Setup — Correct branching strategy in Git helped us to track the changes correctly and allowed us to revert to stable version in case anything broke or did not work as expected
  3. Architecture — After some adequate research, we selected MVVM Architecture for our code and decided to migrate the whole code base to MVVM Architecture as it was a nice fit for our requirements. MVP architecture is equally good for smaller and simpler projects
  4. Technology — We had to make some major decisions to pick the right technology before we could even start the revamp process of our code. It is always recommended to do sufficient research before finalising the tech and hence we finalised our tech stack. More details in upcoming sections
  5. Classes or Files to Target — In the temptation to do it all, we end up doing nothing. To reach destination we had to learn taking the smaller steps. So we shortlisted some part of the code that had to be cleaned for the first iteration
  6. Code Quality Setup — We added sonarqube to the project to make sure it keeps reporting all the unnoticed bugs and errors and then we used it to treat like a checklist for the code cleanup process
  7. Database Management — Using Realm or SQLite or Room sounds straight forward but it needs some good design to make it sustainable
  8. Profiling App profiling is one of the key factors that helped us improve the code to a larger extent. Having profiling metrics as proof of concept for the assumptions you would have made during the cleanups can be really helpful to convince the stake holders
  9. Test Cases — It was difficult to start writing test cases, so we decided to start easy and then go detailed. Though many call test cases as formality, they literally helped us to speed up the whole cleanup process and later to keep it clean. More details in upcoming section
  10. Documentation — Sounds like an overkill, but we all know that after certain point of time we ourselves struggle to understand our own code. Imagine the difficulty that will be faced by others. So we decided to make sure that all our utility codes or business logic are properly documented
  11. Getting rid of helper classHelper.java , Utils.java or Commons.java almost every project that needs cleanup will have at least one of above class. They start as classes for common reusable code and soon end up as a dumping ground for all sorts of codes and code size eventually crosses 5K LOC. So refactoring this into dedicated classes becomes necessary
  12. Resource Cleanups — Sometimes the unused or improperly created resources lying in the resources folder might be causing the major issues even without your knowledge. So cleaning them appropriately becomes necessary
  13. CI Checklist — Trivial for those who already worked on CI tools like Bitrise, Github actions or Jenkins. Not too difficult for those who just started with it. But having a checklist of items to include in CI Workflow makes sure the integration is done smoothly when multiple developers are working on the project. A simple checklist could include a gradle test that validates both the code compilation and test cases. Adding sonarqube or sonarcloud to the pipeline is always a plus
  14. Distribution of tasks — It is very important to understand that code cleanups are best done when there are minimal or no overlaps among the developers

Some key points to consider before starting

  1. Never start a refactoring process without going through the checklist above, or create your own checklist if required
  2. Never start the refactoring until and unless you know what each part of the application does
  3. Always make sure that proper source control environment like Github is set and branching rules are strictly defined
  4. Do not mix refactoring and Feature change. i.e. When performing a refactor make sure to just change the structure of the code and not to play around with the functionality of the code. For example, we had a file download code that was quite complicated. So we first broke it down to smaller classes or methods without changing the actual implementation. Then we wrote the test cases to validate the current code. Once this refactor was merged to stable branch only then we worked on optimising the logic of the code

The Process

Once we had everything in place, we started the cleanup. Please note that the sequence of these operations plays crucial role

  • We began by adding some utility libraries like Lombok that totally eliminates most of the boilerplate code and SonarQube that identifies the code quality issues at early stage
  • Then we added Kotlin support and instead of changing whole code to Kotlin at once, we just migrated the codes that we thought to refactor or break down. Android Studio provides a great utility to convert the existing Java code to Kotlin without any major monitoring
  • Then we added Dagger 2 to introduce dependency injection. Read more about dependency injection here
  • Then we added data binding to introduce the MVVM architecture. i.e. Our view was in the XML files which were linked to reactive Model. This reduces the code size by huge factor and also make it more readable and manageable. Just by adding data binding, most of the if else and setter and getter got eliminated
  • Along with Data Biding we also cleaned up the layout files to reduce the multiple renderings. We eventually moved to constraint layout and got rid of old layouts and nesting
  • Once the basic setup was done, we divided the code into multiple independent logical units or modules and shared among ourselves so as to have minimal overlaps and bugs during the development process
  • We added gradle scripts to introduce environments and build scripts to optimise the builds for different environments like dev, stage and production
  • We optimised the Proguard to obfuscate the code.
  • We added JaCoCo for code coverage along with SonarQube
  • We setup Bitrise as our Ci/CD tool to build and test the App on every PR. It also generated the the quality report and we merged the codes only when the minimum requirement set is met
  • We then introduced repository pattern to the application to make Realm handling more efficient. This eliminated most of our transaction bugs
  • As the things started getting together, we introduced ViewModels to handle the business logic and view manipulations. Eventually our codes in Activities and Fragments got trimmed down and got distributed to multiple classes. One key thing to remember is to try and maintain a single direction dependencies
  • Elaborating on single direction dependency, we have to make sure that lower order classes must be independent of why it is being used. i.e. Classes must have a single responsibility and should be reusable
  • One key change that we did was try and reduce the classes that needs the “Android Context” to work. This allowed us to write unit tests much faster and cleaner without having to worry about how our business logic will work based on underlying operating system
  • Then we replaced most of the hard references, specially wherever async operations were involved, with SoftReference or WeakReference. This particular change reduced our memory footprint drastically
  • We kept performing app profiling time to time to make sure that our changes are actually improving the performance too or accidentally not increasing the memory usage
  • We then cleaned up our resources. Having correct size images and all the copy for different screen densities plays a crucial role in improving the resources as well as the overall memory impact. Larger the image, larger will be the impact while rendering. Choosing the right folder to save the images is also crucial. Using wrong folder may lead to unexpected image scaling and OOM issues
  • We also cleaned up colors.xml and strings.xml to remove all the hardcoding from layout files and Java classes and then we externalised all the resources
  • It took us 6 months to gradually refactor the whole code to a much more cleaner and manageable code
  • Later we introduced Rx Java to further clean up the code specially in case of asynchronous codes and business logic because we were earlier using traditional callbacks, listeners and runOnUi codes and they soon started becoming unmanageable

The Outcomes

One advantage of a planned refactoring is getting the results in incremental order and it is easier to verify the progress. Here are some of the key outcomes from our overall refactoring effort

  1. Reduction in overall app size — Hard to believe but our apk size went down from 108MB to 37MB
  2. Increased performance — Before and After results of the profiling were amazing and verified all our changes that we did for the performance and memory improvements
  3. Reduced Bugs — Number of known and unknown bugs came down tremendously. Since refactoring requires keen observation and understanding of the current code base, it helped us to identify some obvious bugs which were missed earlier
  4. Stable Application — Writing test cases made sure that some common bugs do not appear ever again
  5. More readable and manageable code — Separating out the codes into their respective files, having a dedicated architecture and documentation made our code much more readable and manageable. Adding new features became a straight forward task
  6. Increased Security — One side product of the overall refactoring is better security of the app. Overall auth flow of the app improved as we refactored the data fetching code
  7. Faster delivery — Adding CI/CD not only helped us to validate the coding errors early but also helped us to efficiently manage our releases. Changelogs and APK became easy to track manage

What if the app is already in production?

When the app is already in production stakes become higher. Here are some things that can be done to make the transition smoother.

  • Do not make whole team work on the refactoring. While a part of the team can work on refactoring old code, other developers can continue working on new features, but with planned and finalised architecture
  • First begin with the architecture setup like adding dagger, databinding, unit tests, etc without actually making changes to other files. Test and merge it to stable branch so that other developers can still continue building new features on top of it
  • Refactor in smaller chunks, test and deliver. This will take longer time to refactor but will unblock the deliverables and reduce overall impact on the users who are already using the app
  • Plan for a controlled distribution release. Release the refactored app to a small set of users first, keep tracking the behaviour and app workings, if all looks good then gradually keep increasing the distribution percentage.

The Learnings

  1. Understanding the front end architectures and memory impact on the underlying device is crucial
  2. Understanding and applying SOLID principles improves the overall code quality
  3. Do not ignore some basic warnings as they might prove to be fatal in no time
  4. Time to time upgrade of the libraries is critical but understanding the change-log to identify any breaking changes is more important
  5. Always document the code. Maintain a README.md with build steps, instructions and other important notes to keep all the existing and new devs aware of the whole application

These refactorings were done about 4 years ago, hence few points might not be relevant today. But as I mentioned in the start, research before implement always help. And core approach always remain same.

Please let me know about your refactoring experience or let me know if you have different opinion on the process in the comments section

In ZeMoSo technologies, we follow the best design and coding principles ensuring highest quality applications every time. To know more about our engineering process follow us on https://medium.com/engineering-zemoso

--

--