Having developed Android apps for startups as well as Fortune 500 companies for many years, we at ElevateX strongly believe that every successful Android app should have the following high-level design goals:
- Pleasant user experience with a modern, fluent, and responsive UI
- Offline support for spotty networks in e.g. subways
- Maintainable, clean code base with high test coverage
- Ability to A/B test the UI for rapid prototyping
- Ability to release the app continuously to different user groups
- Push-Notification- and App-Deep-Link-support to re-engage users
We rarely get the chance to start an app from scratch but don’t let this be an excuse to stick with outdated libraries, frameworks, practices, and untested code. Instead, consider applying our recommendations to your current project when e.g. implementing new features or new sections of your app. Thereby, you can keep your legacy code unchanged while slowly but surely improving your overall app quality.
In the following, we outline the Android project set up we aim for today – in February 2019.
We start out with (1) some Android project basics, followed by (2) infrastructure recommendations that go beyond just writing maintainable source code. Then, (3) we outline some app architecture decisions that worked well for us in past projects and enabled (4) automated testing. To achieve all this more easily there are (5) some handy libraries and tools. We finish with (6) some additional tips.
1. Basic Android project setup
- Use the latest Gradle build tool and its dependency management
- Setup build flavors for e.g. development and production environments
- Use ProGuard for release versions
- The minimum API level is likely defined by the customer but should be no smaller than Android 5 (Level 21)
- Use the Android Support Library AndroidX to bridge the gaps between API levels – make sure to adapt to AndroidX and not use the deprecated, version-specific libraries.
- Prefer Kotlin over Java
- Use Lint tools to flag potential programming errors, bugs, code style, etc.
- Use Git version control with master, develop, and feature branches. Tag releases and enforce merge between branches only via a pull request (PR)
- Use Continuous Integration (CI) – e.g. set up Jenkins to build and run (unit) tests on every commit. At least the nightly build must run integration tests, too.
- Optionally, SonarQube could be set up to find code smells and enforce test coverage in the form of a quality gate for every release.
- Require PR review by at least one other developer and allow to merge branches only if the CI build of that branch is passing and marked “green”
- Setup AppCenter (formerly HockeyApp) to be able to release app versions and flavors to custom distribution groups (e.g. developers, testers, clients, beta users, …)
- AppCenter also comes with crash reports and analytics, which will help with fixing bugs reported by users
3. Android Architecture
- Use the Model-View-ViewModel (MVVM) Architecture Pattern as it nicely separates concerns and enables unit testing of the business logic of the app
- The Context or other Android resources are never to be used in models nor in view models to enforce their 100% unit testability
- Introduce “Services” that provide the functionality to ViewModels like receiving and persisting data etc.
- Introduce Navigator(s) to centralize navigation between views using intents (don’t pass massive data as Parcelable but rather use ids to locally stored information)
- Alternatively, the Model-View-Intent (MVI) pattern could be used as it nicely enforces immutable data/objects with a uni-directional data flow by design
- “Package by feature” enforces separation of concerns and enables potential use of Android Instant App
- Introduce a shared module for data models and common functionalities (e.g. date parsing)
- Respect the Android life cycle at all times and use the lifecycle-aware components of Android Jetpack.
- Create custom views to encapsulate common views – e.g. a customer-specific date picker
- Use dependency injection and generate the dependency graph automatically
- This enables better testability as mock objects can be used
- Use RxJava
- As Android Apps are highly driven by asynchronous events and an app can be interrupted by the system at any time, RxJava has proven to be a key asset (more information below)
- Use Androids’ shared preferences, file storage, and SQLite database to persist data locally
- Use Android resource files to define themes, dimensions, images, strings, etc. and use respective subfolders for localization purposes and build flavor differences
- Use Junit as much a possible – it is fast and unit-testable code is a good indicator of good separation of concerns within the app
- Use Espresso for UI and integration testing
- Add at least one test that checks if the app main screen is shown after app launch
- Monkey runner can be used to stress-test the app
5. Android Libraries / External tools
- For dependency injection we recommend Koin for pure Kotlin projects, Dagger2 alternatively
- Define at least a global app scope and an activity-specific scope – add more specific scopes if needed
- Room – Android Jetpack’s persistence library – is the preferred way to harness the full power of SQLite
- The Gson library is suited perfectly for JSON conversion.
- Retrofit to perform REST (backend) calls – it uses Okhttp under the hood
- Set up Okhttp to cache backend responses and downloaded images
- Glide image library
- Use cache and image transformations when displaying images for UI performance reasons and do not waste the user’s mobile data
- RxJava 2
- Data flow can be modeled very nicely and asynchronous by design – events/data can be chained, filtered, composed, etc., threading can be explicitly defined, and observers can subscribe to events of interest
- Introduce some RxBinderUtil that keeps track of subscriptions and is life cycle aware so it can dismiss active subscriptions in case of respective Android life cycle events
- We advise to not use data binding as the benefits don’t justify increased build time and other drawbacks. With the introduction of Kotlin views can access UI elements by their id without using findViewById.
- UI performance
- Use RecyclerView for any sort of lists
- Avoid deep nesting in layout files and use the Constraint Layout instead
- It is already part of AppCenter but it might be useful to introduce e.g. Google Analytics for remarketing via Google Ad Words. Please keep GDPR restrictions in mind. The user needs to explicitly consent to tracking of this kind. This also applies to the usage of Facebook SDK integrations for re-marketing or Single sign-on reasons.
- For push notifications, we recommend Firebase messaging (also available for iOS)
- Make use of Retrofit’s authentication interceptors to handle e.g. OAuth authentication
- For a pleasant user-experience, allow the user to use Single sign-on via Google, Facebook, etc.
- Permission management
- Since the user explicitly needs to grant permissions to the app to e.g. access his location, the core use cases should not require any of these platform features – it should be a nice-to-have for improved user experience
Aiming for this kind of project setup helped us deliver outstanding apps for our clients and we are more than happy to share our learnings with you. What’s the biggest take-away for you? Please provide feedback and drop us an email at firstname.lastname@example.org.
To learn more about Android development, check out our blog post about resources every Android developer should follow.