Sunday, 20 May 2012

Custom Spinner component with images

This post was inspried by a request I received via HackerBuddy, asking for help on creating a customized ListView in Android. The problem was how to show a different image on each row of the ListView. I had implemented something similar to this previously, except in my case it was a Spinner component (A Spinner is the Android equivalent of a drop-down menu selector). As the cases for ListView and Spinner are virtually the same, let's take a look at the Spinner.

Let's say we have an array of countries which we want to put into a spinner. A simple spinner containing text strings only can be created using the default ArrayAdapter provided by the Android SDK:

String[] countries = {...}; Spinner countrySpinner = new Spinner(context); ArrayAdapter adapter = new ArrayAdapter(context, android.R.layout.simple_spinner_item, countries); countrySpinner.setAdapter(adapter);

Now lets say we want have a flag icon representing each country, and we want to show the respective flag next to each country in the list. Because we now need two peices of information per country (the name and the flag), we first replace the "countries" string array with an array of Country objects. Next, we need a more complicated ArrayAdapter to deal with these Country objects, so let's extend ArrayAdapter to create a new CountrySpinnerArrayAdapter.

Country[] countries = {...}; Spinner countrySpinner = new Spinner(context); ArrayAdapter adapter = new CountrySpinnerArrayAdapter(context, android.R.layout.simple_spinner_item, countries); countrySpinner.setAdapter(adapter); class CountrySpinnerArrayAdapter extends ArrayAdapter { private List countries; public CountrySpinnerArrayAdapter(Context context, int resourceId, List countries) { super(context, resourceId, countries); this.countries = countries; } public View getView(int position, View convertView, ViewGroup parent) { TextView textView = (TextView) super.getView(position, convertView, parent); Country country = countries.get(position); textView.setText(country.getName()); textView.setCompoundDrawablesWithIntrinsicBounds(country.getFlagImage(), null, null, null); return textView; } public View getDropDownView(int position, View convertView, ViewGroup parent) { return getView(position, convertView, parent); } }

What's happening here is the adapter returns the required View for each item in the list. This View can be modified to meet our requirements, which in the case means setting an image to the left hand side of the text, using the textView.setCompoundDrawablesWithIntrinsicBounds(...) method.

To see this implemented in the wild, check out my Daylight World Map app.

Wednesday, 18 April 2012

Deploying on multiple markets with in-app purchases

Last year, Google introduced in-app purchases for apps for the Android Market. This provided developers with an alternative model for generating income from apps. Rather than having a two versions of an app (limited free and paid-for premium), developers could now release a single app, with premium content being unlocked via in-app purchases.

More recently, Amazon have also announced a similar in-app purchase system, with promising initial results.

The downside with this in-app purchase model comes when developers have products available on several different app markets. Google's in-app purchase system only works through their own Android Market. So, what to do if you want to leverage in-app purchases, but also want to make your app available on a number of markets?

Well, there are no doubt various possible solutions, but this is what I have come up with. The strategy is essentially:

1. Create a "basic" (free) version of the app - this is the version that is available on markets which don't support in-app purchases.
2. For each market with in-app purchases that you want to support, there will be a separate version of the app. This is less work than it sounds if you simply take the basic version and extend it with the extra features. Then use a common interface to manage the different in-app billing systems.

Here are two suggested interfaces to use for handling in-app billing: public interface BillingHelper { /* initialize the billing system */ public abstract void init(Activity activity, BillingListener listener); /* call when activity.onStart() */ public abstract void start(); /* call when activity.onResume() */ public abstract void resume(); /* call when activity.onDestroy() */ public abstract void destroy(); /* determine whether billing is available */ public abstract boolean isBillingAvailable(); /* determine if a product ID is currently owned */ public abstract boolean isOwned(String itemId); /* request the purchase of a product */ public abstract void purchase(String itemId, String payload); }
public interface BillingListener { public void doUpdate(); }
Then, these interfaces are implemented for each market which supports in-app billing. The key to making life as simple as possible here, is that the different versions of the app can use entirely the same code except for switching between which BillingHelper implementation they choose, and the native billing code for that market:

A typical implementation would work like this... The basic version of the app can query the BillingHelper interface to determine whether billing is available (via the isBillingAvailable method), and only make enable the option the purchase upgrades if this is the case. When the user is ready to purchase an upgrade, the purchase method is called, and the payment and billing are handled by the appropriate BillingHelper implementation and the billing SDK.

The BillingListener interface is used to detect when a purchase event has occured, so the app can take the required action, such as downloading/unlocking the paid-for levels.

With this approach, although it remains necessary to maintain different versions of the app for each market you want to target, in practise nearly all the code is maintained in the basic version of the app. The additional versions for the specific markets are constructed via build scripts, in my case using ant.

Well, that's the strategy I plan to use to build different versions of my apps for a variety of app stores. If you have an alternative approach or a suggestion for improvement, do let me know!

Update 1:
On closer inspection of the Amazon developer payments system, it seems they are totally not set up to deal efficiently with non-US developers! Apparently it is required to obtain a US taxpayer identification, then fill in some US tax forms, and at the end of all that, they make payments by sending US$ cheques out in the mail. Seriously, US$ cheques???

This policy makes Amazon currently unusable for selling products in my view. They should definately take a leaf out of Google's book when it comes to payments. As an international company I would have expected better from Amazon. Let's hope they sort this problem out soon!

Update 2:
Amazon have recently much improved their services for non-US developers. The Amazon app store is now available in multiple counties, and developers in a lot more countries now have the opportunity to get paid in their home currency via bank transfer. Better late than never!