ZEMOSO ENGINEERING STUDIO
April 4, 2021
11 min read

Refactored our Android code from zero to hero for a product rescue project

It’s hard to add new features to a product when the base code is imperfect. Plus, existing bugs further slow down the product development process.

Just like many other early stage, fast-paced organizations, we were managing a project where the code was dreadfully cluttered due to multiple, rapid changes in the features based on customer feedback and an incomplete understanding of what a good Android architecture should look like. 

We had fragments and classes that had codes ranging from 5000 Lines of Code (LOC) to 8000 LOC, violating clean code principles all over. We therefore decided to first clean up the code before the problem got worse. 

In this post, we'll discuss: 

● How we identified and fixed the issues in our code.

● How we didn't let migration and refactoring get in the way of sprints and deliverables.

● How anyone can adopt our methodology to write amazing Android applications with clean architecture.

The need to refactor

Here’s a list of questions that’ll help you decide if your code needs minor cleaning up or if it needs refactoring. 

● Does your code follow any coding architecture? Do words like Model-View-ViewModel (MVVM) or Model-View-Presenter (MVP) sound familiar? If the answer is yes, you need to plan for a major refactoring. 

● Null pointer, illegal state, or out-of-memory exceptions are common? A few major cleanups might do the trick if you have set up an architecture with not-the-best-implementation. 

● Does your code have cyclic dependencies? These are relatively difficult to fix. You need proper planning and an understanding of coding principles to clean up. 

● Are the file sizes too big? If your answers to the above questions are no, then just splitting the larger files into smaller files should be a piece of cake.

Roadblocks

We, as a rule for all similar projects, identify ‌ roadblocks before we begin to migrate or refactor the code to avoid costly surprises later. 

● Set reasonable timelines for the project so that the cleanup initiative doesn’t turn messy due to a lack of time. 

● We ensure that the refactoring has minimal impact on the current functionality of the product, and provide a team that’ll execute fast.

● Sprint Time — Deliverables will slow down for the next few sprints, and it is advisable to keep all the stakeholders in the loop to avoid any last minute surprises.

● Refactor feature by feature, in small chunks, to avoid any major breakdowns in the rest of the product now, or later. 

● Product and code knowledge will speed up the whole process, irrespective of file size. In-depth prior understanding of this is key. 

● Refactoring the app when it is already in production increases the overall risk. Make sure that end users are not impacted because of the decisions taken by developers and that they receive timely updates.

Checklist

Identify the features — Identify all the features of the app. Ad hoc changes or removals of code can cause unrecoverable damage, thereby increasing the overall time taken to refactor. Though messed up, our code had no or minimal hard coupling, and we could target one class at a time.

Source control setup — Correct branching strategy in Git helped us track the changes correctly and revert to the stable version in case anything did not work as expected.

Architecture — After some adequate research, we selected MVVM architecture for our code and decided to migrate the whole code base to MVVM since it was the best fit for our requirements. MVP architecture is equally good for smaller and simpler projects.

Technology — We made smart decisions, and evaluated and chose our tech stack before starting the process with extensive research. 

Classes or files to target — We created a priority list of which tasks and codes to target first to keep the migration organized. 

Code Quality Setup — We added SonarQube to the project to make sure it keeps reporting all the unnoticed bugs and errors, and then we treated it like a checklist for the code cleanup process.

Database management — Using Realm or SQLite or Room sounds straightforward, but we created an intelligent design to make it sustainable.

Profiling — App profiling is one of the key factors that helped us improve the code. We used profiling metrics as proof of concept for the assumptions made during the cleanups. It convinces the stakeholders easily too. 

Test cases — We started simple, and then detailed out our test cases. It helped speed up the cleanup process significantly. 

Documentation — We prepared for the eventuality that other people will be working on this code and documented all our utility codes and business logic. 

