clayallsopp's posterous http://clayallsopp.posterous.com Timid Fireball posterous.com Mon, 07 Mar 2011 13:51:38 -0800 Beginning Android for iOS Developers; or, How to Build a Real-World Android App http://clayallsopp.posterous.com/building-an-android-app-from-scratch-or-this http://clayallsopp.posterous.com/building-an-android-app-from-scratch-or-this

Comparison

That's my baby, Brocabulary. She was my first, way back in 2009. Yeah, I know, she's a little silly, but she's not that complicated:
  • Display a list of items loaded from the filesystem
  • Display a list of downloaded items
  • Display a list of favorited items
  • Show a detail display when an item is tapped
  • Upload a new item

Pretty much the bread and butter of CRUD apps. And guess what? I'm going to show you how to make it.

Table of Contents

Part 1: Overview

Part 2: Tools; or, get up on my API Level

Part 3: Activities; or, getting your Vibrams Five Fingers wet

Part 4: ListActivities; or, "Why are table view chapters always so long?"

Part 5: Files and Objects; or, I prefer JSON Bateman's early work

Part 6: HTTP Requests; or, "Oh, you have to download that? Put it on my Tab."

Part 7: HTTP Requests Part Deux; or, "It's my Intent to be POSTed up right here"

Part 8: Files and Objects Part Deux; or, I prefer my breakfast with Serial

Part 9: Preparing for the Market; or, "Wow, this is actually really easy"

Appendix: Source on Github

NOTE: It was my original intention for these to be a series of articles targeted at iOS developers who wanted to start porting their apps to Android; however, it became apparent that these tutorials are actually useful for anyone wanting to get started making Android apps. There's still a lot of useful information for iOS developers (I name drop a lot of UIKit classes and expain their counterparts in the Android SDK), but I'm confident that anyone can pick up these articles and making something awesome.

With that in mind:

Click to continue to Part 1: Overview

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:35:09 -0800 Beginning Android for iOS Developers - Part 1: Overview http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-1-o http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-1-o

(NOTE: while a lot of my bad jokes prose is targeted at iOS developers, other mortals will find these equally as useful. On with the show...)

You've just built an iOS app. You bit the bullet and downloaded the SDK, paid Apple its $99 developer fee, learned Cocoa and Objective-C, made some provisioning profiles, and out came an App that Apple now holds for a week or so with many others somewhere in Cupertino before being released into the Conradian jungle that is the App Store. You wonder...is this it? 

The answer is no Julian Casablancas, this isn't it. There are, in fact, several other mobile operating systems you can wine and dine, and they are diverse as they are numerous (see: moderately). 

Yet, you already know that don't you? Like any Michael Bay movie, let's cut to the chase: moving on to Android. It's the talk of the town, the belle of the ball, the little engine that could, I can go on for minutes. If you're going to write Apps on something besides iOS devices, you should probably start with Android.

Sadly, I've been trying to do this for far too long. I kept putting learning Android off because no one had written a resource for us iOS developers on how to start swinging both ways. Well, I finally got some time to sit down, shut up, and get to it. Here are the results:

Comparison
That's my baby, Brocabulary. She was my first, way back in 2009. Yeah, I know, she's got a little bit of crazy, but she's really not that complicated:
  • Display a list of items loaded from the filesystem
  • Display a list of downloaded items
  • Display a list of favorited items
  • Show a detail display when an item is tapped
  • Upload a new item

Pretty much the bread and butter of CRUD apps. I figure it's a pretty good starting point for 80% of whatever you're looking for, so why not just write about how I made it? Sound good?

Click to continue to Part 2: Tools; or, get up on my API Level

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:34:54 -0800 Beginning Android for iOS Developers - Part 2: Tools; or, get up on my API Level http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-2-t http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-2-t

Now that you know where we're going, let's...get there? You're going to need to download some things:

1. Android SDK - http://developer.android.com/sdk/index.html

Unzip it and store it somewhere meaningful, like /Developer. Right now it isn't awfully big because there aren't actually any source files in there, but that's where they'll go when we download them. What you actually downloaded was the just tools needed to download and manage the various SDK APIs, so you're not quite ready to develop just yet.

2. Something with which to develop

This is up to you. Many developers opt to use Eclipse, for which Google supplies a nifty plugin, but you're just as fine developing with emacs and using the command line tools provided with Android (which I won't cover in these tutorials). You might also want to consider the TextMate bundle by One Bit Increment, if you're into that kind of thing. Not that there's anything wrong with that.

Now that you're set up, let's download something useful. We do this using the "Android SDK and AVD Manager", a GUI tool Google provides with the SDK download. You can either launch this by going to Window -> Android SDK... in Eclipse or running the "android" executable in /<your SDK Path>/tools.

Androidmanager
This is what you use to manage your SDK packages and virtual devices (more on that in one hot minute), so you best get familiar with it.

Go to "Available Packages". There will probably be two branches: "Android Repository" and "Third party Add-ons." The former is the main branch of the Android SDK, where all of the core API levels are kept. These are the minimum things you need to get to make an app. Third party add-ons are where non-required APIs, such as Google's add-ons (primarily for Maps) or device-specific libraries, are kept.

Expand the main Android repository to browse all of the available packages. Android Platforms are organized into "API Levels" corresponding to each OS version (OS 1.5 == Level 3, OS 1.6 == Level 4, etc). For each API level you'll see not only the actual SDK but also samples and documentation. You'll also see revisions of the SDK Tools and Platform-tools...I'm really not sure about the difference between the two but you'll need those as well.

Androidmanager2
You might as well go and download everything, but if you're trying to save time like Marty McFly then I'd suggest SDKs levels 3 to 7. I'll be using SDK level 7 in these tutorials (Android 2.1). It might ask you to restart the Manager a few times, that's normal, but it'll also cut off your in-progress downloads so make sure you resume those when it restarts.

Let's also set up some emulators while we're here. You know how the iOS Simulator lets you use different hardware and OS versions, but they're all enclosed in the same program? Well...Android works quite a bit differently. You have to create different "virtual devices" for different kinds of devices, and there are all kinds of properties you can add or remove: internet connection, SD card slot, pixel density, resolution, etc etc. So, congratulations: your first encounter with Android fragmentation!

Go to "Virtual Devices" and hit "New". Call it what you like, but I would suggest adding the OS version somewhere in the name for future readability. Target it for API Level 7. Add a New hardware type and keep all the default settings. Feel free to examine all of the possible options if you want to momentarily feel bewildered.

Androidmanager3
*WHEW* That was pretty fun, right? Right? Regardless, good hustle. We'll Mark Pilgrim into proper Android development in the next portion.

 

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:34:41 -0800 Beginning Android for iOS Developers - Part 3: Activities; or, getting your Vibrams Five Fingers wet http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-3-a http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-3-a

You've got the tools set up, so let's start some actual code. Somehow, create a new Android project. In Eclipse, do this by using the default Android Project template. You can name it whatever you want, select Android 2.1 as the target SDK, and give it whatever application and package names you want. The package naming scheme is the same as the iOS bundle naming scheme, so something relevant like com.clayallsopp.isawesome. 

The Eclipse wizard will also let you create the main Activity (one hot minute and I'll explain); go ahead and name it OfflineListActivity. If you're not using Eclipse, you want to create a file called OfflineListActivity.java in the /src/<package path> folder of your Android project.

Finish whatever wizard you use and BAZINGA! Your first Android app :) Except...holy crap, what did you create? There are...so many folders and XML files. Well, let's throw caution to the wind and drop some code before I give a big long list of files and folders. 

If you go into your /src folder, you should see OfflineListActivity.java. It should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.clayallsopp.isawesome;

import android.app.Activity;
import android.os.Bundle;

public class OfflineListActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

Our OfflineListActivity extends (subclasses) Activity, which is the equivalent of a UIViewController in Android-land; accordingly, onCreate is effectually the same as viewDidLoad. We call the superclass's implementation and then do something a bit magical: setContentView(R.layout.main).

You can probably figure out what setContentView means, but what the hell is R.layout.main? Well, whenever you build your Android project, a file called R.java is built (you can find it in your /gen folder). It stores variables which allow you to access resources included with your project. So instead of traversing the bundle or doing a [UIImage imageNamed:], you use the variables created in R.java. Savvy? 

Resources in R.java are in a hierarchy that is directly correlated to their structure in your project file system; thus, R.layout.main refers to the resource "main" in the folder "layout" inside your resources folder, which is /res. These are actually integers, so when you go looking around the documentation it may not be immediately obvious that a function takes a resource! Also note that R.java does not take into account the extension of the file, so don't go putting clayisawesome.png and clayisawesome.jpeg in the same folder.

SO *deep breath* setContentView(R.layout.main) sets our Activity's contentView to the view defined in R.layout.main, aka /res/layout/main.*. Well then, what exactly is this enigmatic main.*?

Android uses XML files to define views (hereafter referred to as layouts), similar to how you can use .xibs to load interface elements in iOS...except there is no included tool that is as pleasant to use as Interface Builder. Yup, you'll be coding this XML layouts by hand. There are several third party resources, such as DroidDraw and the included Eclipse editor, but we won't be using them here. Thus, R.layout.main refers to main.xml, which defines our layout. We, obviously, need to go deeper.

Open up /res/layout/main.xml. It should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello"
        />
</LinearLayout>

