Modernizing Android Apps: Real-World Best Practices

 

Tom Jones

I'm a senior engineer and consultant who loves to bring a client's vision to life. My primary focus is on mobile platforms. From my time game developer I've retained a passion for graphical and real-time systems including AR/VR and projects with a big 'experiential' component to them. I also helps teach teams about the value of automated tests, CI/CD, and lean processes.

Updated Nov 9, 2021

Since its first commercial release in September 2008, the Android mobile operating system has seen many version updates. Most recently, Android 12 (codenamed Snow Cone) was released October 4, 2021, with handset manufacturers rolling out updates over the coming months. Android 12 brings dozens of new features and updates to use in your apps. However, as any app developer knows, with each new version, all existing apps supporting the previous versions must be tested and updated.  

The good news is that not all legacy applications need to be totally refactored each time a new Android version comes out. Depending on the app, it could take minimal effort to make sure it still works properly and will continue providing your users the experience they expect. But each operating system update also comes with opportunities to improve and modernize your app in ways that add value and enhance the user experience.

At GenUI, we’ve been down the app-updating road many times and are here to offer you the benefit of our experience. It all starts with looking closely at what you have and then figuring out how to update it efficiently and without breaking anything or overinvesting. Here we outline important steps and best practices as you prepare your existing applications for the next version of Android.

Review documentation and upgrade guides

When upgrading an app to a newer platform, one of the first steps is to read the associated documentation and changelog describing what changes were made and what updates to an app need to be done to make it work on the newer version of Android.

File IO, permissions, background tasks and services, and other platform-specific application programming interfaces (APIs) may change between versions. It’s always a good idea to keep in mind the current functionality of the app and how you may need to update certain parts of it based on these changes.

Conveniently, Android releases source code for upcoming new versions to the Android Open Source Platform (AOSP) well in advance of commercial release, allowing developers plenty of time to modify their apps as needed. With this release also comes guides rich with details about what to expect and what to look out for, such as its list of behavior changes for apps targeting Android 12 and behavior changes for all apps.

Android’s developer website has a whole host of additional materials published about the upcoming Android 12 platform, including a high-level overview of the app migration process. They break this migration process down into the following two phases: 

  • Ensuring app compatibility: This primarily involves reviewing the changes to identify where your app may encounter issues, testing your existing app on a device or emulator with Android 12 installed, and then making those code changes needed to resolve any issues. Changing the app’s target software development kit (SDK) version or using new APIs is usually unnecessary.
  • Targeting new platform features and APIs: In this phase, you can leverage the latest Android version’s features to improve your app’s performance and user experience. However, it requires adding full support for the new Android version by updating the existing target SDK version and is a more involved process than the first phase. 

In other words, the effort to upgrade to a new target SDK will depend on the current target SDK version and whether the goal is simply compatibility or the targeting of new platform features.

Catch regressions with testing

When upgrading a legacy system, being mindful of the current functionality helps make sure improvements or refactors won’t cause regressions. Ideally, this means performing ample unit tests and lightweight integration tests. 

However, many existing legacy systems lack testing to begin with, so a good first step is to define the critical parts of the app that need tests added before you make any changes. Then, with those tests in place, you can rest a little easier knowing that regressions will be caught when you start refactoring and modernizing. 

Finally, a good testing strategy should be explicit about its manual test plan, including which devices to test on, which core app features may change due to the operating system upgrade or new target SDK version, and edge-cases to test like lifecycle handling, device orientation changes, low memory scenarios, lowest supported version, old device testing, and so on. Those edge cases can be challenging to write automated tests on.

Practice incremental refactoring

If each time you upgrade your app you simply tack on whatever additional code is needed to maintain compatibility, over time you can end up with a tangled mess. Because of this, once your app is supporting the newest version of Android, it’s a good idea to consider longer term improvements.

Sometimes the natural impulse is to start from scratch when an app needs updating, and sometimes that's a good thing, but more often, the app is providing value, and you want to make improvements while the app continues to provide value. Therefore, the best method is to refactor incrementally. The following steps can help you implement this approach:

  • Split up large classes into smaller pieces without changing anything functionally (just structurally). 
  • Add tests where appropriate and continue to incrementally pare down large monoliths in the code where applicable before applying behavioral changes and bringing in new frameworks. 
  • Eventually, you can add new abstraction layers between those smaller pieces to introduce new frameworks, such as how navigation, network requests, or image loading works, for example.

These steps allow you to make multiple small changes to an app instead of one big change. This approach limits the effect of any problems that arise due to changes and will enable you to build up to larger overall application improvements in a way that minimizes potential problems. 

It’s worth noting that the most recent Android versions facilitate this approach and allow for easier testing by using compatibility toggles, which make it possible to focus testing on specific changes or targeted behaviors one at a time. 

Keep future developers in mind

When modernizing an app, it is easy to prescribe a strategy and framework that works for you or your team, but you should be mindful of who may inherit the code afterward. In other words, avoid prescribing an opinionated architecture that may be difficult for an average Android developer to be aware of.

For example, the RXJava mechanism for asynchronous, reactive data is great but extremely complex, and an average developer would have a hard time debugging code with RX added to it. 

You will better serve future developers and their ability to update the application if you employ refactors and modernization approaches to architecture that are as standardized and accessible as possible. The Android developer website is a great source for information about recommended architectures and common architectural principles including separation of concerns, driving your UI from a model, and more.

Establish best practices

Establishing best practices sets the stage for an application that is more easily maintainable and updatable in the future. The following list serves as a starting point for what to consider in setting your own application development ground rules: 

  • Add Kotlin programming language to your apps: This will make it easier to share code between mobile platforms and use all of Android Studio’s existing tools and architecture components that are part of the standard framework. It also makes it easier to implement a model-view-viewmodel (MVVM)—which Google has been pushing toward—or other explicit architecture. 
  • Employ the single responsibility principle: Keep classes small and simple, ideally with a single purpose. When each class or module has only one purpose or reason to change, then there is less likelihood that changing something about that module will lead to unintended consequences.
  • Implement the dependency inversion principle: Provide dependencies via an abstraction rather than have classes internally instantiate internal components, where appropriate. Android now has multiple dependency injection frameworks from Dagger to Hilt.

As a final note, one of the most essential things when updating an application for a newer version of Android is to know when to ask for help. GenUI has a team of Android developers with deep expertise and a proven track record for delivering results. We'd love to talk to you about how to modernize your Android apps to take advantage of the powerful new features and capabilities. Get in touch today.