This is a long post. It covers several decisions like API version, distribution beyond play store, UI & network performance, and minimizing RAM, disk, and battery usage.
- Minimum Android API version
- check Android version distribution dashboard about the userbase on every Android version and decide a min SDK version
- As of July 2018, I would recommend aiming for API 19 as the min SDK version for maximizing the app’s target user base. Do note that some libraries mentioned here don’t support API 19, and you might have to make a choice. Ultimately, its a tradeoff between more engineering effort and your target user base.
- App Size
- Reduce apk size; every 6MB increase translates to a 1% lower install conversion rate globally and 2% in the emerging markets. I would say 20-25MB app size should be the limit while targeting emerging markets.
- Proguard and Redex remove unused code. Proguard suffices in most of the cases.
- Use zopfli and Guetzli to perform lossless image compression on PNG and JPEG, respectively. For larger images, try lossy compression and measure perceived difference using butteraugli.
- Remove unused resources for more compression.
- Eliminate sparse translations from resources.arsc using arscblamer.
- Disk usage
- Learn about different storage spaces on Android
- Use the internal storage’s cache directory for storing non-critical files like downloaded images. The system can delete files from the cache when the disk storage is low, and this is better than the user manually clearing all the internal storage of the app.
- Use “shared external storage” for storing non-private files so that the storage does not count against the app’s disk usage and on devices where the external storage is on SD card, and this prevents the internal storage from becoming full. Such devices with removable SD cards are more common in emerging markets than in the developed world.
- Network usage
- Not all network bytes are created equal. Data usage on Wi-Fi is cheaper than cellular, which is cheaper than roaming. Scale-up experience if the user is on unmetered Wi-Fi and scales it down if they are on cellular and roaming. Auto-download full-resolution media on Wi-Fi. Download a low-resolution thumbnail when the user is on cellular and provide a click-to-download button to download the full media.
- For images downloaded via the network, consider having a server-side ability to configure height and width via the URL parameters. Don’t download images larger than the size you are going to display. The same goes for video; downloading a 4K video is adds little value when the user’s phone is limited to a much smaller resolution.
- For media uploads via the network, consider having some checks before uploading. There is no pointing uploading a media file bigger than the server is willing to accept, similarly, if the server is going to downgrade/downsample/shorten the media file, try doing that on the client-side to save user’s bandwidth and your costs.
- All media downloads/uploads should be resumable. This basically means that if the user is disconnected after downloading 20% of a media file, they should download the rest of the 80% and not the full 100% by specifying the Content-Range header. For uploads, the app has first to ask the server how many bytes have been uploaded before restarting the upload. This requires server-side support but will cut down unnecessary network traffic.
- Ensure that IPv4 support exists since IPv6 support is broken even on Lollipop 5.0. Ensure that the CNAME records of your domain have a good enough TTL value to minimize repeated DNS queries. Test how your app behaves with a broken DNS by intentionally setting the DNS to a non-DNS IP using DNS Changer.
- Use repository pattern to cache on the disk using DiskLruCache. This pattern, not only, reduces the network usage but also makes your app offline-friendly.
- Use Facebook ATC to simulate a bad network and fix the app performance under bad network
- Use AT&T’s Application Resource Optimizer to catch duplicate resource requests and similar content optimizations
- Supporting TLS 1.2 below Android 4.4 requires some effort
- RAM usage
- If you use bitmaps, consider using Fresco to minimize Java memory usage on the older versions. Otherwise, use Glide or Picasso. Unless you are building a photo editing app, don’t do manual bitmap management.
- Avoid GC thrashing by using Object Pools
- Use sparse arrays in lieu of HashMap to minimize object creation due to auto-boxing, which creates an Integer for every int and a Boolean for every bool.
- Avoid enums and use IntDef/StringDef instead
- Avoid leaks by adding LeakCanary to the debug build. Don’t ship this in the release build. The Android architecture, especially activities and fragments, makes it really easy to create leaks.
- Test for activities not being preserved when they are not active by “adbe dont-keep-activities on”. Many low-RAM devices are more aggressive about killing backgrounded activities, and turning this behavior on will recreate that experience.
- Power usage
- To prevent unnecessary power usage, avoid WakeLocks.
- Use FLAG_KEEP_SCREEN_ON for keeping the screen on in an activity
- Use either WorkManager or android-job for long-running background jobs, which should persist beyond app restart. Android-job is more stable, but WorkManager is Google’s official library, its still an alpha release, though.
- Use AlarmManager for short interval callbacks
- If the battery is low and the phone is not charging, or the phone is on battery saver mode, avoid heavy tasks like automatic media downloads or video rendering. Test for app’s behavior when battery saver is on using “adbe battery-saver on”.
- Test for app’s behavior under doze mode using “adbe doze on”
- Test for battery usage using battery historian
- Logging & debugging
- ANR
- Use StrictMode to catch and prevent potential causes of ANRs
- Avoid Inter-process calls (IPC) on the main thread, StrictMode won’t catch the violation, and you would never know what’s happening on the other side of the IPC call which can block the main thread.
- Avoid synchronized method calls on the main thread since a background thread might be holding the lock, leading to the main thread being blocked.
- If BroadcastReceiver#onReceive is going to take more than 10 seconds, then call goAsync for background processing and call PendingResult#onFinish() once that’s finished
- Crashes
- Test for app background restrictions using “adbe restrict-background true ”
- Test for mobile data background restriction by disabling background data access for apps via “adbe mobile-data saver on”
- UI Performance
- Text measurement takes a lot of time on the UI thread, consider creating PrecomputedText on the background thread. PrecomputedText speeds up UI rendering on API 21 onwards.
- “android:autoLink = true” in TextView’s XML works via Regular Expressions and has bad performance. Consider using Linkify on the background thread.
- Learn to use TraceView to debug and improve UI performance manually
- Minimize overdraw in the first and the most used activities/fragments of the app. Turn on overdraw via developer options or use “adbe overdraw on”.
- UI text
- If you are working with text, always use wrap_content as the height or else, the text will be chopped off if the font is large.
- Provide better multi-lingual support as well as support for multiple languages showing up in the same UI. Multiple languages will show up in the UI with the user-generated content. Consider providing an in-app locale change option, preferably at the time of registration/login, so that users can use your app in a language different from the phone’s default.
- Hebrew and Arabic, which cover most of the middle-eastern countries, are RTL. Test how UI appears in the RTL languages using “adbe rtl on”
- Use EmojiCompat and don’t rely on system’s support for Emojis. The Android platform support for Emojis is extremely fragmented.
- Avoid SpannableStringBuilder, StringBuilder + SpannableString is 25x faster.
- UI Quality
Avoid non-deterministic progress indictors - non-deterministic UI indicators, especially, for the network operations, are bad. First, they don’t show progress, and second, sometimes, the developers fail to implement the error case, and the non-deterministic progress bar goes on forever. A deterministic indicator tells the user if any progress is happening or not. An alternative approach would be to show a non-deterministic indicator first and then after a timeout, if the network-based job is still pending, switch to a deterministic indicator.
Any network operation including idempotent operations like search or state-modifying operations like uploads/downloads etc. should be cancelable, or else the user will be forced to kill the app to prevent a big upload/download from using all of their cellular data.
Monitor for network connectivity changes to
- automatically retry operations which got canceled earlier due to the loss of network connectivity
- perform background prefetching or increase media quality to enhance user experience if the user is now on an unmetered Wi-Fi
- stop aggressive prefetching or decrease media quality to prevent data drain if the user is on cellular connection or even more aggressively if the user is on roaming.
Ensure that in the offline mode, actions on the local data like search still works.
Follow Google’s guidelines on asking for runtime permissions at the time of use and with a proper rationale prompt first.
- Programming languages & libraries
- Try to avoid languages other than Java and Kotlin - Native code like C/C++ produces architectural problems like libhoudini. High-level but generic languages like Javascript fail to make the best use of the platform. I would recommend that, if you can, stay with Java/Kotlin. Sometimes, due to business reasons to share cross-platform code or technical reasons (e.g., using webrtc), it is unavoidable to fully stay with Java/Kotlin.
- Always use support library components instead of the platform provided ones - Many things like Fragments, AutoCompleteTextView, etc. are duplicated in the platform as well as the support library. Default to the support library ones since they are more stable and have more bug fixes and performance improvements than the platform components whose updates are tied to the Android OS update.
- Always provide a fallback for Google Play Services features like Google Login, Sms Retriever, or Fused Location Provider to deal with missing/obsolete Google Play Services. Google Play Services app is different from the Google Play app. Google Play Services is a relatively pretty bloated and bulky app taking upwards of 500MB disk space, and therefore, many users uninstall its latest version from time to time to free up space.
- Distribution
- Google Play is not the only distribution channel in the emerging markets. Upload split apks to Google Play but also provide a fat apk as on your website for the direct download. In many parts of the world, users prefer sharing apks via ShareIt, or they simply never sign into their Google account. Rather than letting these users go to random websites, provide a direct download option on your website.
- App updates Apps won’t update automatically if the user is not signed in to Google Play or has disabled automatic updates. Therefore, prompt them to update the app after a certain time interval.
- Login -Many users might not have email, Google, or Facebook account. Therefore, provide phone number based registration and login as an option. Also, the only safe way to verify whether the user owns a number or not is to have them confirm that they can receive a code via SMS or phone call on that number. Similarly, if the user is logging in using their phone number, use their contact book as a source of their connections.
- Miscellaneous
- Use AtomicFile to read/write all files, or at least all critical files. Your app can be killed anytime by the system, and that can lead to corrupt files.
Note: I have referenced the adbe command, which is part of the adb-enhanced tool.