THE HORROR MR. KURTZ, THE HORROR. I'll be plain: writing layouts is my biggest beef with Android development, especially when compared to the alternative in iOS development.

Our parent element in the layout is a LinearLayout, which is an invisible element (very much like a <div>, if you're familiar with HTML) that defines how its children are layed out. There are several other kinds of parent layouts: RelativeLayout, AbsoluteLayout, TableLayout...the list goes on. For the purposes of these articles, we'll get to them when we get to them. Just know that they exist. 

What a LinearLayout does is display each of it's children one after the other (linearly) in the orientation specified. So if you have two text views in a vertical orientation, they are displayed one after the other like a list. It's the simplest of all the layouts, so that's what it's the default. 

Inside our LinearLayout is a TextView, which is exactly what it sounds like. It's text is assigned to "@string/hello"...but that's not literally what it will display. When you start an XML assignment with @, you're referring to another resource. This is used for strings (for localization purposes, mainly) and referring to other interface elements (we'll get there!). @string in particular refers to /res/values/strings.xml, which collects various strings used throughout our project and app.

If you open strings.xml, you'll see <string name="hello">Hello World, OfflineListActivity!</string>. For good measure, replace that with something like...oh I don't know, <string name="hello">Clay is cool jklolwut</string> :). But really, howabout <string name="hello">I feel pretty...oh so pretty!</string>

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, MainTabBarActivity!</string>
    <string name="app_name">Brocabulary</string>
</resources>

You'll also see that all of the layout elements in main.xml have a layout_width and layout_height. These can take all kinds of values, including pixels, percentages, etc, but it's best to try and define non-absolute terms since you can't count on knowing the exact dimensions of every Android device. Alas, such is Android fragmentation.

So go ahead and launch your app! In Eclipse, it's as easy as hitting the Build button. Since we've already set up our virtual device in Part 2, there shouldn't be any more busy work. 

If the simulator wasn't already open, it'll take a minute to...boot up. Yknow, like a real device. The novelty wears off fast. Also, it won't automatically unlock the device for you, so remember to do that by hand.

You'll be greeted by a pretty this:

Part3screen
Didn't work? Check your AndroidManifest.XML and make sure it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.clayallsopp.isawesome"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".OfflineListActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

So what did we learn?
  • Activities == UIViewControllers
  • onCreate == viewDidLoad
  • Use R.java to refer to project resources in code (R.layouts.main)
  • Uses XML files to describe layouts (main.xml)
  • Use special parent layout elements to really define how your layouts look (LinearLayout)
  • Use the strings.xml file for strings

That's actually quite a lot of ground. Not a lot of code, I know, but you have to understand how everything fits together first.

Click to continue to Part 4: ListActivities; or, "Why are table view chapters always so long?"

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:34:00 -0800 Beginning Android for iOS Developers - Part 4: ListActivities; or, "Why are table view chapters always so long?" http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-4-l http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-4-l

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.clayallsopp.isawesome;

import android.app.ListActivity;
import android.os.Bundle;

public class OfflineListActivity extends ListActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
<!--
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
-->

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:

1
2
3
4
public class OfflineListActivity extends ListActivity {
    String[] brocabs = {"Brotocol","Brobot","Theodore Broosevelt"};
    ArrayList<String> brocabList = new ArrayList<String>(Arrays.asList(brocabs));
    ...

Watch as we only have to add *one line* to get our ListView to kick in:

1
2
3
4
5
6
7
8
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // EDIT: Shoutout to Cyril Mottier for pointing out that this setContentView isn't necessary
        // "When using a ListActivity, Android will automatically creates a ListView that fills the screen."
        // setContentView(R.layout.main);
        setListAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,brocabList));
    }

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.

Part4screen_0

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.clayallsopp.isawesome"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".OfflineListActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

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:

1
2
3
4
5
6
7
8
    ...
    <application
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.Light">
        ...
    </application>
    ...

Run the app and you'll see we now have more pleasant list:

Part4screen

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). 

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
...
</RelativeLayout>

For the body of our layout, we want to use a TextView and an ImageView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    ...
    <TextView
        android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:textStyle="bold"
android:gravity="center_vertical"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:paddingLeft="10px"
android:id="@+id/term" />
    <ImageView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignParentRight="true"
android:id="@+id/disclosure" />
    ...

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class BrocabAdapter extends ArrayAdapter<String> {

    public BrocabAdapter(Context context, int textViewResourceId, ArrayList<String> items) {
        super(context, textViewResourceId, items);
    }
    ...

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    ...
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = convertView;
        
        if (row == null) {
            LayoutInflater inflater = LayoutInflater.from(this.getContext());
            
            row = inflater.inflate(R.layout.list_item_with_disclosure,null);
            
            ImageView disclosure = (ImageView)row.findViewById(R.id.disclosure);
            disclosure.setImageResource(R.drawable.disclosure);
        }
        
        TextView label = (TextView)row.findViewById(R.id.term);
        label.setText(this.getItem(position));
        
        return row;
    }
}

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: 

1
2
3
4
5
6
7
    ...
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        setListAdapter(new BrocabAdapter(this,android.R.layout.simple_list_item_1,brocabList));
    }
    ...

Take a gander at our even prettier list:

Part4screen_2

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.

1
2
3
4
5
6
    ...
        row.setOnClickListener((OnClickListener)this.getContext());
        row.setTag(position);
        return row;
    }
    ...

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OfflineListActivity extends ListActivity implements OnClickListener {
    ...
    @Override
    public void onClick(View v) {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("BRO");
        alertDialog.setMessage("Check it: " + brocabs[(Integer) v.getTag()]);
        alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL,"OK",new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
        alertDialog.show();
    }
}

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:

Part4screen_3

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

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:33:16 -0800 Beginning Android for iOS Developers - Part 5: Files and Objects; or, I prefer JSON Bateman's early work http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-5-f http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-5-f

In the original spec for Brocabulary, we said one of our sources was a file included with the app. In this case, I'm going to be using a JSON data source for our data. Why JSON? It's easily readable and non-verbose in comparison to XML. There is fantastic support for XML parsing in Java and Android, actually more so than for JSON, so I'll leave the details of using an XML data source up to you; for now, I'll be sticking with JSON.

Let's take a look at our JSON structure real quick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
    {
        "brocab": {
            "description":"Fear of the brosephs",
            "author":"",
            "term":"Abroaphobia"
        }
    },
    {
        "brocab":{ ...
        }
    },
    ...
]

Appears as if we have an array of dictionaries. Each dictionary has one key, "brocab", which corresponds to a dictionary containing the core Brocab data. We need some way to turn from a file into a string into an ArrayList of corresponding Brocabulary objects. Guess we've got our work cut out for us.

First, we need to actually include our file with the project. Data files such as this are supposed to be included in /res/raw, so make that folder if it does not already exist and copy and paste brocabs.json into it (found in the source). And much like Bon Jovi, we're already half way there.

We could keep our Brocabulary objects as strings or dictionaries or hashes or something, but as seasoned iOS developers you should know it's good form to create a Java object to serve as our model. Create Brocab.java and give it String instance variables called term, author, and description. Unfortunately, Java doesn't have any handy mechanisms like @property for creating getters and setters, so you'll have to write those yourself. Poor you :'( When you're done, it should look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Brocab {

    String term;
    String author;
    String description;

    public String getTerm() {
        return term;
    }
    
    public void setTerm(String term) {
        this.term = term;
    }
    
    
    public String getAuthor() {
        return author;
    }
    
    public void setAuthor(String author) {
        this.author = author;
    }
    
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
}

Mostly harmless.

Now we need to load the original file. Now, there are a few fancy ways you can set this up which are more efficient, but for simplicity we'll just store all of our Brocabs in an ArrayList in our OfflineListActivity. This means we'll need to to make some Find+Replace-esque changes. For starters, we need to change all mentions of String to Brocab (such as in our ArrayList and ArrayAdapter definitions). To spare you (once again, poor you :'( ) the trouble, here are all minute changes (in a before/after format):

1
2
3
4
String[] brocabs = {"Brotocol","Brobot","Theodore Broosevelt"};
ArrayList<Brocab> brocabList = new ArrayList<Brocab>(Arrays.asList(brocabs));
->
ArrayList<Brocab> brocabList = new ArrayList<Brocab>();

1
2
3
4
5
6
7
8
public class BrocabAdapter extends ArrayAdapter<String>
->
public class BrocabAdapter extends ArrayAdapter<Brocab>


public BrocabAdapter(Context context, int textViewResourceId, ArrayList<String> items)
->
public BrocabAdapter(Context context, int textViewResourceId, ArrayList<Brocab> items)

Cool beans. Now let's get to the fun part: loading this bad boy. To make this pretty, we're going to put all of the loading in a separate function of OfflineListActivity. You might be tempted to put it in onCreate, but it'll look a bit sloppy.

1
2
3
4
5
6
7
8
9
10
11
12
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
setContentView(R.layout.local);
loadBrocabulary();
}

public void loadBrocabulary() {
    ...TO BE CONTINUED...
}
...

This is going to be a bit of a mess, so I'll go through it in chunks.

