Home Insights UI and Mobile Unlock innovation with Android application modernization
Unlock innovation with android application modernization

Unlock innovation with Android application modernization

Everything needs refactoring eventually

There are many architecture approaches used in the Android development field nowadays. You have probably already chosen your preferred methods and strive to use a unified approach when writing your applications. However, in some cases many approaches have to co-exist in the same project, often in conflict with each other. What’s more, a project can be written with technologies and approaches that become obsolete before the project is even completed. And this is where refactoring comes into play.

The meaning of refactoring can vary a lot. So much so that the only common attribute of refactoring is changing the variable name. But sometimes it’s about changing the architecture of an entire module.

The reason such tasks are performed is because most developers prefer to use new technologies, for obvious reasons:

  • Newer tools are more likely to be easier to use, because they can give you new ways to solve routine problems.
  • Sometimes, they even bring brand new ways of thinking about the project.

The big picture

In simple words, refactoring means changing the implementation without changing the behavior. Just as you start making things cleaner though, you are bound to begin noticing other places that need to be set in order too. And after a while, this evolves into something that can be expressed as a kind of Boy Scouts code – every place you’ve visited should be left cleaner than it was before you arrived.

Some key principles of refactoring are described in Martin Fowler’s book “Refactoring”; they are:

  • All refactored code should be tested.
  • Refactored code should have advantages compared to the old code.
  • Refactoring should consist of the small pieces that you can commit.

Sometimes it’s very difficult to understand where refactoring ends and rewriting begins. So although our story is about rewriting, we were guided by the rules of refactoring established by Martin Fowler.

Eat an elephant one bite at a time

The more I learn refactor, the more I realize how much I don’t know still have to refactor.

It’s a classic feeling. You cannot estimate and preview the whole scope before you start a deal. Each refactored line, method or class ultimately leads you to thoughts of rewriting the whole project.

But can you imagine how much time and effort needs to be spent? There are hundreds of thousands of lines of code. Lines that someone is still writing. Don’t forget that the project keeps evolving so making unagreed changes makes it extremely vulnerable.

And this is the very moment at which the third rule from the previous section comes to the rescue: “Refactoring should consist of the small pieces that you can commit”. It may sound odd to do rewriting in pieces but this is the approach that has helped us to perform efficient rewriting of a running project. Step by step, screen by screen, feature by feature – that is how we eat an elephant.

Our elephant

Name: Androidapplication

Field: E-commerce

Age: 5 y/o

Architecture: Single activity app

Design patterns: MVC + MVP + MVVM + their wild mixes

Structure: app module + several modules

Programming language: Java + Kotlin

In a nutshell, the project involves a mobile client for a large US-based department store network. Since its very beginning the application has survived several iterations of rewriting – enough to say that in the first version it was a web view wrapper for the website. The main purpose of the project has evolved from being just a showcase, to a handy place for online purchases, to the application having dozens of unique screens, several payment systems and microservices on the back-end. The building process of the application could take an hour or more just to describe.

In the current on-demand marketplace, customers dictate the rules of engagement. And with the rapid rate of cool new features becoming available, businesses need to be ever-present and just as rapid in development and delivery. In such circumstances, remaining stagnant means falling back.  In the case of the project we were tasked with, the application had become barely scalable and the process of onboarding a new team member to work on it would take way too much time.

After contemplating forward-looking ways for development, we decided to perform not just massive refactoring but a total rewrite. Every single entity inside the application would need to be revised and either optimized or recombined with the others.

A brief outline of the refactoring could be expressed as:

  • improving project structure understanding;
  • bringing new trending technologies to the application to get rid of outdated frameworks and approaches; and
  • implementing proven ways to work with the obvious tricky places that couldn’t be elegantly built into the common architecture.

Starting point

The main problem with the application was its age. It is no secret that in mobile development things are changing quickly. For half a decade, the project had been developed and refactored. Some features were written from scratch in separate modules or packages. Some tiny portions of new functional requirements were added in old classes which made the code messy.

In some cases, old code was not removed after refactored code was extracted to new modules or packages. It was confusing for newcomers, because multiple implementations existed for the same functionality. At the same time, adding new code to old code can cause even bigger problems. The development was started when many contemporary key tools like binding did not exist yet. So, the initial implementation was based on the old-school MVC approach.

For sure, when making changes, the initial pattern used for the feature implementation should be respected. For some reason, however, many developers ignore this rule. An alternative to the initial approach is to refactor the complete screen with a new one, but such a change would need to be approved by the business and be well tested. As a result, there are some places in the code where MVC is mixed with MVVM.

The bulk of legacy code was placed in a huge monolith module that was separated by packages. Over the project lifetime, separate modules were created for features that were rewritten from scratch. Some custom views and other common components were also extracted to separate modules. For such a heavy and complex mechanism, Gradle incremental building was not applicable, but this wasn’t the only problem.