Getting rid of the helper class Helper.java, Utils.java, or Commons.java in almost all projects start as common reusable code and end up being a dumping ground. The size eventually crosses 5000 LOC. So we refactored these into dedicated classes. 

Resource cleanups — Unused or improperly created resources in the resources folder may cause major issues. 

Continuous integration (CI) checklist — This is easier for those who work on CI tools like Bitrise, Github actions, or Jenkins. We created a checklist of items to include in the CI workflow to make the integration smooth across the work of different developers. 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.

Distribution of tasks — It is very important to understand that code cleanups are best done with minimal or no overlaps across developers.

Key points to consider before starting

● Never start a refactoring process without going through the checklist above, or create your own checklist if required

● Never start refactoring until you know what each part of the application does

● Always make sure that a proper source control environment like Github is set up and branching rules are strictly defined

● Don't mix refactoring and feature updates

● When refactoring, change only the structure of the code, not the functionality. We optimized the code logic only after it was refactored and merged into the stable branch of the code

Our process

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

Basic setup

● We added some utility libraries like Lombok that eliminated most of the boilerplate code and added SonarQube to identify code quality issues at an early stage.  

● We added Kotlin support and migrated the code that we'd need to refactor or break. AndroidStudio converts ‌existing Java code to Kotlin without requiring any major monitoring.

● We added Dagger 2 to introduce dependency injection.

● We used data binding to introduce the MVVM architecture. i.e. Our view was in the XML files which were linked to the reactive model. This reduced the code size, and made it more readable and manageable. It also eliminated if else, setter, and getter.

● We cleaned up the layout files to reduce multiple renderings. We moved to constraint layouts and removed old layouts and nesting.

Begin refactoring

● We divided the code into multiple independent logical units or modules. We shared and assigned among all the developers to have minimal overlaps and bugs during the process.

● We added Gradle scripts to introduce environments and build scripts that optimize the builds for different environments like dev, stage, and production.

● We optimized the ProGuard to obfuscate the code.

● We added JaCoCo for code coverage along with SonarQube.

● We set up Bitrise as our CI/CD tool to build and test the app on every pull request. It also generated ‌the quality report and we merged the codes only when the minimum requirement set was met.

● We then introduced a repository pattern to the application to make handling Realm more efficient. This eliminated most of our transaction bugs.

● We introduced ViewModels to handle business logic and view manipulations. Our code in activities and fragments were trimmed and distributed to multiple classes. One key thing to remember is to try and maintain single direction dependencies.

● We made sure that the lower order classes had a single responsibility and were reusable.

● We reduced classes that needed the “Android context” to work. This allowed us to write unit tests much faster, without worrying about how the business logic will work based on the underlying operating system, overall future-proofing the app.

● We replaced most of the hard references, especially wherever async operations were involved, with SoftReference or WeakReference. This reduced our memory footprint drastically.

● We kept profiling performance regularly to make sure that our changes are improving performance too and not accidentally increasing memory usage.

We then cleaned up our resources. We used correct-size images and copy for all different screen densities, reducing overall memory impact. The larger the image, the larger the impact will be while rendering. Using the wrong storage folder may lead to unexpected image scaling and out-of-memory (OOM) issues.

● We also cleaned up colors.xml and strings.xml to remove all the hardcoding from layout files and Java classes, and then externalized all the resources.

It took us six months to refactor the whole code gradually to a cleaner and manageable version. Later we introduced RxJava to further clean up the code, specially in case of asynchronous code and business logic. We were earlier using traditional callbacks, listeners and runOnUi codes, and they soon started becoming unmanageable.

One advantage of planned refactoring is getting results incrementally. It's also easier to verify progress. Here are some of the key outcomes from our overall refactoring effort.

● Reduction in overall app size from 108MB to 37MB

● Improved performance 

● Reduced bugs 

● Stabilized application with test cases 

● More readable and manageable code 

● Increased security with improved auth flow and data fetching

● Faster delivery with CI/CD 

● Easier tracking with Changelogs and APK 