1
2
3
    public void loadBrocabulary() {
String jsonRep = null;
...

We're going to store our JSON representation as a string, THEN parse that string.

1
2
3
4
5
6
7
8
    ...
    try {
        InputStream in = getResources().openRawResource(R.raw.iphone);
        ...TO BE CONTINUED...
    } catch (Throwable t) {
     Toast.makeText(this, "Exception: "+t.toString(), 2000).show();
    }
    ...

We try to create an InputStream using our iphone.json file (found in the source). Note, once again, that R.java doesn't care for the extensions of our files. If it fails, we create a Toast. WAIT JUST A MINUTE THERE CLAY WHAT IS A TO--okay, that's understandable. A Toast is a neat little Android UI element that sort of resembles a UIProgressHUD (...Apple, I swear I don't know what that is and I've never used one...). It's a little box that fades in near the bottom of the screen, displays a message, and fades out. It's designed to be used for non-intrusive notifications, but I like them because they're dead easy to create.

Anyway, where were we?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    ...
    if (in != null) {
        Writer writer = new StringWriter();
        char[] buffer = new char[1024];
        try {
            Reader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
            int n;
            while((n = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, n);
            }
        } finally {
            in.close();
        }
        jsonRep = writer.toString();
    }
    else {
    }
    ...

I'm sorry this isn't as magical as it could be. There's probably a prettier abstraction somewhere, but this'll work. Basically, it reads through our InputStream and writes its contents to a Writer, which in turn we turn into our jsonRep string.

WHEW. Here's that whole block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    ...
    String jsonRep = null;
    try {
        InputStream in = getResources().openRawResource(R.raw.iphone);
if (in != null) {
Writer writer = new StringWriter();
char[] buffer = new char[1024];
try {
Reader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
int n;
while((n = reader.read(buffer)) != -1) {
writer.write(buffer, 0, n);
}
} finally {
in.close();
}
jsonRep = writer.toString();
}
else {
Toast.makeText(this, "Problem opening the file", 2000).show();
}
    } catch (Throwable t) {
     Toast.makeText(this, "Exception: "+t.toString(), 2000).show();
    }
    ...

NEXT we need to turn our string into an ArrayList. There are a few options for JSON parsing in Java, some more magical than others. This is a pretty lo-fi one but, as with many things in my life, it "just works". Remember to import org.json.JSONException, org.json.JSONObject, and org.json.JSONArray.

1
2
3
4
5
6
7
8
    ...
    try {
     JSONArray jsonArray = new JSONArray(jsonRep);
     ...TO BE CONTINUED...
    } catch (JSONException e) {
     Toast.makeText(this, "JSONException: "+e.toString(), 2000).show();
    }
    ...

This tries to turn our string into a JSONArray, which we know to be the structure of the data. If that fails, then something incorrectly formatted in your file =O

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    ...
    for (int i = 0; i < jsonArray.length(); i++) {
JSONObject brocabContainerDict = jsonArray.getJSONObject(i);
JSONObject brocabDict = brocabContainerDict.getJSONObject("brocab");

String term = brocabDict.getString("term");
String author = null;
if(brocabDict.has("author")) {
author = brocabDict.getString("author");
}
String description = brocabDict.getString("description");

Brocab brocab = new Brocab();
brocab.setTerm(term);
brocab.setAuthor(author);
brocab.setDescription(description);

brocabs.add(brocab);
    }
    ...

There's probably a way to compress this code, but I made it a bit verbose for readability. We iterate through the JSONArray with getJSONObject(i) to pick out each JSONObject (dictionary) before getting the real data we want from it with getJSONObject("brocab"). We then create strings for each property available, checking to see if the author exists (which it may not). Finally, we create a new Brocab object and add it to our ArrayList.

The complete code for this section looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    ...
    try {
        JSONArray jsonArray = new JSONArray(jsonRep);
        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject brocabContainerDict = jsonArray.getJSONObject(i);
            JSONObject brocabDict = brocabContainerDict.getJSONObject("brocab");
        
            String term = brocabDict.getString("term");
            String author = null;
            if(brocabDict.has("author")) {
                author = brocabDict.getString("author");
            }
            String description = brocabDict.getString("description");
        
            Brocab brocab = new Brocab();
            brocab.setTerm(term);
            brocab.setAuthor(author);
            brocab.setDescription(description);
        
            brocabs.add(brocab);
        }
    } catch (JSONException e) {
        Toast.makeText(this, "JSONException: "+e.toString(), 2000).show();
    }
    ...

ALMOST THERE!

The last thing we need to do in our loadBrocabulary() function is actually create our BrocabAdapter with a simple

1
2
3
    ...
    setListAdapter(new BrocabAdapter(this,android.R.layout.simple_list_item_1,brocabList));
}

So, if you haven't been following along, the monstrous function looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;

import android.widget.Toast;

...

public void loadBrocabulary() {
    String jsonRep = null;
    try {
        InputStream in = getResources().openRawResource(R.raw.iphone);
        if (in != null) {
            Writer writer = new StringWriter();
            char[] buffer = new char[1024];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
                int n;
                while((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                in.close();
            }
            jsonRep = writer.toString();
        }
        else {
            Toast.makeText(this, "Problem opening the file", 2000).show();
        }
    } catch (Throwable t) {
        Toast.makeText(this, "Exception: "+t.toString(), 2000).show();
    }

    try {
        JSONArray jsonArray = new JSONArray(jsonRep);
        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject brocabContainerDict = jsonArray.getJSONObject(i);
            JSONObject brocabDict = brocabContainerDict.getJSONObject("brocab");
    
            String term = brocabDict.getString("term");
            String author = null;
            if(brocabDict.has("author")) {
                author = brocabDict.getString("author");
            }
            String description = brocabDict.getString("description");
    
            Brocab brocab = new Brocab();
            brocab.setTerm(term);
            brocab.setAuthor(author);
            brocab.setDescription(description);
    
            brocabList.add(brocab);
        }
    } catch (JSONException e) {
        Toast.makeText(this, "JSONException: "+e.toString(), 2000).show();
    }

    setListAdapter(new BrocabAdapter(this,android.R.layout.simple_list_item_1,brocabList));
}

In the words of Drake, you can Thank Me Later.

SO ARE WE DONE YET??!?!?! *Almost*. We still need to tell our BrocabAdapter how to use the Brocab objects instead of simple strings. This, unlike Ron Burgandy, is not a big deal:

1
2
3
4
5
6
7
8
    ...
    TextView label = (TextView)row.findViewById(R.id.term);

    Brocab rowItem = (Brocab)this.getItem(position);
    label.setText(rowItem.getTerm());
    ...
    return row;
}

It's probably a good idea to handle this back in our OfflineListActivity onClick method as well:

1
2
3
4
5
6
7
8
9
10
11
    ...
    alertDialog.setTitle("BRO");
    Brocab viewItem = (Brocab) brocabList.get((Integer)v.getTag());
    alertDialog.setMessage("Check it: " + viewItem.getTerm());
    alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL,"OK",new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int which) {
        
        }
    });
    alertDialog.show();
}

OKAY. Now we're done. Run it and bask at what we've done...which looks remarkably like our previous version:

Part5screen

Here's a quick recap:
  • Creating a Java class for our model
  • Loaded a file included as a project resource
  • Parsed the JSON of said file into our model
  • Adjusted our ArrayAdapter to use the model
The awesome thing is that our BrocabAdapter will work the same with no changes for all ListActivities we use Brocabs in: our favorites activity, our online activity...speaking of which...

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:32:20 -0800 Beginning Android for iOS Developers - Part 7: HTTP Requests Part Deux; or, "It's my Intent to be POSTed up right here" http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-7-h http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-7-h

Let's think of our user: I'm downloading all these brodacious brocabulary words, but how can I get in on the action? Well, luckily our awesome server is RESTful and will let him (or her!) upload new words using POST requests.

We have the technology, but how do we put it all together? We'll need to create a new View for this. In the original Brocabulary, we popped up a modal view controller to serve as our form. There's no equivalent presentation type on Android, so we'll just have to come up with something else.

Wait, we haven't even discussed how to bring up arbitrary Activities have we? So far, we've let our TabActivity handle all of that behind the scene. So let's start there.

Create a new Activity called CreateBrocabActivity.java and a corresponding layout create.XML. Also add CreateBrocabActivity to our AndroidManifest.XML (<activity android:name=".CreateBrocabActivity" />). It'll look a bit plain: three text inputs for term, description, and (optionally) the author's name, plus buttons for canceling and submitting. Hey, I didn't say I was going to teach you about design did it?

This is going to be a pretty packed layout so let's take it a few elements at a time:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    ...
</RelativeLayout>

We're going to be using a relative layout as our parent. Why? We want to align things on top of each other and along the edges of the parent. Makes the most sense to use a RelativeLayout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    ...
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:id="@+id/term_hint"
        android:text="What's your brocab?" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:textSize="30px"
        android:textStyle="bold"
        android:layout_below="@id/term_hint"
        android:id="@+id/create_term"
        android:singleLine="true"/>
    ...

These are our elements for the Brocab's term. Why both a TextView and EditText (an equivalent to UITextField and UITextView)? Well, we want our EditText to be a singleLine, but we also want to give the user some instruction. Now, EditTexts do provide an "android:hint" option which serves as an equivalent to the UITextField's "placeholder" property; HOWEVER, if you enable singleLine, the hint disappears. I know, I know, that makes zero sense, but it's what happens. Google it or try it out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    ...
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_description"
        android:gravity="center_horizontal"
        android:id="@+id/description_hint"
        android:text="Give a sweet definition" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:gravity="center_horizontal"
        android:textSize="20px"
        android:id="@id/create_description"
        android:singleLine="true"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_author"
        android:gravity="center_horizontal"
        android:id="@+id/description_hint"
        android:text="And your name? (optional!)" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_buttons"
        android:gravity="center_horizontal"
        android:textSize="20px"
        android:id="@id/create_author"
        android:singleLine="true"/>
    ...