Upon investigation of the accumulated tech stack, we discovered a myriad of different tools: Java, Kotlin (it was accepted as a standard for newly created files), Dagger2, RxJava, Retrofit, LoganSquare, Mochi, JUnit, Mockito, and more. In short, we can surmise that the team was experienced with old approaches and tools as well as brand new trends in the Android environment.

Goal

Changing old-fashioned approaches is a dream for the majority of developers who work on projects for a solid amount of time. However, only a minority ever actually get the opportunity to do so. Despite the fact that it is not likely for product owners to allow it, sometimes such opportunities arise for a lucky few development teams. This time, we were the lucky ones.

The key challenge we needed to resolve was logically separating and low coupling the updated components.  At the same time we needed to ensure that we kept a similar look to the legacy components. With such an approach we can avoid feature rollbacks, and hence, user confusion.

The rewrite project was created as a separate project that could be built and run independently and the rewritten codebase was regularly added as a subfolder to the legacy project using merge procedures. As a result, both projects can be run separately as well as together using special glue mechanisms. Enabling or disabling the rewrite part or specific features is managed by kill switches, ​​also known as feature flags, received from the server.

Further, the rewrite project needed to adopt modern best practices so it strictly followed the accepted vision of Clean architecture and used MVVM as an architecture pattern. Novelties of Android Jetpack could also be used in full power. From an implementation perspective, we kept the convention “only Kotlin for new files”, and technologies such as Dagger2 for DI, RxJava, Retrofit, Mochi. Some additional restrictions were also added to CI, such as code formatting check, test coverage, module dependency rules etc.

Main project tips

  • The rewrite code is located in the ‘apprewrite’ folder in the legacy project.
  • Legacy code depends on rewrite code, but rewrite doesn’t know anything about legacy.
  • All fragments run in the legacy activity if we run a combined application.
  • The rewrite project includes a ‘legacy-bridge’ module to provide the interface between both projects.
  • The legacy application module can not depend on the rewrite application module. That is why we have a separate module for Dagger purposes that is accessed by both legacy and rewrite application modules.

Architecture

In general, the architecture of the rewrite project can be expressed as MVVM + Databinding + RxJava2 + Dagger2 + Kotlin. It was created according to Google’s official recommendations for Android applications and based on usage of Android Architecture Components libraries such as NavGraph, ViewModels, LifeCycle, Room, LiveData, Databinding.

architecture concept for android modernization

Components

Components of the application were split into several module groups. Every group has its own prefix name in order to make the decomposition process easier.

  • Application development platform (ADP) contains generic modules required by android applications, with no dependency on specific features.

E.g., Network, Logger, Storage

  • Domain agnostic (DA) includes components that can be used in any app or can be a part of an app framework.

E.g., adp-da-network, adb-da-logger

  • Domain specific (DS) components can be used only in this exact app and represent dependencies for concrete features.

E.g., adp-ds-auth, adp-ds-network

  • Feature group contains the high-level components that actually deliver value. Theoretically, they can be seen as only applicable to the application’s domain. However, we have this conceptual separation in order to extract these components from ADP.

Application development platform

ADP consists of domain agnostic and domain-specific components which can be reused to stitch together the application.

Naming conventions

  • No ADP in symbols like classes, variables or methods.
  • Prefix adp-da or adp-ds for adp modules.
  • Package in adp.*

Domain agnostic

  • adp-da-core: android component supertypes.
  • adp-da-core-android: android component supertypes, app/activity lib initialization wrappers.
  • adp-da-analytics: analytics framework includes a facade, wrapper for vendor SDK’s and generic events.
  • adp-da-location: single shot location, geofences.
  • adp-da-network: retrofit, okhttp, parser; configurable framework for features to provide their interceptors.
  • adp-da-config: abstraction for retrieving configuration files. Implementation app-specific.
  • adp-da-ui: custom views, spannable utilities, other view utilities, image loader wrapper, other UI wrappers.

Domain specific

  • adp-ds-core: application’s domain models shared amongst features.
  • adp-ds-ui: application’s specific themes, styles, color values, and other resources.
Application development platform by Grid Dynamics Structure Example

Feature modules

Module splitting conventions

