Friday, July 1, 2011

Simple Android Developer Mistakes

I would really like to write more tips, tricks, and tutorial posts relating to Android, but I’ve been extremely busy on apps both professionally and personally, and I am also doing technical review on an Android dev book due out later this year. So, while I don’t have time to go in the level of depth I’d like to, I thought I could work on a post with a bunch of short pieces that could be written when I have a few free minutes here and there and save the longer posts that require a greater chunk of continuous focus for later.

This post talks about some challenges that you may run into early in your foray into Android development. There isn’t really any sense of order to these; they’re more-or-less a brain dump of issues I’ve experienced or seen that take seconds to solve if you know what you’re doing or hours if you don’t. OMG! The emulator is slow! There are two things to note here: the default AVDs tend to have limited resources and the devices you’re targeting are likely running with a different chipset than the computer you’re developing on.

The first limitation can be easily overcome; the second is a much bigger issue, particularly with Android 3.x and attempting to develop for Honeycomb tablets without actually having a Xoom, Asus Eee Pad Transformer, etc.

Currently, it’s not very feasible to develop a 3.x app without a tablet. Back to that first limitation: When creating a new AVD, you can specify hardware settings, which can dramatically improve performance. The most important setting is the device RAM size; I tend to want the emulator to be as fast as possible, relying on on-device testing for a sense of “real-world speed,” so I usually give my phone emulators 512MB of RAM. That’s the equivalent of most good smartphones from 2010. The 1.x AVDs seem to run fine on 256MB, so you can potentially drop to that if your machine is limited (the Motorola Droid/Milestone had 256MB of RAM as well).

You can also try upping the cache partition size (128MB is a good start) and the max VM application heap size (48 is plenty) for a minor improvement. LinearLayout mistakes LinearLayout is a very useful and very simple ViewGroup. It essentially puts one View after another either horizontally or vertically, depending on the orientation you have set. Set an orientation! LinearLayout defaults to horizontal, but you should always manually specify an orientation. It’s very easy to throw two Views in there and not see one because the first View is set to match_parent for width.

That said, LinearLayout also processes layouts linearly, which is what you’d expect, but that means that setting a child View to match_parent can leave no room for subsequent Views. If you aren’t seeing one of your Views in a LinearLayout, verify you’ve set the orientation and that none of the Views are taking up all the available space. Within a LinearLayout, you should generally rely on layout_weight to “grow” the View.

Here’s an example: The heights are set to 0, but both child Views have weights. The first has a weight of 2 and the second has a weight of 3. Out of a total of 5, the first is taking 2 (40%) and the second gets 3 (60%), so their heights will actually use that portion of the parent View’s space (if that View has a 100px height, the first child will be 40px tall). The weights are relative, so you can set whatever values make sense for you.

In this example, the second layout could have had a set height and no weight and the first could have had a weight of 1, which would have made the first take up all space that the second didn’t use. Stupid Android! It keeps restarting my Activity! The default behavior when a configuration change happens (e.g., orientation change, sliding out the keyboard, etc.) is to finish the current Activity and restart it with the new configuration.

If you’re not getting any dynamic data, such as loading content from the web, this means you probably don’t have to do any work to support configuration changes; however, if you are loading other data, you need to pass it between configuration changes. Fortunately, there’s already a great post on Faster screen orientation changes. Read it and remember that you should not pass anything via the config change methods that is tied to the Activity or Context. Density Density should only be considered in terms of density not size. In other words, don’t decide that MDPI means 320px wide. It doesn’t.

It means that the device has around 160dpi like the G1 (320px wide) and the Xoom (1280px wide). Fortunately, you can combine multiple qualifiers for your folders to give precise control, so you can have a drawable-large-land-hdpi/ containing only drawables that are used on large HDPI devices that are in landscape orientation. One other important thing to note: Support for different screen sizes and densities came in Android 1.6. Android 1.5 devices are all MDPI devices, but they don’t know to look in drawable-mdpi/ because there were no different densities then.

So, if you’re supporting Android 1.5, your MDPI drawables should go in drawables/. If your minSdkVersion is 1.6 or above, put the MDPI drawables in drawable-mdpi/. What the heck is ListView doing? A full explanation of ListView would take quite a while, but here’s a quick summary: A ListView’s associated Adapter’s getView method is called for each child View it needs to construct to fill the ListView.

If you scroll down, the top View that leaves the screen is not garbage collected but is instead passed back to the getView method as the “convertView” (second param). Use this View! If you don’t, not only does it need to be garbage collected, you have to inflate new Views. You should also use the ViewHolder paradigm where you use a simple class to store View references to avoid constant findViewById() calls.

Here’s an example: @Override public View getView(int position, View convertView, ViewGroup parent) { final ViewHolder holder;

if (convertView == null) {
convertView = mInflater.inflate(R.layout.some_layout, null);
holder = new ViewHolder();
holder.thumbnail = (ImageView) convertView.findViewById(R.id.thumbnail);
holder.title = (TextView) convertView.findViewById(R.id.title);
convertView.setTag(holder);
}
else {
holder = (ViewHolder) convertView.getTag();
}
final MyObject obj = (MyObject) getItem(position);
holder.thumbnail.setImageDrawable(obj.getThumbnail());
holder.title.setText(Html.fromHtml(obj.getTitle()));
return convertView;
} /** * Static ViewHolder class for retaining View references*/

private static class ViewHolder {
ImageView thumbnail; TextView title;
}

This method is first checking if the convertView exists (when the ListView is first laid out, convertView will not exist). If it doesn’t exist, a new layout is inflated with the LayoutInflater. A ViewHolder object is instantiated and the View references are set. Then, the holder is assigned to the View with the setTag method (which essentially allows you to associate an arbitrary object with a View).

If the convertView already exists, all that has already been done, so you just have to call getTag() to pull that ViewHolder out. The “bulk” of the method is just grabbing MyObject (whatever object your Adapter is connecting to) and setting the thumbnail ImageView and the title TextView.

You could also make the ViewHolder fields final and set them in the constructor, but I tried to keep this example as simple as possible. This method no longer requires View inflation while scrolling nor does it require looking through the Views for specific IDs each time. This little bit of work can make a huge different on scrolling smoothness. One last thing about ListViews… never set the height of a ListView to wrap_content.

If you have all your data locally available, it might not seem so bad, but it becomes particularly troublesome when you don’t. Consider the above example but instead of the setImageDrawable call, it triggers an image download (though it’s better to have the Adapter handle that).

If you use wrap_content on your ListView, this is what happens: The first getView call is done, convertView is null, and position 0 is loaded. Now, position 1 is loaded, but it is passed the View you just generated for position 0 as its convertView. Then position 2 is loaded with that same View, and so on. This is done to lay out the ListView since it has to figure out how tall it should be and you didn’t explicitly tell it.

Once it has run through all of those positions, that View is passed back to position 0 for yet another getView call, and then position 1 and on are loaded with getView and no convertView. You’re going to end up seeing getView called two or three times as often as you would have expected.

Not only does that suck for performance, but you can get some really confusing issues (e.g., if you had been passing a reference to an ImageView with an image download request, that same ImageView could be tied to all of the download requests, causing it to flicker through the images as they return).

No comments:

Post a Comment