We do the same sort of thing for the description and author inputs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    ...
    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal"
        android:id="@id/create_buttons">
        <Button
            android:id="@+id/create_back"
            android:text="Back"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
        <Button
            android:id="@+id/create_upload"
            android:text="Upload"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
    ...

At the bottom of the page, we want two bottoms (similar to our OnlineListActivity.) We give them equal, non-zero weights so the layout is split evenly between them (recall layout_weight is a property of LinearLayout).

The whole thing looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:id="@+id/term_hint"
        android:text="What's your brocab?" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:textSize="30px"
        android:textStyle="bold"
        android:layout_below="@id/term_hint"
        android:id="@+id/create_term"
        android:singleLine="true"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_description"
        android:gravity="center_horizontal"
        android:id="@+id/description_hint"
        android:text="Give a sweet definition" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:gravity="center_horizontal"
        android:textSize="20px"
        android:id="@id/create_description"
        android:singleLine="true"/>
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_author"
        android:gravity="center_horizontal"
        android:id="@+id/description_hint"
        android:text="And your name? (optional!)" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/create_buttons"
        android:gravity="center_horizontal"
        android:textSize="20px"
        android:id="@id/create_author"
        android:singleLine="true"/>
    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal"
        android:id="@id/create_buttons">
        <Button
            android:id="@+id/create_back"
            android:text="Back"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
        <Button
            android:id="@+id/create_upload"
            android:text="Upload"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
</RelativeLayout>

FUN STUFF. Now let's code it! We need to add some instance variables to our CreateBrocabActivity and make it implement the OnClickListener interface:

1
2
3
4
5
6
7
8
9
10
11
12
public class CreateBrocabActivity extends Activity implements OnClickListener {
    Button upload;
    Button back;
    
    boolean uploaded;

    EditText term;
    EditText description;
    EditText author;

    ProgressDialog progress;
    ...

What's up with all those? We need to keep track of the buttons to see which one was tapped in our onClick function. The uploaded boolean is used to see if the brocab was uploaded, thus preventing the app from uploading it twice if the user taps the button repeatedly. We obviously need to keep our EditTexts so we can find the user input later. And the progress dialog is kept so we can show and dismiss it when needed.

We then connect all of these in our onCreate function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.create);

        uploaded = false;

        upload=(Button)findViewById(R.id.create_upload);
        upload.setOnClickListener(this);

        back=(Button)findViewById(R.id.create_back);
        back.setOnClickListener(this);

        term=(EditText)findViewById(R.id.create_term);
        description=(EditText)findViewById(R.id.create_description);
        author=(EditText)findViewById(R.id.create_author);
    }
    ...

All we have left to do is handle those button clicks:

1
2
3
4
5
6
7
8
9
10
11
12
    ...
    @Override
    public void onClick(View v) {
        if(v == back) {
            // Get rid of the Activity
        }
        else if (v == upload && !uploaded) {
            progress = ProgressDialog.show(this, "Uploading...","Just chill bro, it's going to space.",true,false);
            upload();
        }
    }
    ...

We'll hold off on what needs to happen when the back button is clicked; for now, we need to fill in that upload function, which is another beast like its GET brother.

1
2
3
4
5
6
    ...
    public void upload() {
        String u_term = term.getText().toString();
        String u_description = description.getText().toString();
        String u_author = author.getText().toString();
        ...

Cool story bro, you assigned the values of the EditTexts to string.

1
2
3
4
5
6
7
8
9
10
11
12
13
        ...
        boolean term_is_zero = (u_term.length() == 0);
        boolean description_is_zero = (u_description.length() == 0);

        if (term_is_zero || description_is_zero) {
            if(term_is_zero && description_is_zero)
                showErrorWithString("Bro, you need a brocab term and definition!");
            else if (term_is_zero)
                showErrorWithString("Bro, you need a brocab term!");
            else if (description_is_zero)
                showErrorWithString("Bro, you need a definition!");
        }
        ...

We do some basic input checking here. Note that since the author field is optional so we don't look at it. We're using some helper methods to shorten our code a bit, which we'll define later. Perhaps you should try and imagine what the code looks like? Basic AlertDialogs, nothing too difficult to implement yourself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        ...
        else {
            Handler handler = new Handler () {
                public void handleMessage(Message message) {
                    switch (message.what) {
                        case HttpConnection.DID_START:
                            upload.setText("Uploading...");
                            break;
                        case HttpConnection.DID_SUCCEED:
                            upload.setText("Uploaded!");
                            uploadToast();
                            uploaded = true;
                            progress.dismiss();
                            break;
                        case HttpConnection.DID_ERROR:
                            upload.setText("Upload");
                            Exception e = (Exception) message.obj;
                            progress.dismiss();
                            handleError(e);
                            break;
                    }
                }
            };
            ...

If there weren't any problems, we create a Handler. Note that the above statement is only creating the Handler, not the end of the else structure. This Handler does the same sort of thing as our GET Handler did, so it should look familiar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
            ...
            try {
                String c_term = URLEncoder.encode(u_term,"utf-8");
                String c_description = URLEncoder.encode(u_description,"utf-8");
                String c_author = null;
                if (u_author.length() > 0)
                    c_author = URLEncoder.encode(u_author,"utf-8");
                String query;
                if (c_author != null)
                    query = "term="+c_term+"&description="+c_description+"&author="+c_author;
                else
                    query = "term="+c_term+"&description="+c_description;
                // NOTE: THIS WON'T WORK
                new HttpConnection(handler).post("http://derpington.com/brocabs/",query);
            }
            catch (Exception T) {
                handleError(T);
            }
        }
    }
    ...

Still inside of the else, we go ahead and URL encode all of the inputs for transport. We also account for the case where there wasn't an author.

Here are all the little helper functions I used to make it more readable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    ...
    public void handleError(Exception e) {
        showErrorWithString("There was a problem: " + e.toString());
    }

    public void showErrorWithString(String e) {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("Uh, error bro");
        alertDialog.setMessage(e);
        alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
                      // here you can add functions
                   }
        });
        alertDialog.show();
    }

    public void uploadToast() {
        Toast.makeText(this,"Uploaded successful",2000).show();
    }
}

So here's the big ole list of functions itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import java.net.URLEncoder;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.Toast;

    ...
    public void upload() {
        String u_term = term.getText().toString();
        String u_description = description.getText().toString();
        String u_author = author.getText().toString();
        
        boolean term_is_zero = (u_term.length() == 0);
        boolean description_is_zero = (u_description.length() == 0);
        
        if (term_is_zero || description_is_zero) {
            if(term_is_zero && description_is_zero)
                showErrorWithString("Bro, you need a brocab term and definition!");
            else if (term_is_zero)
                showErrorWithString("Bro, you need a brocab term!");
            else if (description_is_zero)
                showErrorWithString("Bro, you need a definition!");
        }
        else {
            Handler handler = new Handler () {
                public void handleMessage(Message message) {
                    switch (message.what) {
                        case HttpConnection.DID_START:
                            upload.setText("Uploading...");
                            break;
                        case HttpConnection.DID_SUCCEED:
                            upload.setText("Uploaded!");
                            uploadToast();
                            uploaded = true;
                            progress.dismiss();
                            break;
                        case HttpConnection.DID_ERROR:
                            upload.setText("Upload");
                            Exception e = (Exception) message.obj;
                            progress.dismiss();
                            handleError(e);
                            break;
                    }
                }
            };
            
            try {
                String c_term = URLEncoder.encode(u_term,"utf-8");
                String c_description = URLEncoder.encode(u_description,"utf-8");
                String c_author = null;
                if (u_author.length() > 0)
                    c_author = URLEncoder.encode(u_author,"utf-8");
                String query;
                if (c_author != null)
                    query = "term="+c_term+"&description="+c_description+"&author="+c_author;
                else
                    query = "term="+c_term+"&description="+c_description;
                // NOTE: THIS WON'T WORK.
                new HttpConnection(handler).post("http://derpington.com/brocabs/",query);
            }
            catch (Exception T) {
                handleError(T);
            }
            
        }
    }
    
    public void handleError(Exception e) {
        showErrorWithString("There was a problem: " + e.toString());
    }
    
    public void showErrorWithString(String e) {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("Uh, error bro");
        alertDialog.setMessage(e);
        alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
                      // here you can add functions
                   }
        });
        alertDialog.show();
    }
    
    public void uploadToast() {
        Toast.makeText(this,"Uploaded successful",2000).show();
    }
}

HOORAY! Now all we need to do is display it.

In iOS development, we use some sort of parent UIViewController to display new view controllers, such as a UINavigationController. Unfortunately, there's no equivalent class in Android. Instead, any Activity can spawn a new Activity and bring it to the front using Intents. Let's do this in our OnlineListActivity.

Back in that Activity's onClick method, add the following:

1
2
3
4
5
6
7
8
9
10
11
    ...
    public void onClick(View v) {
        if (v == refresh) {
            ...
        }
        else if (v == create) {
            startActivity(new Intent(this, CreateBrocabActivity.class));
        }
        ...
    }
    ...