Each feature should be split into several gradle modules to promote a clearer separation of concerns and to determine what kind of code is grouped together.

  • feature-app: A small app to test these feature modules. Module type is com.android.application.
  • feature-ui:  A feature shall declare implementations (fragments) for navigation nodes and any other code related to presentation style like ViewModels. This implements the feature user interface and contains fragments, view models etc.
  • feature-ui-slices: A feature shall provide basic UI components that can be reusable in feature-ui. They are just layouts and view models and can be composed into larger UI components. This approach promotes better design by discouraging page VM to view slice coupling.
  • feature-nav: A feature shall declare its node IDs, deep links, argument interfaces for usage by other layers. It declares the android architecture navigation graph for a feature. Other features that should navigate to this feature depend on this module. Android navigation controller is used to perform navigation. No custom fragment manager transactions are allowed.  All feature nav graphs are added as sub graphs in the rewrite application module.
  • feature-domain: A feature shall provide granular models and repositories for retrieving/manipulating these models. It contains domain classes, repo interfaces and use case interfaces. In other words, it is the interaction point for other modules to communicate with the feature. Models are immutable data objects. They are related only to business logic and have no information about data or ui layers.
  • feature-data: A feature shall provide this layer to have a concrete implementation of domain interfaces like repositories and to interact with network/storage directly. It is an implementation of the domain and contains repo implementations, use case implementations, dagger modules. feature-data depends on its feature-domain module. No feature-data depends on any other feature-data module.
Feature Modules in Grid Dynamics Application development platform

Kill switches

Thanks to our experience working on large-scale projects, we realized that using the feature branches approach for very outdated code was a problem. After rethinking, we came to the conclusion that the best solution is trunk-based development combined with the ability to enable and disable part of the functionality in real time with Kill Switches (KS).

Essence of the approach

As mentioned, the main purpose of Kill Switches is to dynamically enable or disable certain application features on the same application version. This approach was implemented even for the legacy code. When we started rewriting, we decided to use KS in the new code as well so as to provide users with rewritten screens step by step, and have the ability to disable the feature at any time.

Despite the fact that each feature has KS, it is not a single condition for enabling features for users. Each feature also has a version that represents a minimal application version where this feature becomes available for users.

Having this ‘minimumVersionSupported’ property is a solution for the following possible issue:

  • Because we followed trunk-based development, there can be scenarios where some incomplete version of a feature is included in release. E.g. a feature was in development for version “X”, and was implemented, tested and included in the release of version “X.5”.
  • When the server starts to return TRUE for the corresponding KS, the feature will be available for all users if the app version hasn’t been checked.
  • The result is that users who haven’t updated the app to “X.5” may see the unfinished version of the feature.

Note: Besides Kill Switches for each feature, a global “isAppRewrite” Kill Switch was implemented. Setting this KS to FALSE disables all rewritten code.

Not only Heroku

Firebase Remote Config is a cloud service that lets you change the behavior and appearance of your app without requiring users to download an app update. Using Remote Config you can create in-app default values that control the behavior and appearance of your app. Then you can use the Firebase console or the Remote Config REST API to override in-app default values either for all app users or for a specific selection of them. Your app controls when updates are applied, and it is able to frequently check for updates and apply them with a negligible impact on performance.

Firebase Remote Config can be used as a Kill Switch to enable or disable some features depending on business needs and state. For example, if some feature is unstable, like Translation, it can be turned off remotely without requiring users to update the app.

The results

  • We introduced core ADP components: Networking, Cache, Analytics etc.
  • We reduced CI and local build time by 50%.
  • We switched 90% of APIs to protobuf which decreased API response time by 2 times.
  • We improved the Mobile Web Conversion Rate from 1.5% to 1.8%.
  • We successfully migrated the Android codebase to Kotlin and Clean/MVVM architecture with databinding.
  • We introduced a new approach to navigation with Android JetPack.
  • We divided the application into modules, which enabled greater independence between teams.
  • We rewrote all features almost from scratch which resolved many existing defects.
  • We safely removed legacy code.
  • We created brand new documentation that automates the onboarding process.
  • The resulting application is more flexible, scalable and maintainable.
Results of implementing ADP and migrating to Kotlin

Conclusion

Do you need to follow every step we described above? Not really. You know your project best, and it is up to you to decide on the best approach. The reason we decided to share our experience with you is because it is a success story. We did it! However, if you recognize similarities in your project,  feel free to use our solutions as a guide.

What do we have after implementing all of the above?  Our project is still evolving as we continue to add new features and implement marketing changes, but our client is enjoying the following benefits:

  • Adding features and implementing changes is not only easier, thanks to the new modular structure, but it is also more elegant, thanks to Kotlin.
  • Standardizing the architectural approaches used in the application reduces the downtime required to onboard a new team member.
  • Kill Switches helped the QA department to make the testing process clean. They also made feature releases smooth because there is no rush now and the feature is well-tested before becoming available to customers.

In this article, we omitted any concrete examples of implementation and focused on the approach we took instead. If you found this information valuable, stay tuned and we will share more details with you soon!

Need more information about Android Application Modernization? Get in touch with us to start a discussion.

Get in touch

We'd love to hear from you. Please provide us with your preferred contact method so we can be sure to reach you.

    Unlock innovation with Android application modernization

    Thank you for getting in touch with Grid Dynamics!

    Your inquiry will be directed to the appropriate team and we will get back to you as soon as possible.

    check

    Something went wrong...

    There are possible difficulties with connection or other issues.
    Please try again after some time.

    Retry