Refactoring a functioning app

When the app is already in production, the stakes are higher. Here’s how to transition smoother. 

● Do not make the 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 finalized architecture.

● Begin with the architecture setup, like adding dagger, databinding, unit tests, etc. without actually making changes to other files. Test and merge it to a 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 to refactor, but will unblock the deliverables and reduce the overall impact on users who are already using the app.

● Plan for a controlled distribution release. Release the refactored app to a small set of users first, and keep tracking the behavior. If it all looks good, then gradually increase the distribution percentage.

Learnings

● Understanding the front end architecture and memory impact on the underlying device is crucial

● Understanding and applying SOLID principles improves overall code quality

● Don't ignore some basic warnings to prevent long-term fall-out

● Regularly upgrade the libraries, and understanding the change-log to identify any breaking changes 

● Always document the code. Maintain a README.md with build steps, instructions, and other important notes to keep all the existing and new developers aware of the whole application

A version of this blog post was earlier published on Medium. 

ZEMOSO ENGINEERING STUDIO

Refactored our Android code from zero to hero for a product rescue project

April 4, 2021
11 min read

It’s hard to add new features to a product when the base code is imperfect. Plus, existing bugs further slow down the product development process.

Just like many other early stage, fast-paced organizations, we were managing a project where the code was dreadfully cluttered due to multiple, rapid changes in the features based on customer feedback and an incomplete understanding of what a good Android architecture should look like. 

We had fragments and classes that had codes ranging from 5000 Lines of Code (LOC) to 8000 LOC, violating clean code principles all over. We therefore decided to first clean up the code before the problem got worse. 

In this post, we'll discuss: 

● How we identified and fixed the issues in our code.

● How we didn't let migration and refactoring get in the way of sprints and deliverables.

● How anyone can adopt our methodology to write amazing Android applications with clean architecture.

The need to refactor

Here’s a list of questions that’ll help you decide if your code needs minor cleaning up or if it needs refactoring. 

● Does your code follow any coding architecture? Do words like Model-View-ViewModel (MVVM) or Model-View-Presenter (MVP) sound familiar? If the answer is yes, you need to plan for a major refactoring. 

● Null pointer, illegal state, or out-of-memory exceptions are common? A few major cleanups might do the trick if you have set up an architecture with not-the-best-implementation. 

● Does your code have cyclic dependencies? These are relatively difficult to fix. You need proper planning and an understanding of coding principles to clean up. 

● Are the file sizes too big? If your answers to the above questions are no, then just splitting the larger files into smaller files should be a piece of cake.

Roadblocks

We, as a rule for all similar projects, identify ‌ roadblocks before we begin to migrate or refactor the code to avoid costly surprises later. 

● Set reasonable timelines for the project so that the cleanup initiative doesn’t turn messy due to a lack of time. 

● We ensure that the refactoring has minimal impact on the current functionality of the product, and provide a team that’ll execute fast.

● Sprint Time — Deliverables will slow down for the next few sprints, and it is advisable to keep all the stakeholders in the loop to avoid any last minute surprises.

● Refactor feature by feature, in small chunks, to avoid any major breakdowns in the rest of the product now, or later. 

● Product and code knowledge will speed up the whole process, irrespective of file size. In-depth prior understanding of this is key. 

● Refactoring the app when it is already in production increases the overall risk. Make sure that end users are not impacted because of the decisions taken by developers and that they receive timely updates.

Checklist

Identify the features — Identify all the features of the app. Ad hoc changes or removals of code can cause unrecoverable damage, thereby increasing the overall time taken to refactor. Though messed up, our code had no or minimal hard coupling, and we could target one class at a time.

Source control setup — Correct branching strategy in Git helped us track the changes correctly and revert to the stable version in case anything did not work as expected.

Architecture — After some adequate research, we selected MVVM architecture for our code and decided to migrate the whole code base to MVVM since it was the best fit for our requirements. MVP architecture is equally good for smaller and simpler projects.