You also need to import android.content.Intent. This is a pretty simple implementation of starting a new Activity. The theory behind Activities is that they have a purpose that starts and ends; thus, it is also possible for an newly spawned Activity to send a message back to its spawner when it completes. This isn't necessary in our case, but...the more you know.

Anyway, back in CreateBrocabActivity, add a call to finish() in the onClick method:

1
2
3
4
5
6
    public void onClick(View v) {
        if(v == back) {
            finish();
        }
        ...
    }

And we're done! finish() is an Activity method that closes itself and returns the app to whence it came.

Get it a whirl! It won't actually work, since I'm not putting up my own server for you kids to send naughty requests to, but plug it should work fine for your own web apps.

Part7screen

Reflect on what we've done:
  • Create a new Activity using Intents
  • Use EditTexts to create a form
  • Sent HTTP requests with some URL encoded data

Click to continue to Part 8: Files and Objects Part Deux; or, "I prefer my breakfast with Serial"

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:32:00 -0800 Beginning Android for iOS Developers - Part 6: HTTP Requests; or, "Oh, you have to download that? Put it on my Tab." http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-6-h http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-6-h

Now that we've successfully loaded and parsed something from a local file...TO THE CLOUD!™ It's not all that difficult, especially since we've already written the code which parses the JSON; we simply need to make an HTTP request. But, unfortunately, doing only that in this article would be too easy: we also need to change how our application is structured.

If you recall, Brocabulary on iOS was built around UITabBarController. Thankfully, there is an equivalent on Android: TabActivity (hard to figure out, right?).

EDIT: More thanks to Cyril Mottier, who pointed out that this is the default layout Android provides for you, thus you don't need to create it yourself.

Let's lay this bad boy out. Create a new file called tab.XML, which will serve as our layout for the app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:orientation="vertical"
        android:layout_height="fill_parent"
        android:layout_width="fill_parent">
        <TabWidget
            android:id="@android:id/tabs"
            android:layout_height="wrap_content"
            android:layout_width="fill_parent" />
        <FrameLayout
            android:id="@android:id/tabcontent"
            android:layout_height="fill_parent"
            android:layout_width="fill_parent">
        </FrameLayout>
    </LinearLayout>
</TabHost>

Essentially, we make our parent layout a TabHost and then can layout our children however we wish. This is quite unlike iOS, where our UITabBar is stuck at the bottom of the screen. You could move our TabBar wherever you like, but having a TabBar on top is a native Android practice and should be respected wherever possible. You might also notice that for ids we're using "@android:id" instead of "@+id". This is a way of referencing android.R.java, not our R.java.

Next, we need to create a TabBarActivity. Make BrocabTabBarActivity, a subclass of TabActivity. All we're doing here is overriding onCreate and adding a tab:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.app.TabActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;

public class BrocabTabBarActivity extends TabActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // EDIT: More thanks to Cyril Mottier, who pointed out that this
        // is unnecessary.
        //setContentView(R.layout.tab);

        TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);
        
        TabSpec firstTabSpec = tabHost.newTabSpec("tid1");
        firstTabSpec.setIndicator("Brocabs");
        firstTabSpec.setContent(new Intent(this,OfflineListActivity.class));

        tabHost.addTab(firstTabSpec);
    }
}

A TabSpec is a simple way of organizing the label, icon, and Activity for the tab.

The interesting bit is this: "new Intent(this,OfflineListActivity.class)". What's all this about? Well I *intend* to tell you. :).

I briefly mentioned them way back in article 4 in AndroidManifest.XML. Intents are quite powerful objects used to specify an "operation to be performed" (such as when a system-wide event happens, another App opens a certain URL, or when a tab bar is tapped). In this case, our intent only contains information about the class to be created for that particular tab. TabSpec and TabHost abstract away actually dealing with handling the intent and just ask us for one; don't worry though, we'll be using our own soon enough.

Done yet? Nope. We have to actually get this TabActivity to display. Currently, we're launching the app with our OfflineListActivity. Open AndroidManifest.XML and let's change it to use our BrocabTabBarActivity:

1
2
3
4
5
6
<application android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@android:style/Theme.Light">
    <activity android:name="BrocabTabBarActivity" android:label="@string/app_name">
        ...
    </activity>
    <activity android:name=".OfflineListActivity" />
</application>

See that last <activity> tag? For every Activity our application we use, we need to specify it in the Manifest.

Run our application and you should see something pretty similar to what we had before, except with a large grey bar on top. Looks like we need another Activity, no?

Part6screen

We're going to call our next ListActivity OnlineListActivity.java (poignant, no?). For now, just copypasta the definition of OfflineListActivity in there.

Add another tab to our TabHost and a line to our AndroidManifest.XML to put it all together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);

TabSpec firstTabSpec = tabHost.newTabSpec("tid1");
firstTabSpec.setIndicator("Brocabs");
firstTabSpec.setContent(new Intent(this,OfflineListActivity.class));

TabSpec secondTabSpec = tabHost.newTabSpec("tid2");
secondTabSpec.setIndicator("Online");
secondTabSpec.setContent(new Intent(this,OnlineListActivity.class));

tabHost.addTab(firstTabSpec);
tabHost.addTab(secondTabSpec);
...

1
2
3
4
5
6
<application android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@android:style/Theme.Light">
    ...
    <activity android:name=".OfflineListActivity" />
    <activity android:name=".OnlineListActivity" />
</application>
...

Try running it now; much prettier, right? 

Part6screen_1

Real quick, we need to also tell our app that we're going to use the internet. There are many other permissions that we can request, but I'll leave that to you.

1
2
3
4
5
6
...
<uses-permission android:name="android.permission.INTERNET" />
<application android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:theme="@android:style/Theme.Light">
...

Let's Free Willy 1 and get back to our original porpoise: downloading from the cloud. However, we need to make some simple UI changes first. We're going to add a row of two bottoms at the bottom of the screen: "Refresh" and "Create" (we'll get to that chestnut later). 

Create a new XML file called online.XML and copypasta main.XML as a start. We're going to keep our LinearLayout but add a few things:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ListView
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_weight="1" />
    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:orientation="horizontal">
            <Button
                android:id="@+id/online_refresh"
                android:text="Refresh"
                android:layout_weight="1"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content" />
            <Button
                android:id="@+id/online_create"
                android:text="Create"
                android:layout_weight="1"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

First, android:layout_weight. LinearLayouts support adding "weights" to layout elements, which provides a way for proportionally distributing screen real-estate (thus, a weight is unitless). So in this example, everything has a weight of 1, thus every element will take up the space specified by its layout_height (taking up the space necessary for its content, in this case). 

Everything else here should look typical. We assigned IDs to our two buttons, which we'll be using right...about...now!

Go back to OnlineListActivity.java, add two Button instance variables, and change our onCreate to look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
Button refresh;
Button create;
ProgressDialog progress;
ArrayList<Brocab> brocabList = new ArrayList<Brocab>();

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.online);
    refresh=(Button)findViewById(R.id.online_refresh);
    refresh.setOnClickListener(this);
    
    create=(Button)findViewById(R.id.online_create);
    create.setOnClickListener(this);
}
...

Well, looks like we also need to update our onClick method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void onClick(View v) {
    if (v == refresh) {
        progress = ProgressDialog.show(this, "Refreshing...","Just chill bro.",true,false);
        downloadBrocabulary();
    }
    else if (v == create) {
        
    }
    else {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("BRO");
        Brocab viewItem = (Brocab) brocabList.get((Integer)v.getTag());
        alertDialog.setMessage("Check it: " + viewItem.getTerm());
        alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL,"OK",new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
            
            }
        });
        alertDialog.show();
    }
}

There's a better way of doing this that we'll implement later, but for now this will do. We also added a fancy ProgressDialog which we'll show while refreshing so the user doesn't get impatient and, as Starcraft players would say, "QQ". So let's get to the Gorilla: downloadBrocabulary.

On iOS, making asynchronous HTTP requests is pretty simple: NSString -> NSURL -> NSURLRequest -> NSURLConnection. Lots of layers, yes, but it's not that hard to understand. Java, on the other hand, somehow makes everything difficult. There's no well defined solution for doing this, so we'll have to resort to external libraries. You can pick and choose what you want to use (Ning, NIO, Jetty are some examples), but I'm going to use some code by Greg Zavitz & Joseph Roth that uses just two classes: HTTPConnection and ConnectionManager. Add these (found in the source) to your project.

We're also going to use a Handler object, which lets us do all kinds of fancy threading magic with our functions.

So, with that in mind, here's downloadBrocabulary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void downloadBrocabulary() {
    Handler handler = new Handler () {
        public void handleMessage(Message message) {
            switch (message.what) {
                case HttpConnection.DID_START:
                    refresh.setText("Refreshing...");
                    break;
                case HttpConnection.DID_SUCCEED:
                    refresh.setText("Refresh");
                    String response = (String)message.obj;
                    loadBrocabulary(response);
                    progress.dismiss();
                    break;
                case HttpConnection.DID_ERROR:
                    refresh.setText("Refresh");
                    Exception e = (Exception) message.obj;
                    e.printStackTrace();
                    progress.dismiss();
                    handleError(e);
                    break;
            }
        }
    };

    new HttpConnection(handler).get("http://clayallsopp.com/brocabs.json");
}

