Ask any iOS developer what the best UIKit component is and odds are it'll be UITableView. It's the backbone of nearly every non-game App out there, in one way or another. It's an efficient, highly customizable element that is a natural fit for the form factor, as opposed to the sprawling, one-dimensional interfaces we see on Desktops. In fact, whenever I start developing for a new mobile OS, I almost always look at how tables are handled because it will probably be the 75% of my app. So, how does lovely Android stack up?
The droid you're looking for is ListActivity. If you haven't picked up on it by now, we're going to make OfflineListActivity from Part 3 such an Activity. Go ahead and import android.app.ListActivity in OfflineListActivity.java and change OfflineListActivity so it extends ListActivity, not Activity. Should now looking something like:
EDIT: Shoutout to Cyril Mottier, who corrected me on this bit: "When using a ListActivity, Android will automatically creates a ListView that fills the screen."
We also need to edit our main.xml, replacing our beloved TextView with a ListView:
Should be pretty easy to see what's going on here. Note that we haven't told Android anything about how the list rows themselves should look, only that we have one that fills up the parent layout. Now that we've updated our layout, let's get back to fun land. Pay special attention to this next part!
Unlike UITableViews, we don't need to add any new methods to implement list indexing. The Android SDK actually provides a mechanism by which this happens automagically: ArrayAdapters. These are sort of like NSArrayControllers, if you're familiar with OS X development. Basically, you assign a ListActivity an ArrayAdapter and it'll use the contents of an ArrayList in that ArrayAdapter to populate the ListView automagically. Wow, that's a lot of camel case.
Import android.widget.ArrayAdapter, java.util.ArrayList, and (optionally) java.util.Arrays. Depending on your memory of Java, you can create an ArrayList however you want, but I'm going to create it with a String array. At the least, create an ArrayList as an instance variable of our OfflineListActivity. Here's my use:
Watch as we only have to add *one line* to get our ListView to kick in:
In this use, the ArrayAdapter takes a Context (a parent object, essentially), resource, and List as arguments. The resource is android.R.layout.simple_list_item_1, a default view that works well for single-line list items. We use *android*.R in order to differentiate between OUR R.java and the default R.java. Savvy?
Hit the build and install button to see our creation come to life. Depending on your screen resolution, you might be able to scroll, all handled courtesy of ListView. You can also play with the emulator's arrow keys and see that your Activity automagically has support for non-touch devices. Kind of neat, actually.
Let's pretty this up a bit, shall we? Specifically, let's try to get some iOS look-and-feel in there. Why? Because, as nice as grey and orange are, we want *some* type of continuity between our iOS and Android apps. There's a delicate balance between respecting an operating system's native UI guidelines and keeping a consistent cross-platform experience, so always count to 10 before doing something rash. We'll come back to this topic later.
One cool feature about Android that iOS lacks is the ability to apply an app-wide "theme", which enables you to reformat the look of every View element in one fell swoop; so if you want all your fonts to be in Marker Felt, you should use a theme instead of doing a custom XML file for each view in your app. Thankfully, there's a default theme we can use to get some of that iOS mojo.
Look in your project folder and find AndroidManifest.XML. This is very much like the Info.plist in an iOS app. You should see something like:
A lot of this is pretty self-explanatory: package name, version code, version name, etc. Note the use of @drawable and @string to refer to special resources. We've already encountered @string, but what's @drawable? Check out your /res folder again. You will either see one folder (/drawable) or three folders (/drawable-hdpi, /drawable-ldpi, /drawable-mdpi), depending on how you created your project. The three folder configuration allows you to easily include multiple resolutions of the same resources for different pixel densities, much like the @2x feature on iOS.
What there isn't an equivalent to on iOS is the <intent-filter> section. On Android, there are more types of apps than just those that are launched by the tap of an icon; in fact, you can make apps that don't have a full screen activity at all. Accordingly, you use the <intent-filter> to specify how your app can be launched and used.
Anyway, back to making it pretty. Add android:theme="@android:style/Theme.Light" as a property of your application:
Run the app and you'll see we now have more pleasant list:
You should also realize that handset manufacturers can ship their own default themes and UI features in place of normal Android ones; for example, the Galaxy S devices often use blue UI elements and rubber-band scrolling. So while you might not be able to depend on getting the *exact* same look on every device with this approach, you can expect it to look the same as the other apps on the device.
We're still missing something from the iOS UI that's actual more than just eye candy: the disclosure arrow. Although a simple graphic, it's a valuable piece of UX information, as it lets the user know that the row can be tapped to go deeper in a navigation hierarchy. Unfortunately, there's no default Android style that has this, so I whipped up an icon in Photoshop that'll do the job (found in the
source). You're probably better at simple icons then I am, so you might want to use something else for production. Just sayin.
Anyway, we want our list row to have a label aligned to the left and the arrow image aligned to the right, both centered vertically. In an iOS app, this would mean customizing a UITableViewCell, either in code or in Interface Builder. On Android, this means using our own subclass of ArrayAdapter in our ListActivity which knows how to use an XML layout to construct the row view. Make sense? If not, let's just Nike it and then we'll talk.
First, we want to actually write our row XML file. In /res/layout, create list_item_with_disclosure.xml (or whatever you want to call it).
A CHALLENGER APPEARS: this time, we're going to use a RelativeLayout the parent of our layout. RelativeLayouts are pretty neat, actually. Recall that LinearLayouts just display one thing after another in the order listed in the XML file; RelativeLayouts, on the other hand, draw their children in terms of defined relationships. So if you want to write a caption for an image, you can position the caption below the image, irregardless of where the image is drawn. Make sense? Relative to our situation (heh, get it?), this also allows us to position elements relative to their parent elements (such as aligning them to the left and right, as we wish to do with our label and image).
For the body of our layout, we want to use a TextView and an ImageView.
The first unusual thing you may notice is android:gravity. This is a property which defines a sort-of automatic padding of a UI element (in that it tells Android how to align the contents *inside* of a View, not outside). In this case, we want to align the contents of the TextView vertically centered. In both elements, you will also see the use of android:layout_alignParent, which is a property exclusive to RelativeLayouts that makes our lives easier. This especially helps in creating interfaces that are resolution and orientation independent, as is often the case on Android phones.
The other interesting thing going on here is android:id. We can assign any layout element an id, which is then built into R.java so we can access it from our code, much like how an IBOutlet lets us reference an Interface Builder element. However, it needs to be prefixed with the special "@+id/" to work.
Let's get back to talking code. As I mentioned earlier, we need to subclass ArrayAdapter to get this to work. Create a file called BrocabAdapter.java and have it extend ArrayAdapter<String>. There are quite a few imports here so I'll just copypaste below. We also need to implement a constructor, but it's pretty painless:
The function we need to override is getView. This is sort of like tableView:cellForRowAtIndexPath:, as it enables us to reuse already created views in our list to save resources.
Pretty simple stuff for the most part. We check to see if convertView (the view used for the row) already exists; if it doesn't, we create it using a LayoutInflater. This is a pretty handy object that is utilized quite often, as it lets you jump back and forth between View objects and XML files. (FYI: this.getContext() returns the context in which this ArrayAdapter exists, thus our parent ListActivity). As noted earlier, we use R.java to access the interface elements declared in the XML files: R.id.disclosure and R.id.term.
To glue it all together, simply change ArrayAdapter<String> to BrocabAdapter in OfflineListActivity.java:
Take a gander at our even prettier list:
I'm about to Steve Jobs it up because there's just ONE MORE THING! We're going to do something when a row is tapped. Ultimately, we want to push a new view to the stack, but let's start small.
On Android, every view has an "onClickListener" property. When the view is clicked (tapped), the listener's onClick function is called. To K.I.S.S., we're just going to have it display an AlertDialog, which is the equivalent of a UIAlertView.
BAM, we turned our ListActivity into an onClickListener for the entire row. We also tagged the row with its index, just "in case" we need to refer to it "later". We could have just made the Adapter itself an onClick listener, but it'll make things easier down the road if we do it this way. Also, it's a better separation of model and controller (...right?).
Import android.view.View.OnClickListener and android.app.AlertDialog. We need to implement the OnClickListener interface (like conforming to an Objective-C Protocol, if you're a bit foggy on Java) and provide an implementation for onClick:
All of it is pretty simple and uses familiar vocabulary (brocabulary?) to its iOS counterpart. One note: setButton is pretty verbose, but this is the non-deprecated use.
Here it is in action:
We covered a lot of ground this time:
- ListActivities == UITableViewControllers
- ArrayAdapters == NSArrayControllers; magically fill in ListViews.
- Change themes and ALL KINDS OF AWESOME THINGS in AndroidManifest.XML.
- Subclass ArrayAdapter to use a custom row XML file
- Convert XML files to Views with LayoutInflaters
- Use onClickListeners to track row events.
Click to continue to Part 5: Files and Objects; or, I prefer JSON Bateman's early work