Technology — We made smart decisions, and evaluated and chose our tech stack before starting the process with extensive research. 

Classes or files to target — We created a priority list of which tasks and codes to target first to keep the migration organized. 

Code Quality Setup — We added SonarQube to the project to make sure it keeps reporting all the unnoticed bugs and errors, and then we treated it like a checklist for the code cleanup process.

Database management — Using Realm or SQLite or Room sounds straightforward, but we created an intelligent design to make it sustainable.

Profiling — App profiling is one of the key factors that helped us improve the code. We used profiling metrics as proof of concept for the assumptions made during the cleanups. It convinces the stakeholders easily too. 

Test cases — We started simple, and then detailed out our test cases. It helped speed up the cleanup process significantly. 

Documentation — We prepared for the eventuality that other people will be working on this code and documented all our utility codes and business logic. 

Getting rid of the helper class Helper.java, Utils.java, or Commons.java in almost all projects start as common reusable code and end up being a dumping ground. The size eventually crosses 5000 LOC. So we refactored these into dedicated classes. 

Resource cleanups — Unused or improperly created resources in the resources folder may cause major issues. 

Continuous integration (CI) checklist — This is easier for those who work on CI tools like Bitrise, Github actions, or Jenkins. We created a checklist of items to include in the CI workflow to make the integration smooth across the work of different developers. 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.

Distribution of tasks — It is very important to understand that code cleanups are best done with minimal or no overlaps across developers.

Key points to consider before starting

● Never start a refactoring process without going through the checklist above, or create your own checklist if required

● Never start refactoring until you know what each part of the application does

● Always make sure that a proper source control environment like Github is set up and branching rules are strictly defined

● Don't mix refactoring and feature updates

● When refactoring, change only the structure of the code, not the functionality. We optimized the code logic only after it was refactored and merged into the stable branch of the code

Our process

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

Basic setup

● We added some utility libraries like Lombok that eliminated most of the boilerplate code and added SonarQube to identify code quality issues at an early stage.  

● We added Kotlin support and migrated the code that we'd need to refactor or break. AndroidStudio converts ‌existing Java code to Kotlin without requiring any major monitoring.

● We added Dagger 2 to introduce dependency injection.

● We used data binding to introduce the MVVM architecture. i.e. Our view was in the XML files which were linked to the reactive model. This reduced the code size, and made it more readable and manageable. It also eliminated if else, setter, and getter.

● We cleaned up the layout files to reduce multiple renderings. We moved to constraint layouts and removed old layouts and nesting.

Begin refactoring

● We divided the code into multiple independent logical units or modules. We shared and assigned among all the developers to have minimal overlaps and bugs during the process.

● We added Gradle scripts to introduce environments and build scripts that optimize the builds for different environments like dev, stage, and production.

● We optimized the ProGuard to obfuscate the code.

● We added JaCoCo for code coverage along with SonarQube.

● We set up Bitrise as our CI/CD tool to build and test the app on every pull request. It also generated ‌the quality report and we merged the codes only when the minimum requirement set was met.

● We then introduced a repository pattern to the application to make handling Realm more efficient. This eliminated most of our transaction bugs.

● We introduced ViewModels to handle business logic and view manipulations. Our code in activities and fragments were trimmed and distributed to multiple classes. One key thing to remember is to try and maintain single direction dependencies.

● We made sure that the lower order classes had a single responsibility and were reusable.

● We reduced classes that needed the “Android context” to work. This allowed us to write unit tests much faster, without worrying about how the business logic will work based on the underlying operating system, overall future-proofing the app.

● We replaced most of the hard references, especially wherever async operations were involved, with SoftReference or WeakReference. This reduced our memory footprint drastically.

● We kept profiling performance regularly to make sure that our changes are improving performance too and not accidentally increasing memory usage.