Pretty simple. Our handleMessage function (which overrides the default implementation, if you didn't catch the Java syntax) responds to changes in the HttpConnection's state. If it succeeds, we pass the response to our loadBrocabulary as expected (with the minor change of passing the string instead of loading it from a file). If it fails, we display the error with a separate function, which makes our code a little smaller since displaying errors is a pretty common thing; either way, we also dismiss our ProgressDialog. Both of these functions are listed below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void loadBrocabulary(String jsonRep) {
    try {
        JSONArray jsonArray = new JSONArray(jsonRep);
        ... EVERYTHING ELSE IS THE SAME ...
    }
    setListAdapter(new BrocabAdapter(this,android.R.layout.simple_list_item_1,brocabList));
}

public void handleError(Exception e) {
    AlertDialog alertDialog = new AlertDialog.Builder(this).create();
    alertDialog.setTitle("Uh, error bro");
    alertDialog.setMessage("There was a problem: " + e.toString());
    alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int which) {
                  // here you can add functions
               }
    });
    alertDialog.show();
}

Run it and TELL THE INTERNET I SAID HI!

Part6screen_3

So what did we learn this go-round?
  • TabActivity == UITabBarController
  • How to set up a TabBar and TabActivity
  • Setting up Permissions and multiple Activities in AndroidManifest.XML
  • Sending an HTTP request (remember, there are other libraries that can do this too!)
  • Parsing the JSON response

Click to continue to Part 7: HTTP Requests Part Deux; or, "It's my Intent to be POSTed up right here".

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:31:56 -0800 Beginning Android for iOS Developers - Part 8: Files and Objects Part Deux; or, "I prefer my breakfast with Serial" http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-8-f http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-8-f

While we're on this creating new Activities and Intents business, let's go ahead and create a detail view for our Brocabulary. Instead of an AlertDialog appearing when the user selects a row, we should push a new Activity that gives the term, author, and description of the Brocab (pretty much how our CreateBrocabActivity looks). 

But...that's just *too* easy. We also want to keep track of the user's favorite Brocabulary, both from our offline and online sources. Sound like a plan?

Create BrocabDetailActivity.java and detail.XML. Remember to add BrocabDetailActivity to your AndroidManifest.XML! (<activity android:name=".BrocabDetailActivity" />) Accordingly, detail.XML bears a striking resemblance to create.XML, so I won't go into too much detail about its implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center_horizontal"
        android:textSize="40px"
        android:textStyle="bold"
        android:id="@+id/detail_term" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:gravity="center_horizontal"
        android:textSize="30px"
        android:id="@+id/detail_description" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/detail_buttons"
        android:gravity="center_horizontal"
        android:textSize="30px"
        android:id="@+id/detail_author" />
    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal"
        android:id="@id/detail_buttons">
        <Button
            android:id="@+id/detail_back"
            android:text="Back"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
        <Button
            android:id="@+id/detail_add"
            android:text="Add to Favorites"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
</RelativeLayout>

That was quick. Now, let's talk for a moment about how we're going to pass this data around. When we did our CreateBrocabActivity, there was no need for it and the OnlineListActivity to communicate; with our detail Activity, however, we need to send the source Brocab object to the new Activity. There are a few potential ways of solving this, but I'm going to use a nifty trick Intents provide: "Extras". And no, not the Ricky Gervais show.

Intents can store extra data in a dictionary-like format which can then be accessed in the spawned Activity. Pretty neat, eh? Thus, we'll just pass our Brocab object inside the Intent and then get it out in our detail Activity's onCreate method. Speaking of which, let's take a quick look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class BrocabDetailActivity extends Activity implements OnClickListener {
    Button back;
    Button favorite;

    Brocab brocab;
    boolean favorited;
    boolean fromFavorites;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.detail);
        
        brocab = (Brocab)this.getIntent().getSerializableExtra("brocab");
        
        back=(Button)findViewById(R.id.detail_back);
        back.setOnClickListener(this);
        
        favorite=(Button)findViewById(R.id.detail_add);
        favorite.setOnClickListener(this);

        TextView term = (TextView)findViewById(R.id.detail_term);
        term.setText(brocab.getTerm());
        
        TextView description = (TextView)findViewById(R.id.detail_description);
        description.setText(brocab.getDescription());
        
        TextView author = (TextView)findViewById(R.id.detail_author);
        if(brocab.getAuthor() != null && brocab.getAuthor().length() > 0 && !brocab.getAuthor().equals("null")) {
            author.setText("Submitted by " + brocab.getAuthor());
        }
    }
    ...

Pretty basic stuff. The only thing foreign is when we retrieve our Brocab: "brocab = (Brocab)this.getIntent().getSerializableExtra("brocab");". What's this "serializable" business? Well, you can't store just any old object in an Intent's extras; it needs to implement the Java Serializable interface, which is sort of like complying to the NSCoding and NSCopying protocols back in Objective-C. Now, those can be kind of a hassle to implement, so how about this Serializable business? 

NOPE. Not difficult in the least. We need only to say that our object is Serializable, declare a serialVersionUID, and Java will handle the rest. So, way back in Brocab.java, add:

1
2
3
4
5
6
import java.io.Serializable;

public class Brocab implements Serializable {
    private static final long serialVersionUID = -8403439264570933221L;
    ...
}

AND THAT'S IT! Congratulations, your object is now serializable. This will come in handy very soon.

Now, back to our BrocabDetailActivity. We've retrieved our Brocab object, filled in the text boxes, and just need to get our onClicks working:

1
2
3
4
5
6
7
8
9
10
11
    ...
    @Override
    public void onClick(View v) {
        if (v == back) {
            finish();
        }
        else if (v == favorite) {
            // Do this later
        }
    }
}

Piece of cake. Just like our CreateBrocabActivity, we just throw a finish() and we're done. We'll implement all the favoriting magic later.

In each of our ListActivities, we need to push the detail Activities. Where we previous had code creating AlertDialogs, use the following snippet:

1
2
3
4
5
6
    ...
    Brocab brocab = brocabList.get((Integer)v.getTag());
    Intent i = new Intent(this, BrocabDetailActivity.class);
    i.putExtra("brocab",brocab);
    startActivity(i);
    ...

Run the app and bask in your own abilities. 

Part8screen

Now, on to that favorites business. We need some way to keep a persistent list of the user's favorite Brocabs. On iOS, we have SQLite3, file system access, and NSUserDefaults. For something like this, I would probably go with NSUserDefaults since it's pretty painless to use for the amount of data we're storing (probably no more than a few dozen entries). On Android, we have equivalents of all those methods, plus the ability to store data externally (such as an SD card). So we're using the SharedPreferences (NSUserDefaults equivalent), right? 

Unfortunately, Android's SharedPreferences can only store primitive data types. We could figure out some way to compress Brocabs into a string or bytes or something, but that seems like a hack-ish solution. Our Brocabs are already Serializable...could that help? "Well, actually", having a class be Serializable allows us to easily read and write that class to a file. Problem solved.

So, where are we going to put these Favorites? In a new ListActivity, naturally. Create FavoritesListActivity.java, add it to our AndroidManifest (<activity android:name=".FavoritesListActivity" />), and make the corresponding favorites.XML. Copypasta main.XML into favorites.XML, as they are identical. Also use the same instance variables, onCreate, and onClick functions as OfflineListActivity.

The only thing we need to change is our loadBrocabulary method, which is thankfully much shorter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    ...
    public void loadBrocabulary() {
        try {
            FileInputStream fis = openFileInput("favorites.ser");
            if (fis != null) {
                ObjectInputStream in = new ObjectInputStream(fis);
                brocabList = (ArrayList<Brocab>)in.readObject();
                in.close();
            }
            else {
                // File didn't exist, nothing to do right now
            }
        } catch (Throwable t) {
            handleError(t);
        }
        setListAdapter(new BrocabAdapter(this,android.R.layout.simple_list_item_1,brocabList));
    }

    public void handleError(Throwable e) {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("Uh, error bro");
        alertDialog.setMessage("There was a problem: " + e.toString());
        alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
                      // here you can add functions
                   }
        });
        alertDialog.show();
    }
    ...

Because of Serializable, all we need to do is create an ObjectInputStream from a FileInputStream and retrieve our ArrayList from it. Now, we aren't guaranteed that readObject will return an ArrayList, but because we know that's the only object we're ever putting into it we can ignore any warnings Eclipse gives you.

So go ahead and ru--WAIT. Almost forgot, we need to add it to our TabBar! Get into your Wayback Machine and add the following to BrocabTabActivity.java:

1
2
3
4
5
6
7
8
9
10
        ...
        TabSpec thirdTabSpec = tabHost.newTabSpec("tid3");
        thirdTabSpec.setIndicator("Favorites");
        thirdTabSpec.setContent(new Intent(this,FavoritesListActivity.class));
    
        tabHost.addTab(firstTabSpec);
        tabHost.addTab(secondTabSpec);
        tabHost.addTab(thirdTabSpec);
    }
}

NOW run it. If you hit the Favorites tab, you'll see...WOOPS! We actually need to build let users add things to the favorites now don't we?

Back in BrocabDetailActivity.java, let's take another look at that onClick function. Add a call to the function addToFavorites:

