We do a lot of Android software development at Xtreme Labs. Every two months we have a retrospective meeting to look back and reflect on the exciting discoveries, petty frustrations, and disseminate our findings to the group. There are some lessons that we’ve learned time and time again.
1. Don’t Make Blocking Requests on the UI-thread
Have you ever seen an application hang and stop responding to your input? Did the animations stop sparkling? Did you see the dreaded Application Not Responding dialog box? These little emergencies can occur if you block your application’s UI-thread for too long.
Every application begins with one thread running a message loop. Everything that the user sees and all feedback they receive depends on the message loop. All of those Activity onResume, button OnClick, and all drawing events depend on that same message loop. If anything running on that thread takes too long, the user can experience a jarring episode of jankiness.
In particular, any operation that makes network requests or accesses the device’s filesystem can take precious time away from this message loop. If you make a network request and it takes more than a few seconds to return, then the user will see the Application Not Responding dialog box. A lot of users can’t tell the difference between these kinds of hangs and mundane crashes and will think your application is buggy (which, for all intents and purposes, it is).
This sin is so grievous that applications targetting the Honeycomb API, or greater, will experience NetworkOnMainThreadException if the application makes a network request using the UI-thread.
How do you guard against these misdeeds? Use AsyncTasks and ThreadPoolExecutors to toss your blocking calls onto worker threads. When your background tasks complete you can use callbacks or post messages to your UI-thread’s message loop to process the results.
You can also enable strict mode (available in Gingerbread) to provide visual feedback when your application performs a naughty operations on the UI-thread.
2. Loading Too Many Big Images
Handling large bitmap images on Android is hard. We still haven’t found the silver bullet that helps us load as many images as we want without running out of memory.
The main problem is that the amount of RAM available to individual processes in Android applications is disappointingly small. The maximum heap size keeps getting bigger and bigger with successive OS releases and fancier devices, but it’s hard to believe that we’ll ever have the luxury to load as many huge images as we could in desktop environments.
The math is simple. The resolution of the Galaxy Nexus screen is 1280×720 pixels. Images with a bit-depth of 32-bits-per-pixel will consume about 3.5 MB of memory once they are uncompressed into your application’s heap. It doesn’t matter what kind of compression the image uses.
Most applications seem to get heap spaces around 20 to 30 MB. You can see that loading only a handful of images into memory can cause your application to run dry of heap space very quickly. You can try using the android:largeHeap attribute in your application Manifest to request more heap space, but, at best, this attribute is just a hint and the operation system doesn’t need to respect it.
What can you do? First, make sure that you are not leaking references to your images when you’re done. You want to get that image off of your heap as soon as possible.
- Make sure you recycle your bitmaps when you’re done with them.
- Make sure to set the callback on your Drawable objects to null when you’re done with them.
- Don’t leak references to Activities or Contexts that could reference your images, or any views that could reference your images.
- Don’t load images that are bigger than the screen size. If you’re downloading images from a server, see if you can rescale the images on the server before you even have to download them.
- Don’t download images straight to memory. Use streams and write them directly to the filesystem. Rescale or resample them before loading them into RAM.
- Don’t build full screens using images. Be clever and change your screen to use combinations of smaller images and XML-drawables, if possible.
- Capture hprof heap dumps and use the Eclipse Memory Analyzer Tool to see what kind of objects are using most of your heap space. Make sure that you aren’t suffering any other memory leaks that could be exacerbating your memory crunch.
3. No Visual Feedback When Touching Buttons
This problem is simple to solve, but I’ve seen it done poorly so many times. Your application needs to give positive feedback when the user interacts with the application’s display. If you touch a button, it should be highlighted.
Android makes it easy to provide different graphical states for on-screen elements based on their current selection or pressed states. You need to assign a StateListDrawable to your custom screen elements. The easiest way to do this is to create a drawable XML file with a state selector (see the above link for an example).
Think carefully about where to apply these kinds of drawable. You probably won’t need it on regular Button objects (as long as you are using one of the built-in themes), but you will definitely need it on ImageButtons. You may or may not need to use it on individual rows in ListViews. Be careful.
4. Android Apps That Look Like iOS Apps
A lot of people wants us to port existing iOS apps to the Android platform and want to recycle the same design. That’s a horrible taboo to break. Android applications have their own look-and-feel that is distinct from iOS and other platforms. What makes sense on iOS doesn’t always make sense on Android. Android users are smart and will call out and give poor ratings to Android applications that look like iOS applications.
Google has written extensive Design Guidelines elaborating on how Android applications should look. Read it! Learn it! Some design rules are made to be broken and you can distinguish your application by bending the rules in shrewd ways — but you should learn the rules before you play ball.
Subscribe to the Android Niceties tumblr blog for inspiration.
A particularly frequent transgression is to put tabs at the bottom of the screen. The built-in Android tab controls should live at the top of the screen.
5. Poor Support For Multiple Device Formats
Android device fragmentation is real. There’s lots of versions of the operating system, lots of screen sizes, and lots of keyboard layouts in the ecosystem. Many applications do a poor job of supporting the vast diversity of devices in the world.
It doesn’t have to be so hard. Android gives developers many tools to combat this bewildering space. Here’s some things to remember:
- Use dp (density-independent pixels) or layout_weights to layout your UI. Density-independent pixels are scaled automatically by the layout system to be approximately the same size regardless of screen size and density. layout_weights are useful if you want to device the screen into regions that are proportionally the same regardless of the screen size (e.g.: when you want the left pane to be one-third of the screen-width on all devices). Note that layout_weights force the layout routines to repeatedly measure your Views on screen and can be slow.
- Use XML resources as much as possible to layout your screens. You can provide alternate layouts for different screen sizes which will be automatically used at run-time.
- If you need to provide pixel measurements programmatically during runtime, you can get the device to convert dp to px automatically by reading your dp-scaled dimensions out of a dimensions XML resource file.
- Be careful if you decide to lock the screen orientation to portrait-only. Many Android devices with slide-out keyboards will switch to landscape orientation when the keyboard is pulled out. If your application is locked to a portrait screen-orientation then you may infuriate your users.
- Some devices have trackballs or cursor keys. Make sure any touchable items have the “focusable” attribute set to enable keyboard navigation.