We then cleaned up our resources. We used correct-size images and copy for all different screen densities, reducing overall memory impact. The larger the image, the larger the impact will be while rendering. Using the wrong storage folder may lead to unexpected image scaling and out-of-memory (OOM) issues.

● We also cleaned up colors.xml and strings.xml to remove all the hardcoding from layout files and Java classes, and then externalized all the resources.

It took us six months to refactor the whole code gradually to a cleaner and manageable version. Later we introduced RxJava to further clean up the code, specially in case of asynchronous code and business logic. We were earlier using traditional callbacks, listeners and runOnUi codes, and they soon started becoming unmanageable.

One advantage of planned refactoring is getting results incrementally. It's also easier to verify progress. Here are some of the key outcomes from our overall refactoring effort.

● Reduction in overall app size from 108MB to 37MB

● Improved performance 

● Reduced bugs 

● Stabilized application with test cases 

● More readable and manageable code 

● Increased security with improved auth flow and data fetching

● Faster delivery with CI/CD 

● Easier tracking with Changelogs and APK 

Refactoring a functioning app

When the app is already in production, the stakes are higher. Here’s how to transition smoother. 

● Do not make the 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 finalized architecture.

● Begin with the architecture setup, like adding dagger, databinding, unit tests, etc. without actually making changes to other files. Test and merge it to a 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 to refactor, but will unblock the deliverables and reduce the overall impact on users who are already using the app.

● Plan for a controlled distribution release. Release the refactored app to a small set of users first, and keep tracking the behavior. If it all looks good, then gradually increase the distribution percentage.

Learnings

● Understanding the front end architecture and memory impact on the underlying device is crucial

● Understanding and applying SOLID principles improves overall code quality

● Don't ignore some basic warnings to prevent long-term fall-out

● Regularly upgrade the libraries, and understanding the change-log to identify any breaking changes 

● Always document the code. Maintain a README.md with build steps, instructions, and other important notes to keep all the existing and new developers aware of the whole application

A version of this blog post was earlier published on Medium. 

Recent Publications
Actual access control without getting in the way of actual work: 2023
Actual access control without getting in the way of actual work: 2023
March 13, 2023
Breaking the time barrier: Test Automation and its impact on product launch cycles
Breaking the time barrier: Test Automation and its impact on product launch cycles
January 20, 2023
Product innovation for today and the future! It’s outcome-first, timeboxed, and accountable
Product innovation for today and the future! It’s outcome-first, timeboxed, and accountable
January 9, 2023
From "great potential" purgatory to "actual usage" reality: getting SDKs right in a product-led world
From "great potential" purgatory to "actual usage" reality: getting SDKs right in a product-led world
December 6, 2022
Why Realm trumps SQLite as database of choice for complex mobile apps — Part 2
Why Realm trumps SQLite as database of choice for complex mobile apps — Part 2
October 13, 2022
Testing what doesn’t exist with a Wizard of Oz twist
Testing what doesn’t exist with a Wizard of Oz twist
October 12, 2022
Docs, Guides, Resources: Getting developer microsites right in a product-led world
Docs, Guides, Resources: Getting developer microsites right in a product-led world
September 20, 2022
Beyond methodologies: Five engineering do's for an agile product build
Beyond methodologies: Five engineering do's for an agile product build
September 5, 2022
Actual access control without getting in the way of actual work: 2023
Actual access control without getting in the way of actual work: 2023
March 13, 2023
Breaking the time barrier: Test Automation and its impact on product launch cycles
Breaking the time barrier: Test Automation and its impact on product launch cycles
January 20, 2023
Product innovation for today and the future! It’s outcome-first, timeboxed, and accountable
Product innovation for today and the future! It’s outcome-first, timeboxed, and accountable
January 9, 2023
From "great potential" purgatory to "actual usage" reality: getting SDKs right in a product-led world
From "great potential" purgatory to "actual usage" reality: getting SDKs right in a product-led world
December 6, 2022
Why Realm trumps SQLite as database of choice for complex mobile apps — Part 2
Why Realm trumps SQLite as database of choice for complex mobile apps — Part 2
October 13, 2022
ZEMOSO ENGINEERING STUDIO
April 4, 2021
11 min read