1
2
3
4
5
6
7
8
9
10
11
    ...
    @Override
    public void onClick(View v) {
        if (v == back) {
            finish();
        }
        else if (v == favorite) {
            addToFavorites();
        }
    }
}

addToFavorites is another long function, so I'll break it up and go one step at a time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    ...
    public void addToFavorites() {
        ArrayList<Brocab> brocabs = null;
        try {
            FileInputStream fis = openFileInput("favorites.ser");
            if (fis != null) {
                ObjectInputStream in = new ObjectInputStream(fis);
                brocabs = (ArrayList<Brocab>)in.readObject();
                in.close();
            }
            else {
                // File didn't exist
            }
        } catch (Throwable t) {
            handleError(t);
        }
        ... TO BE CONTINUED ...

This is pretty much identical to our code in FavoritesListActivity, so it's not much of a big deal. HOWEVER, you do need to add the handleError method found in FavoritesListActivity to the class.

Anyway, moving on:

1
2
3
4
5
6
7
8
9
        ...
        if (brocabs != null) {
            brocabs.add(brocab);
        }
        else {
            brocabs = new ArrayList<Brocab>();
            brocabs.add(brocab);
        }
        ... TO BE CONTINUED ...

If the ArrayList is null, then there were no previous favorites. Thus, we should create a new ArrayList. Either way, we add our current brocab to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
        ...
        try {
            FileOutputStream fos = openFileOutput("favorites.ser",0);
            if (fos != null) {
                ObjectOutputStream out = new ObjectOutputStream(fos);
                out.writeObject(brocabs);
                out.close();
                Toast.makeText(this, "Added to Favorites!",2000).show();
            }
            else {
                // :(
            }
        } catch (Throwable t) {
            handleError(t);
        }
    }

    public void handleError(Throwable e) {
        AlertDialog alertDialog = new AlertDialog.Builder(this).create();
        alertDialog.setTitle("Uh, error bro");
        alertDialog.setMessage("There was a problem: " + e.toString());
        alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
                      // here you can add functions
                   }
        });
        alertDialog.show();
    }
    ...

This is pretty much the serialization code in reverse, and it bears striking similarity. Give your app and run and watch it work :)

Part8screen_2

Let's recap our knowledge:
  • Making Java clases Serializable
  • Saving Serializable objects
  • Loading Serializable objects

Click to continue to Part 9: Preparing for the Market; or, "Wow, that's easy."

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 07 Mar 2011 09:31:47 -0800 Beginning Android for iOS Developers - Part 9: Preparing for the Market; or, "Wow, this is actually really easy" http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-9-p http://clayallsopp.posterous.com/building-an-android-app-from-scratch-part-9-p

Well, we've made a pretty neat little app, and we want to share it with the whole world! Unlike iOS apps, there are a few different methods of distributing Android apps. You could even distribute it yourself so long as your users allow apps from unknown sources to be installed (which may or may not be possible, depending on the carrier and handset).

The most widespread way of selling your app is through Google's Android Market. What's different about it from the App Store? Here are some key points:

  • No review process. Once you submit an app, it's ready for download (after it propagates through all the servers). This is good for the developer, but questionable for the end-user, as many crap or malicious apps are left unfiltered.
  • $25 fee. 75% cheaper than Apple's yearly fee.
  • Payments handled through Google Checkout. Instead of waiting for your bank and tax info to clear through Apple, payments are instantly sent through Google Checkout. You don't even need to provide an SSN unless your app makes over $1k a month.
  • Arbitrary pricing. Your app prices do not need to conform to pricing "tiers". Want to charge $1.29? Go for it. Want to charge $0.99 and 0.99 Euros? That's cool too.
  • You can (and should) include promo graphics. Apple only asks select developers for large-form and banner-style graphics for display in iTunes; the Market, on the other hand, will psuedo-randomly show these images throughout the Store.
  • DRM optional. Like the App Store, Android Market has piracy. Google has provided a DRM solution that you can enable, but at the time of writing it is deprecated and will disabled in the future.

How do we prepare our app? It's not a big deal. Make sure you've declared minimum and base SDK targets in your AndroidManifest.XML:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.clayallsopp.isawesome"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="3"
      android:targetSdkVersion="4" />
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

When you're ready, you can use either the Android command line tools or Eclipse's "Export Android Application" option to build your application. You'll need to setup a keystore and key, which Eclipse will walk you through. At the end of this (very quick) process, you'll have a shiny APK ready to upload to the Market (here).

And that's all he wrote. As my Dad always told me after school: "I hope you learned something!"

You can check out Brocabulary on the App Store and the Android Market. Feel free to browse my other work as well!

AND MOST IMPORTANTLY: follow me on twitter: @clayallsopp :)

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Wed, 02 Mar 2011 11:47:00 -0800 Protip: dragging files to "Open" dialogs in OS X http://clayallsopp.posterous.com/protip-dragging-files-to-open-dialogs-in-os-x http://clayallsopp.posterous.com/protip-dragging-files-to-open-dialogs-in-os-x

If you're opening a file using the normal Cocoa dialog, you can drag and drop files into that dialog to automatically jump to them. So if you already have your file open in another window, no digging around or searching necessary.

Upload

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Fri, 25 Feb 2011 13:36:00 -0800 Organizing your Downloads folder; or, building a personal assistant that doesn't smell like solder and sadness. http://clayallsopp.posterous.com/organizing-your-downloads-folder-or-building http://clayallsopp.posterous.com/organizing-your-downloads-folder-or-building

By default, web browsers on OS X like to stuff whatever porn files you save into the Downloads folder. It's a smart practice for the first week or so, until the folder is probably filled with hundreds of porn files. Go ahead, have fun trying to find that Sasha Grey very important PDF you downloaded last week: I'll be waiting. 

Eventually, the Downloads folder clutter will take its physical and emotional toll on you. Just like porn.

So what have I done about it? I'm ecstatic you asked. As they say, a clean folder is a clean mind (as well as the lesser known, a dirty....yeah).

tl;dr I set up a folder action that routes recently downloaded files to subfolders within the Downloads folder.

Here's what it ends up looking like. Pretty zen, right?

Downloads

So let's get to it. First, find your Downloads folder in Finder. Right click it and select "Folder Actions Setup"

Rightclick

You should be greeted by a pleasant popup with a number of default scripts. Choose "add - new item alert.scpt".

Setup

Check off "Enable Folder Actions" in the upper left corner!

Enable

Then select the script and click "Edit Script".

Edit_script
The AppleScript editor will appear. Here's the code you'll want to use (adjust the directories and paths as necessary):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
property Docs : "Macintosh HD:Users:<YOUR NAME>:Downloads:Docs"
property Music : "Macintosh HD:Users:<YOUR NAME>:Downloads:Music"
property Videos : "Macintosh HD:Users:<YOUR NAME>:Downloads:Videos"
property Images : "Macintosh HD:Users:clayallsopp:Downloads:Images"
property Profiles : "Macintosh HD:Users:clayallsopp:Downloads:iPhone:Profiles"

on adding folder items to thefolder after receiving theAddedItems
repeat with eachitem in theAddedItems
tell application "Finder"
if (name of eachitem ends with ".png") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".JPEG") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".gif") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".jpg") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".jpeg") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".PNG") then
move eachitem to folder Images
end if
if (name of eachitem ends with ".mov") then
move eachitem to folder Videos
end if
if (name of eachitem ends with ".avi") then
move eachitem to folder Videos
end if
if (name of eachitem ends with ".wma") then
move eachitem to folder Videos
end if
if (name of eachitem ends with ".m4v") then
move eachitem to folder Videos
end if
if (name of eachitem ends with ".mp4") then
move eachitem to folder Music
end if
if (name of eachitem ends with ".mp3") then
move eachitem to folder Music
end if
if (name of eachitem ends with ".wav") then
move eachitem to folder Music
end if
if (name of eachitem ends with ".wma") then
move eachitem to folder Music
end if
if (name of eachitem ends with ".pdf") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".doc") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".docx") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".pages") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".ppt") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".pptx") then
move eachitem to folder Docs
end if
if (name of eachitem ends with ".mobileprovision") then
move eachitem to folder Profiles
end if
end tell
end repeat
end adding folder items to

Feel free to filter by more criteria. Also note that you can move the files to any arbtirary path, including outside of the Downloads folder, so long as it's properly assigned to a variable.

Save the file and exit the Folder Actions setup.

It'll take about 3 to 5 seconds for Finder to register a newly added file, but you'll know it's done when it plays the familiar copy+paste sound.

And now, even if your mind is dirty, your Downloads folder will be clean.

EDIT: Justin pointed out in the comments that it might be handy to make this go ahead and sort all of your current items in Downloads. Add this bit of code between all of the property declarations and the "on adding..." part:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
property Downloads : "Macintosh HD:Users:clayallsopp:Downloads:"

on run
tell application "Finder"
set d to Downloads as alias
set folder_contents to every file in d
repeat with eachitem in folder_contents
if (name of eachitem ends with ".html") then
move eachitem to folder WebDev
end if
... etc etc
end repeat
end tell
end run

Make sure you declare the Downloads property with the colon at the end. 

Hit the "Run" button in the AppleScript editor and it'll go ahead and organize your folder.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Fri, 25 Feb 2011 12:50:58 -0800 Oh, hey there http://clayallsopp.posterous.com/oh-hey-there http://clayallsopp.posterous.com/oh-hey-there

Hey…it’s been awhile.