Refactored our Android code from zero to hero for a product rescue project

It’s hard to add new features to a product when the base code is imperfect. Plus, existing bugs further slow down the product development process.

Just like many other early stage, fast-paced organizations, we were managing a project where the code was dreadfully cluttered due to multiple, rapid changes in the features based on customer feedback and an incomplete understanding of what a good Android architecture should look like. 

We had fragments and classes that had codes ranging from 5000 Lines of Code (LOC) to 8000 LOC, violating clean code principles all over. We therefore decided to first clean up the code before the problem got worse. 

In this post, we'll discuss: 

● How we identified and fixed the issues in our code.

● How we didn't let migration and refactoring get in the way of sprints and deliverables.

● How anyone can adopt our methodology to write amazing Android applications with clean architecture.

The need to refactor

Here’s a list of questions that’ll help you decide if your code needs minor cleaning up or if it needs refactoring. 

● Does your code follow any coding architecture? Do words like Model-View-ViewModel (MVVM) or Model-View-Presenter (MVP) sound familiar? If the answer is yes, you need to plan for a major refactoring. 

● Null pointer, illegal state, or out-of-memory exceptions are common? A few major cleanups might do the trick if you have set up an architecture with not-the-best-implementation. 

● Does your code have cyclic dependencies? These are relatively difficult to fix. You need proper planning and an understanding of coding principles to clean up. 

● Are the file sizes too big? If your answers to the above questions are no, then just splitting the larger files into smaller files should be a piece of cake.

Roadblocks

We, as a rule for all similar projects, identify ‌ roadblocks before we begin to migrate or refactor the code to avoid costly surprises later. 

● Set reasonable timelines for the project so that the cleanup initiative doesn’t turn messy due to a lack of time. 

● We ensure that the refactoring has minimal impact on the current functionality of the product, and provide a team that’ll execute fast.

● Sprint Time — Deliverables will slow down for the next few sprints, and it is advisable to keep all the stakeholders in the loop to avoid any last minute surprises.

● Refactor feature by feature, in small chunks, to avoid any major breakdowns in the rest of the product now, or later. 

● Product and code knowledge will speed up the whole process, irrespective of file size. In-depth prior understanding of this is key. 

● Refactoring the app when it is already in production increases the overall risk. Make sure that end users are not impacted because of the decisions taken by developers and that they receive timely updates.

Checklist

Identify the features — Identify all the features of the app. Ad hoc changes or removals of code can cause unrecoverable damage, thereby increasing the overall time taken to refactor. Though messed up, our code had no or minimal hard coupling, and we could target one class at a time.

Source control setup — Correct branching strategy in Git helped us track the changes correctly and revert to the stable version in case anything did not work as expected.

Architecture — After some adequate research, we selected MVVM architecture for our code and decided to migrate the whole code base to MVVM since it was the best fit for our requirements. MVP architecture is equally good for smaller and simpler projects.

Technology — We made smart decisions, and evaluated and chose our tech stack before starting the process with extensive research. 

Classes or files to target — We created a priority list of which tasks and codes to target first to keep the migration organized. 

Code Quality Setup — We added SonarQube to the project to make sure it keeps reporting all the unnoticed bugs and errors, and then we treated it like a checklist for the code cleanup process.

Database management — Using Realm or SQLite or Room sounds straightforward, but we created an intelligent design to make it sustainable.

Profiling — App profiling is one of the key factors that helped us improve the code. We used profiling metrics as proof of concept for the assumptions made during the cleanups. It convinces the stakeholders easily too. 

Test cases — We started simple, and then detailed out our test cases. It helped speed up the cleanup process significantly. 

Documentation — We prepared for the eventuality that other people will be working on this code and documented all our utility codes and business logic. 

Getting rid of the helper class Helper.java, Utils.java, or Commons.java in almost all projects start as common reusable code and end up being a dumping ground. The size eventually crosses 5000 LOC. So we refactored these into dedicated classes. 

Resource cleanups — Unused or improperly created resources in the resources folder may cause major issues. 

Continuous integration (CI) checklist — This is easier for those who work on CI tools like Bitrise, Github actions, or Jenkins. We created a checklist of items to include in the CI workflow to make the integration smooth across the work of different developers. 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.

Distribution of tasks — It is very important to understand that code cleanups are best done with minimal or no overlaps across developers.

Key points to consider before starting

● Never start a refactoring process without going through the checklist above, or create your own checklist if required

● Never start refactoring until you know what each part of the application does

● Always make sure that a proper source control environment like Github is set up and branching rules are strictly defined

● Don't mix refactoring and feature updates

● When refactoring, change only the structure of the code, not the functionality. We optimized the code logic only after it was refactored and merged into the stable branch of the code

Our process

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

Basic setup

● We added some utility libraries like Lombok that eliminated most of the boilerplate code and added SonarQube to identify code quality issues at an early stage.  

● We added Kotlin support and migrated the code that we'd need to refactor or break. AndroidStudio converts ‌existing Java code to Kotlin without requiring any major monitoring.

● We added Dagger 2 to introduce dependency injection.

● We used data binding to introduce the MVVM architecture. i.e. Our view was in the XML files which were linked to the reactive model. This reduced the code size, and made it more readable and manageable. It also eliminated if else, setter, and getter.

● We cleaned up the layout files to reduce multiple renderings. We moved to constraint layouts and removed old layouts and nesting.

Begin refactoring

● We divided the code into multiple independent logical units or modules. We shared and assigned among all the developers to have minimal overlaps and bugs during the process.

● We added Gradle scripts to introduce environments and build scripts that optimize the builds for different environments like dev, stage, and production.

● We optimized the ProGuard to obfuscate the code.

● We added JaCoCo for code coverage along with SonarQube.

● We set up Bitrise as our CI/CD tool to build and test the app on every pull request. It also generated ‌the quality report and we merged the codes only when the minimum requirement set was met.

● We then introduced a repository pattern to the application to make handling Realm more efficient. This eliminated most of our transaction bugs.

● We introduced ViewModels to handle business logic and view manipulations. Our code in activities and fragments were trimmed and distributed to multiple classes. One key thing to remember is to try and maintain single direction dependencies.

● We made sure that the lower order classes had a single responsibility and were reusable.

● We reduced classes that needed the “Android context” to work. This allowed us to write unit tests much faster, without worrying about how the business logic will work based on the underlying operating system, overall future-proofing the app.

● We replaced most of the hard references, especially wherever async operations were involved, with SoftReference or WeakReference. This reduced our memory footprint drastically.

● We kept profiling performance regularly to make sure that our changes are improving performance too and not accidentally increasing memory usage.

We then cleaned up our resources. We used correct-size images and copy for all different screen densities, reducing overall memory impact. The larger the image, the larger the impact will be while rendering. Using the wrong storage folder may lead to unexpected image scaling and out-of-memory (OOM) issues.

● We also cleaned up colors.xml and strings.xml to remove all the hardcoding from layout files and Java classes, and then externalized all the resources.

It took us six months to refactor the whole code gradually to a cleaner and manageable version. Later we introduced RxJava to further clean up the code, specially in case of asynchronous code and business logic. We were earlier using traditional callbacks, listeners and runOnUi codes, and they soon started becoming unmanageable.

One advantage of planned refactoring is getting results incrementally. It's also easier to verify progress. Here are some of the key outcomes from our overall refactoring effort.

● Reduction in overall app size from 108MB to 37MB

● Improved performance 

● Reduced bugs 