I-…I know I should have called, or texted, or poked or something, but I’ve been pretty busy lately. I mean I know that’s not an excuse, in fact it’s a reason I should have gotten back to you earlier. So can we agree to just move on and get to what really matters?

We have a lot to catch up on, don’t we?

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Thu, 12 Aug 2010 11:14:00 -0700 VolumeCamera http://clayallsopp.posterous.com/volumecamera http://clayallsopp.posterous.com/volumecamera

Inspired by TapTapTap’s infamous easter-egg, I decided to make a quick app where the camera is triggered by the volume buttons. No private APIs were used. This is not intended for the App Store, but merely as an experiment.

If you’re an iPhone developer, you can get it here.

If you’re not, you’ll need to find someone who is to send you a binary enabled for your phone.

The source also contains generic code for monitoring when the device volume was changed, which you can use as you see fit, found under VolumeDelegate.h and VolumeDelegate.m.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 26 Jul 2010 11:36:56 -0700 What Apple can learn from 37Signals http://clayallsopp.posterous.com/what-apple-can-learn-from-37signals http://clayallsopp.posterous.com/what-apple-can-learn-from-37signals

When something goes wrong, someone is going to tell the story. You’ll be better off if it’s you. Otherwise, you create an opportunity for rumors, hearsay, and false information to spread.

When I read that in 37Signal’s new book Rework, I immediately thought of Antennagate. Was the whole thing overblown? Of course. But did Apple handle it in the best possible way? Not really.

If your first piece of PR is an email like this, something isn’t right. Apple really didn’t control the story from the start, and it proceeded to get completely out of hand. Their amazing earnings were not doubt overshadowed on Wall Street by the collective panic that was Antennagate. Apple did, however, make a pretty good non-apology.

In my opinion, the antenna thing isn’t a big deal. Apple made a choice to go for better reception for the majority and risk problems for a minority of situations (if you have weak reception and if you’re holding it wrong and if you don’t have a case). I love my iPhone 4; it’s just a shame that I have to keep its sexy stainless steel curves hidden under a rubber/plastic bumper.

So, what is the end result of Antennagate? I was at an O'Charley’s the other day and the waiter noticed my phone. He said his buddy had one and it drops calls. He didn’t mention FaceTime, folders, multitasking, the gyroscope, or the LED flash. Just the dropped calls.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 26 Jul 2010 08:19:00 -0700 Christopher Nolan http://clayallsopp.posterous.com/christopher-nolan-0 http://clayallsopp.posterous.com/christopher-nolan-0

My most enjoyable movie-going experiences have always been going to a movie theater, sitting there, and the lights go down and a film comes on the screen that you don’t know everything about, and you don’t know every plot turn and every character movement that’s going to happen. I want to be surprised and entertained by a movie, so that’s what we’re trying to do for the audience.

Exactly how I felt during Inception, especially due to the fact that I was at a midnight screening. Nobody there knew what was coming, and thus the idea that movies are shared, discovered experiences was that much more palpable. At the end of the movie, the atmosphere was just electric.

Well, that may have been because everyone there was a huge nerd like me.

I think really, for me, the primary interest in dreams and in making this film is this notion that in your mind, while you’re asleep, you can create an entire world that you’re also experiencing without realizing that you’re doing that. I think that says a lot about the potential of the human mind, especially the creative potential. It’s something I find fascinating.

Thank god I’m not the only one who became obsessed with this after I saw the movie. It hit me when Cobb drew the diagram of the experience being “right in between creation and perception” (or something like that). Never before had I considered the fact that dreams, these unreal worlds which are filled with very real dialogue and detail, are being experienced at the exact same time as they are created. Isn’t that just nuts?

Anyway, I find Nolan and his work absolutely fascinating. After seeing Inception and Memento (very similar works, by the way), his Batman films seem so…tame. They lack the bold, important statements made about reality and experience one finds in the aforementioned films. They simply come off as really, really, really good action movies. Hope that doesn’t ruin Batman 3 for me.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Wed, 07 Jul 2010 09:24:00 -0700 The difference between American car manufacturers http://clayallsopp.posterous.com/-the-difference-between-american-car-manufact http://clayallsopp.posterous.com/-the-difference-between-american-car-manufact

In 2006, Tesla shows off its initial Roadster design.

2006 initial Roadster design - looks awesome

Two years later, it releases this:

Actual production Roadster - still looks awesome

In 2007, GM shows off its Volt concept.

2007 Chevy Volt Concept - looks awesome

Three years later, it releases this:

The actual 2010 Chevy Volt - looks disappointing

At a typical car company, [the design process] lasts about 12 months. At [Fisker Automotive] it’s two.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Wed, 07 Jul 2010 09:18:44 -0700 iPad and the HIG http://clayallsopp.posterous.com/ipad-and-the-hig http://clayallsopp.posterous.com/ipad-and-the-hig

Apple says not to do a lot of things in its HIG; however, if you actually do one of those things in your app, it won’t raise an exception. For some things, like toolbars on top versus on the bottom, that’s not the sort of thing to raise an exception. However, there are things you can do which violate the HIG and will appear to function normally…until you start playing around.

For instance, the HIG says not to put UISplitViewControllers inside of a UITabBar. Well, the first time I started to make a universal app I had not read this, and I assumed that I could just port the original hierarchy from my iPhone app to the iPad version. So I went about making a UITabBar filled with UISplitViewControllers and it all worked just fine. Then I started checking rotations…and things got a bit out of hand. For example, the popover view would vanish when I changed tabs after a rotation. This kind of behavior led me to believe, “Oh, maybe I didn’t retain something correctly.” I wasted a few hours tracking the view hierarchy and things just got plain weird. I googled around a bit until I found out that Apple says not to do that in its HIG. DOH.

I thought to myself, “That’s the last time that’ll happen.” Then, while porting the same app, I tried to show a modal view which was generated from a popover view. It worked fine if you started in landscape mode, but if you instantiated the modal view in portrait mode and rotated the device, it would magically disappear! It wasn’t being destroyed, just hidden in the view hierarchy and unassociated with the view controller that presented it. I wasted a few more hours tracing the lifecycle of the view, doing all kinds of crazy things to re-present the modal view as it was disappearing. I googled around until I read that Apple says not to present a modal view and a popover view at the same time in the HIG. All I had to do was add a line to dismiss the popover when the modal view was presented.

So what have I learned?

  1. Don’t violate the HIG.
  2. Don’t violate the HIG, especially with new UI elements. If Apple tells you not to do a view hierarchy in a certain way, it’s probably because that hierarchy is unstable and should not be used.
  3. Don’t put UISplitViewControllers inside of a UITabBar.
  4. Don’t show a modal view and a UIPopoverController view at the same time.
  5. Don’t violate the HIG.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 05 Jul 2010 16:15:42 -0700 Inertial Scrolling http://clayallsopp.posterous.com/inertial-scrolling http://clayallsopp.posterous.com/inertial-scrolling

And I gave it to one of our really brilliant UI guys. He got inertial scrolling working and some other things, and I thought, ‘my God, we can build a phone with this!’

Not surprising that inertial scrolling was the one thing that seems to stick out in Jobs' mind about early iPhone prototypes (or, at the least, UI design). To me, inertial scrolling is one of the key features about the iPhone that made it a fun experience from day one. I don’t think its importance has been championed enough.

From the SpringBoard flicking to table view scrolling, the “springy” action of the iPhone’s UI helps combat the lack of tactile feedback in the glass touchscreen. When you scroll beyond the limit of a web page or table, it appears as if the page or table is pulling back. It’s a nifty slight of hand that provides valuable information to our brains and tricks us into thinking that we’re physically manipulating something. It’s also fun to play with.

It’s also something that I miss whenever I use another touchscreen phone as a result of my three years with the iPhone. Apple probably has a patent or something on it, which is a shame because it’s one of those decisions where you think, “Well, why doesn’t everybody do this?” Yet, it’s one of the many subtle features that make the iPhone experience distinctly iPhone.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp
Mon, 05 Jul 2010 16:15:00 -0700 Cross-platform development - the 'Gapple' developers http://clayallsopp.posterous.com/cross-platform-development-the-gapple-develop http://clayallsopp.posterous.com/cross-platform-development-the-gapple-develop

It’s immediately apparent that single-platform development is the norm (with Apple holding the predictable edge)

While Apple does seem to retain more of its developers, it also interesting that a far greater proportion – five times greater, in fact – of Android developers are cross-platform developers. Why is this?

Unfortunately, the AppStoreHQ data doesn’t say anything about whether the developers' Android or iPhone apps came first, but I would wager that the decision to expand is triggered by money. Either they aren’t making enough and want to explore a new market, or they are making money and want to make even more. The former is probably more representative of Android-first developers (as seen in this IAmA).

While 78% of polled developers saw Apple’s iOS as having the “best near-term outlook” (vs 16% for Android), a majority of the same group (54%) picked Android as having the “best long-term outlook” (vs 40% for iOS).

Expected. Unless Apple gets off AT&T, Android will eventually have a far larger marketshare, guaranteed. But will that growth translate to a proportional increase in sales? Maybe. I bet that Android users (in the “long-term”) will buy less apps per phone than iPhone users simply because they don’t know about it – or don’t care.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1069762/profile.jpeg http://posterous.com/users/5BhyKAZH0bst Clay Allsopp clayallsopp Clay Allsopp