● Stabilized application with test cases 

● More readable and manageable code 

● Increased security with improved auth flow and data fetching

● Faster delivery with CI/CD 

● Easier tracking with Changelogs and APK 

Refactoring a functioning app

When the app is already in production, the stakes are higher. Here’s how to transition smoother. 

● Do not make the 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 finalized architecture.

● Begin with the architecture setup, like adding dagger, databinding, unit tests, etc. without actually making changes to other files. Test and merge it to a 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 to refactor, but will unblock the deliverables and reduce the overall impact on users who are already using the app.

● Plan for a controlled distribution release. Release the refactored app to a small set of users first, and keep tracking the behavior. If it all looks good, then gradually increase the distribution percentage.

Learnings

● Understanding the front end architecture and memory impact on the underlying device is crucial

● Understanding and applying SOLID principles improves overall code quality

● Don't ignore some basic warnings to prevent long-term fall-out

● Regularly upgrade the libraries, and understanding the change-log to identify any breaking changes 

● Always document the code. Maintain a README.md with build steps, instructions, and other important notes to keep all the existing and new developers aware of the whole application

A version of this blog post was earlier published on Medium. 

Recent Publications

ZEMOSO ENGINEERING STUDIO

Testing what doesn’t exist with a Wizard of Oz twist

October 12, 2022
7 min read
ZEMOSO ENGINEERING STUDIO

Beyond methodologies: Five engineering do's for an agile product build

September 5, 2022
6 min read
ZEMOSO ENGINEERING STUDIO

How we built a big data platform for a futuristic AgriTech product

June 3, 2022
8 min read
ZEMOSO NEWS

Zemoso Labs starts operations in Waterloo, Canada

May 25, 2022
5 min read
ZEMOSO ENGINEERING STUDIO

Honorable mention at O’Reilly’s Architectural Katas event

May 17, 2021
5 min read
ZEMOSO ENGINEERING STUDIO

Product dev with testable spring boot applications, from day one

May 4, 2021
5 min read
ZEMOSO ENGINEERING STUDIO

When not to @Autowire in Spring or Spring Boot applications

May 1, 2021
5 min read
ZEMOSO ENGINEERING STUDIO

Efficiently handle data and integrations in Spring Boot

January 24, 2021
5 min read
ZEMOSO ENGINEERING STUDIO

Our favorite CI/CD DevOps Practice: Simplify with GitHub Actions

October 25, 2020
5 min read
ZEMOSO ENGINEERING STUDIO

How to use BERT and DNN to build smarter NLP algorithms for products

February 14, 2020
12 min read
ZEMOSO ENGINEERING STUDIO

GraphQL — Why is it essential for agile product development?

April 30, 2019
12 min read
ZEMOSO ENGINEERING STUDIO

GraphQL with Java Spring Boot and Apollo Angular for Agile platforms

April 30, 2019
9 min read
ZEMOSO ENGINEERING STUDIO

Deploying Airflow on Kubernetes 

November 30, 2018
2 min read
ZEMOSO PRODUCT STUDIO

How to validate your Innovation: Mastering Experiment Design

November 22, 2018
8 min read
ZEMOSO PRODUCT STUDIO

Working Backwards: Amazon's Culture of Innovation: My notes

November 19, 2018
8 min read
ZEMOSO ENGINEERING STUDIO

Product developer POV: Caveats when building with Spark

November 5, 2018
2 min read

Want more best practices?

Access thought-leadership and best practice content across
the product development lifecycle

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

© 2023  Zemoso Technologies
Privacy Policy

Terms of Use
LinkedIn Page - Zemoso TechnologiesFacebook Page - Zemoso TechnologiesTwitter Account - Zemoso Technologies

© 2021 Zemoso Technologies
Privacy Policy

LinkedIn Page - Zemoso TechnologiesFacebook Page - Zemoso TechnologiesTwitter Account - Zemoso Technologies
May 25, 2023