Upload
harsh-tibrewal
View
87
Download
0
Embed Size (px)
DESCRIPTION
Android application sample
Citation preview
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 1/59
metagear.de
A Sort-Of Full-Fledged Android
Sample Application Walked-Through
In-Depth
August 12, 2012 Robert Sding
Show Table of Contents
1. Preface
While the sales volumes of PCs and feature cell phones stagnate, smartphone and, since recently, tablet PC
sales boom.
Looking at operation system distributions in the mobile sector, iOS and Android are prevalent, squeezing other
operation systems (like Symbian or Blackberry OS) out of the market (upcoming Windows Phone is still playing in
its small niche).
Although Apple has pioneered the mobile market with their iPhone and iPad, Android device sales are growing
much more rapidly in both relative and absolute terms. For smartphones, in year 2012, Android has a 59%
market share (iOS: 23%), and 145% growth per year (iOS: 89%).
Still being a niche segment as of today, tablet PC sales are estimated to rapidly grow as well. In this segment it
is forecasted that, while iOS will retain its leadership for several upcoming years, Android's relative growth is,
and will be, higher. See chapter Android Market Share for some resources on these issues.
The article at hand documents a sample application that puts the Android SDK (software development kit) to
work.
In contrast to just providing a Eureka! Hooray!, few-lines, blog posting that fakes its writer as an all-time
problem solver, this sample application implements a - well, not so real-world, complex, use case, which
needs to be implemented at any rate whether the Android APIs would fit or wouldn't. We've been trying to
make this application reproducible as a whole, not just lightening single aspects of it.
As this article does not start at a "Hello World" entry level, interested readers are supposed to already have
gone through introductory Android articles or tutorials (seriously). As for the server-side implementation, a basic
knowledge of Servlet and Spring Framework technologies ought to be helpful, however that's not mandantory.
Any feedback is appreciated and may be directed to [email protected].
If you don't want to install the sample application, straightly skip to the Sample Application section.
2. Prerequisites
Unfortunately, there are more than two or three steps involved in order to get the sample application
running, so this cannot be considered a Hello World sample.
Nevertheless, I've tested the deployment procedure, thus feel free to contact me in case of incidents after
"aunt Google" hasn't brought you forward.
The application has been developed on
Eclipse 3.6 (Helios) for Java EE Developers
with the Android Development Tools (ADT) installed.
The sample application makes use of Android's Location-Based Services (LBS) and Google Maps. Due to
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 2/59
officially unresolved bugs related to using these with the Android Emulator in Android 2.2 and 2.3 (i.e., this one
), the sample application uses the
Android SDK 2.1, API level 7, along with the corresponding
Google APIs, API level 7.
This SDK version can be installed from within the Android SDK and AVD Manager, which is included with the
Android SDK :
To run the server application,
Apache Tomcat
has been used (both 6.0 and 7.0 work).
2.1. Downloading, Importing, and Running the Sample
Application
Download the zipped project sources, which comprise of the following Eclipse projects:
mg-library-android a library of custom Android views, activities, and Android-specific utilities, which
can be re-used in any Android project
mg-pizzastore-android the Android client (our main application)
mg-pizzastore-android-test automated tests for the Android client
mg-pizzastore-server the server-side application
mg-pizzastore-shared a library of domain / model classes, which are used by both mg-pizzastore-
android and mg-pizzastore-server
Make sure that you've set up the Android SDK in the Eclipse Preferences. As mentioned in the previous
chapter, the SDK must contain the Android SDK 2.1, API level 7 version:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 3/59
To import the aforementioned projects into Eclipse, click File --> Import --> Existing Projects into
Workspace, and choose the mg-pizzastore-android.zip archive file previously downloaded.
Assign a server to the mg-pizzastore-server project by completing the following steps:
In Eclipse, open the Servers view by selecting from the main menu: Window --> Show View -->
Servers.
In the Servers view, right-click, and select New --> Server.
Choose an existing Apache Tomcat 6.0 oder 7.0 installation, and in a subsequent dialog,
assign the mg-pizzastore-server project.
At this stage, there shouldn't be any compile errors in the projects, and we're heading towards running the
sample application.
Fix the mg-pizzastore-android project properties by right-clicking the project, Properties --> Android. In
the field group Library, remove the referenced, but invalid mg-library-android project ...
... and newly add it:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 4/59
Please excuse that annoyance. I've looked at the settings files but haven't found a better solution yet.
To start the MapViewActivity, you need to obtain a custom Google Maps API Key . Next, edit the
corresponding file mg-pizzastore-android/res/layout/map_linearlayout.xml accordingly. See chapter
MapViewActivity: General Setup for additional information.
It hasn't been hard to set up the Maps API Key, has it? ;-)
To finally run the sample application from within Eclipse, first start the mg-pizzastore-server project by
right-clicking it and selecting Run as --> Run on Server.
Next, run mg-pizzastore-android by right-clicking its project root and choosing Run as --> Android
Application (select a matching Android Virtual Device (AVD) in the Eclipse Run Configurations).
To run the tests, right-click the mg-pizzastore-android-test project in Eclipse, and select Run As -->
Android JUnit Test.
2.2. Tip: Getting the android.jar Source Code
Unfortunately, the Android SDK doesn't ship with the
source code for android.jar, and you'll probably
stumble upon that.
Google'ing around, everyone recommends to checkout
the source code from the Git source code management
system at sources.android.com . However, with 2.6
GB in download size and several installation steps
involved, that may be overkill. Noone seems to realize
that there's a lightweight alternative:
Install the adt-addons Eclipse Plugin .
Theoretically, this should be sufficient to view the
Android class libraries' source code in Eclipse; however,
it did not work for me. In this case find the matching
sources.zip in
$ECLIPSE_HOME/plugins/com.android.ide.eclipse.source*.
These sources may be not perfectly accurate when they don't exactly match your Android SDK revision.
3. The Sample Application
The sample application represents an imaginary pizza store, where the user can select, and order, from a list
of available pizzas. Additionally they need to fill in their address data. As a plus, the user can edit their location
in a Google Map.
At application start time, the list of pizzas is retrieved from the remote server, and at shopping cart checkout
time, the order gets submitted to that server.
3.1. Data Model
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 5/59
3.1.1. Domain Classes
The application's domain respectively, model classes are located at the mg-pizzastore-shared project.
The following class diagram visualizes the entities and their relationships:
The sample application's first, and
main, screen is the Pizzas List,
which lists a set of Pizzas along with
their properties.
When the user selects a Pizza into
their Shopping Cart, that shopping
cart displays a set of PizzaLineItems,
where a PizzaLineItem while
realizing all Pizza properties has an
additional property quantity.
In order to finally checkout their
shopping cart, the user needs to fill
in a User Data Form, which
corresponds to a UserData instance.
An Address contains user data that are required for Location-Based Services; such data include street, city,
and a Country. UserData contains an Address plus some additional data such as the user's name and phone
number.
When the user finally checks out their shopping cart, an Order will be submitted to the server.
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model
3.1.2. Using the JSON ObjectMapper with the Model Objects
In the sample application, the Jackson ObjectMapper is used to marshal, resp., unmarshal, (our domain class)
instances from, resp., to, JSON -encoded strings. We're using it when Dealing with Android's
SharedPreferences as well as with the backend communications (HttpGetPizzasListTask and HttpPostTask)
toward the server.
The ObjectMapper requires some custom settings regarding the class' constructor and transient properties, as
shown on our Address model object:
@JsonIgnoreProperties({ "valid" })
public class Address implements Serializable {
// ...
public Address(String street, String zipCode, String city, Country country) {
street = street;
zipCode = zipCode;
city = city;
country = country;
}
public Address() {
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
street = street;
}
// ...
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 6/59
@JsonIgnore
public boolean isValid() {
if (StringUtils.isEmpty(street) || StringUtils.isEmpty(zipCode)
|| StringUtils.isEmpty(city) || !country.isValid()) {
return false;
} else {
return true;
}
}
}
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model.Address
First-off, the mapper requires either a nillary (default) constructor or (not used in the sample application) the
@JsonCreator annotation on another, public, parametrized, constructor.
Additionally, our boolean isValid() method is transient and evaluated on-the-fly, at runtime (accordingly,
there is no matching setter). - Therefore, we're instructing the ObjectMapper to ignore the getter property
(using the @JsonIgnore annotation) and not to expect a corresponding setter property (using the
@JsonIgnoreProperties({ "valid" }) annotation).
Consult the Jackson Annotations ApiDoc in case you'd need further information.
3.2. Spring-Based Backend, and Remoting
3.2.1. Overview
As for the sample app's client/server connection, the HTTP transport protocol has been favored over other
protocols (like using RMI, for instance) in order to avoid possible firewall restrictions. From there, JSON has
been preferred over XML (or binary contents, like using Hessian ), and REST over XML Web Services,
because of performance reasons and ease of implementation with Android.
Spring Android provides a
client-side API for JSON
(and, alternatively, XML-)
based REST operations and
Android integration. At the
server side, Spring Android is
"simply" leveraging Spring's
Web MVC Framework APIs.
Just like other Spring
Framework domains, Spring
Android provides a set of templates, elegantly figuring out, and implementing, best practices.
For related concepts, see chapter Remoting of my previously written article on the Spring Framework .
You may also want to read chapter Spring MVC of the same article, although Spring MVC in portions has
partially evolved from Spring 2.5 to 3.0.
The sample application's MainController residing in project mg-pizzastore-server is basically implemented
as follows:
@Controller
@RequestMapping("/*")
public class MainController {
@RequestMapping(
value = "fetchpizzas",
method = RequestMethod.GET,
headers = "Accept=application/json")
public @ResponseBody
List fetchPizzasJson() {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 7/59
return getAllPizzas();
}
@RequestMapping(
value = "postorder",
method = RequestMethod.POST,
headers = "Content-Type=application/json")
public @ResponseBody
String postOrderJson(@RequestBody Order order) {
return "OK";
}
}
For corresponding code see: mg-pizzastore-server/de.metagear.pizzastore.service.MainController
We're configuring the server to listen to HTTP requests on the (exemplary) URLs http://localhost:8080/mg-
pizzastore-server/fetchpizzas and http://localhost:8080/mg-pizzastore-server/postorder in case the
application/json content type is supported, respectively, provided.
From the Emulator's device point of view, the server address ain't localhost or 127.0.0.1 (that refer to its
own loopback device) but 10.0.2.2.
As for the @Controller, @RequestMapping, and @ResponseCode annotations, see the Spring MVC reference
documentation. There is some configuration involved in WEB-INF/web.xml as well as in a couple of Spring
bean configuration files under the folder WEB-INF. These settings are both documented in my previous Spring
article and in the current Spring MVC reference documentation, both linked above.
There is no database interaction involved. Instead, the List getAllPizzas() method simply returns a
hard-coded list.
3.2.2. JSON Encoding
Somewhat "automagically", the server serves its response in a JSON-encoded format.
In the above code snippet you can see that the server looks for the client-side HTTP headers
Accept=application/json on HTTP GET requests, and Content-Type=application/json on HTTP POST requests.
If both classes, org.codehaus.jackson.map.ObjectMapper and org.codehaus.jackson.JsonGenerator, are on the
classpath, the Spring Framework will identify the ObjectMapper to perform object/JSON marshaling as Spring's
MappingJacksonHttpMessageConverter has registered for the application/json MediaType and does employ that
JSON ObjectMapper.
We'll implement a custom MappingJacksonHttpMessageConverter later (in chapter Decoding the GZIP'ed
Response), however you don't really need to know about which Spring or JSON classes are involved, exactly.
(The Spring reference documentation itself doesn't even mention these internal matters.) Just make sure
that jackson-core-asl-x.x.x.jar and jackson-mapper-asl-x.x.x.jar are on the classpath.
The sample's application's remoting techniques have been derived, and inspired, by the spring-android-
showcase project of the Spring Mobile Samples .
While mouse-clicking Windows guys might hate SpringSource for forcing them into an extra command line tool
(Git SCM ), the samples are to be checked out from the Source Code Management System (SCM) by issuing
the following command:
git clone git://git.springsource.org/spring-mobile/samples.git spring-mobile-samples
You'll also want to check out the Spring Android source code:
git clone --recursive git://git.springsource.org/spring-mobile/spring-android.git
On Ubuntu, git can be installed by simply issueing sudo apt-get install git at the command line. On
Windows, please consult the installation instructions at the Git Website .
3.2.3. GZIP Compression
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 8/59
Large HTTP downloads on mobile devices may be costly in measures of limited data tariffs, network speed,
and battery life. Thus we're compressing the HTTP response body before sending it to the client, using GZIP-
compression.
To do so, we're implementing a custom servlet Filter:
public class GzipFilter implements Filter {
// ...
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
if ((request instanceof HttpServletRequest)) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String str = httpRequest.getHeader("accept-encoding");
if ((str != null) && (str.indexOf("gzip") != -1)) {
GzipResponseWrapper wrapper = new GzipResponseWrapper(
httpResponse);
filterChain.doFilter(request, wrapper);
wrapper.finishResponse();
} else {
filterChain.doFilter(request, response);
}
}
}
// ...
}
For corresponding code see: mg-pizzastore-server/de.metagear.util.servlet.GzipFilter
In WEB-INF/web.xml, this GzipFilter is configured to intercept responses of Spring's DispatcherServlet
(which, itself, is configured to handle all requests):
appServlet
org.springframework.web.servlet.DispatcherServlet
...
appServlet
/
gzipFilter
de.metagear.util.servlet.GzipFilter
gzipFilter
appServlet
For corresponding code see: mg-pizzastore-server/webapp/WEB-INF/web.xml
The GzipFilter employs the custom GzipResponseWrapper, which decorates the HttpServletResponse to output a
custom GzipResponseStream, which in turn uses the Java SE standard GZIPOutputStream library to encode
HttpServletResponse's ServletOutputStream.
See this article for a more in-depth discussion on this GZIP filter.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 9/59
Tomcat itself provides configuration options on GZIP compression, although these might not work with
Tomcat's mod_jk . After all (and after the time of writing), using the PJL Compressing Filter might be
preferred over what I'd chosen for this sample application. Looking at the source code, obviously, the PJL
Filter has been written with expert knowledge and, observably, with loads of love and care.
3.3. Android Client
Hey, finally the fun part is soon to come!
There are even screenshots to go! ;-)
3.3.1. Retrieving the Pizzas List
3.3.1.1. Overview
See the following class diagram for an overview of the main classes and interfaces involved:
Retrieving the Pizzas list, and ordering a Pizza, are two tasks of class PizzasListActivity, which is shown in
the center of the above diagram. These two tasks are accomplished by the classes HttpGetPizzasTask and
HttpPostTask (shown at the diagram's bottom), which both are components (has-a relation) of
HttpGetPizzasTask. The latter implements two certain interfaces that the task classes expect in order to be
able to call back certain interface methods when the tasks are completed, successfully.
The upper part of the class diagram looks more complex than it is. Basically, class AsyncActivity (which can be
re-used in Android Activitys as well as in ListActivitys) shows a progress dialog before the task starts and
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 10/59
dismisses the dialog after it completes.
In case a task fails, interface AsyncActivity defines method onAsyncTaskFailed(), which needs to be
implemented by our main PizzasListActivity ( the diagram is inaccurate in not displaying the aforementioned
method).
When the Android client application initially loads, the main Activity, PizzasListActivity, initializes the
download of an up-to-date List:
Several techniques and frameworks are involved.
The basic HTTP communication is accomplished by Spring's
RestTemplate , which is used by the Spring Android project.
The RestTemplate is configured to send a client-side HTTP GET
header: Accept=application/json. As mentioned in chapter JSON
Encoding, this header will be picked up at the server side as a
command to deliver JSON-encoded contents. Just like at the server
side, the Jackson JSON libraries need to be on the classpath to let
Spring Android decode JSON automatically.
From there, as the server-side response is GZIP-compressed, we're
extending Spring Android's MappingJacksonHttpMessageConverter to
decompress the server response. We're also extending Spring
Android's RestTemplate to be aware of our custom GZIP-enabled
HttpMessageConverter.
Not yet finally, the download task needs to be decoupled from the
main user interface (UI) thread into an independant thread that does
not block the UI. The Spring Android samples provide exemplary
classes to use the RestTemplate within an asynchronous Android
AsyncTask , which does threading by leveraging the
java.util.concurrent framework.
And finally, when the download thread returns, it needs to update
the state of the main UI thread. This is accomplished by using an
Android Handler .
As you can see, this is fairly complex. In the following chapters, each scope is discussed individually.
3.3.1.2. HttpGetPizzasListTask
The sample application's main Activity is PizzasListActivity, whose Activity.onCreate(Bundle) gets called
when it is created the first time. That's from where (through an intermediate method) new
HttpGetPizzasListTask(this, uri, 0).execute() gets called.
HttpGetPizzasListTask a component of PizzasListActivity extends Android's AsyncTask (take a brief look at
the API and APIDocs ), whose class structure the following screenshot shows:
Its main method is execute(Params...), which - simplified calls the following methods, one after the other:
onPreExecute()
doInBackground(Params...) (which does its work in a separate, decoupled, thread)
onPostExecute(Result)
So, as mentioned in the chapter above, this task does not block the main UI thread. For details see below.
We've already mentioned Spring Android , its RestTemplate for REST/HTTP-based communications, and the
corresponding Spring Mobile Samples .
The latter's DownloadStatesTask extends Android's AsyncTask and leverages Spring Android's RestTemplate in a
separate thread.
Basically, it is defined as follows, implementing the aforementioned three basic methods:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 11/59
private class DownloadStatesTask extends AsyncTask {
@Override
protected void onPreExecute() {
// before the network request begins, show a progress indicator
showLoadingProgressDialog();
}
@Override
protected List doInBackground(Void... params) {
try {
...
// Perform the HTTP GET request
ResponseEntity responseEntity = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, State[].class);
// convert the array to a list and return it
return Arrays.asList(responseEntity.getBody());
}
catch(Exception e) {
Log.e(TAG, e.getMessage(), e);
}
return null;
}
@Override
protected void onPostExecute(List result) {
// hide the progress indicator when the network request is complete
dismissProgressDialog();
// return the list of states
refreshStates(result);
}
}
}
For corresponding code see: DownloadStatesTask
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 12/59
3.3.1.3. Implementing a ProgressDialog
In the above code snippet (which stems from an Spring Android sample), the
sample DownloadStatesTask is an inner class of an Android Activity
implementation, on which the task calls the methods showLoadingProgressDialog()
and dismissProgressDialog().
In contrast to that, our own HttpGetPizzasListTask is a self-contained class that
calls these methods on an Activity that has been passed to its constructor (from where it is stored in an
instance variable). This Activity therefore needs to implement our custom interface AsyncActivity, which is
defined as follows:
public interface AsyncActivity {
void showLoadingProgressDialog();
void dismissProgressDialog();
// ...
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.async.AsyncActivity
Doing so, our pizza-downloading PizzasListActivity extends AbstractAsyncListActivity, which is shown below:
public abstract class AbstractAsyncListActivity extends ListActivity implements
AsyncActivity {
private Context context;
private ProgressDialog progressDialog;
public AbstractAsyncListActivity(Context context) {
context = context;
}
@Override
public void showLoadingProgressDialog() {
progressDialog = ProgressDialog.show(context,
context.getString(R.string.loadingProgressDialog_title),
context.getString(R.string.loadingProgressDialog_text),
true);
}
@Override
public void dismissProgressDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
}
}
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.async.AbstractAsyncListActivity
3.3.1.4. HTTP GET, and JSON Decoding, and Using the RestTemplate
In HttpGetPizzasListTask, the doInBackground(..) method is defined as follows:
@Override
protected List doInBackground(Void... params) {
try {
HttpHeaders requestHeaders = new HttpHeaders();
List acceptableMediaTypes = new ArrayList();
acceptableMediaTypes.add(MediaType.APPLICATION_JSON);
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 13/59
requestHeaders.setAccept(acceptableMediaTypes);
HttpEntity requestEntity = new HttpEntity(requestHeaders);
RestTemplate restTemplate = new GzipJsonRestTemplate();
ResponseEntity responseEntity = restTemplate.exchange(uri,
HttpMethod.GET, requestEntity, Pizza[].class);
return Arrays.asList(responseEntity.getBody());
} catch (Exception e) {
...
return null;
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.task.HttpGetPizzasListTask
Basically, we just add an application/json header and, next, call RestTemplate.exchange(..).
Android's AsyncTask, from which our HttpGetPizzasListTask inherits, calls the doInBackground(..) method in a
separate thread and, next, makes the results available in onPostExecute(..).
Don't get confused because of the GzipJsonRestTemplate in the above code snippet - it's a custom subclass
of the standard RestTemplate, which is covered in the following chapter.
As already mentioned in chapter JSON Encoding (at the Server Side), the Jackson Core and Jackson Mapper
JAR files need to be on the classpath. The RestTemplate's infrastructure will automatically detect them and
use them to decode the JSON response.
3.3.1.5. Decoding the GZIP'ed Response
Spring's RestTemplate internally prepares a Lists. If the aforementioned Jackson JAR files
are on the classpath, the RestTemplate also adds a MappingJacksonHttpMessageConverter to that list.
These HttpMessageConverters register for certain Java classes and Spring MediaTypes (like application/json) via
their methods canRead(..) and canWrite(..). If the RestTemplate finds a matching converter, that one will be
selected to perform the unmarshaling, resp., marshaling.
In the sample application, GzipEnabledMappingJacksonHttpMessageConverter extends
MappingJacksonHttpMessageConverter to additionally decompress the InputStream that gets returned by the
RestTemplate:
public class GzipEnabledMappingJacksonHttpMessageConverter extends
MappingJacksonHttpMessageConverter {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected Object readInternal(Class clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
if (isGzipEncoded(inputMessage)) {
InputStream inputStream = new GZIPInputStream(
inputMessage.getBody());
return objectMapper.readValue(inputStream, getJavaType(clazz));
} else {
return super.readInternal(clazz, inputMessage);
}
}
private boolean isGzipEncoded(HttpInputMessage inputMessage) {
HttpHeaders headers = inputMessage.getHeaders();
if (headers != null) {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 14/59
List contentEncodings = headers.get("Content-Encoding");
if (contentEncodings != null) {
for (String contentEncoding : contentEncodings) {
if (contentEncoding != null
&& contentEncoding.toLowerCase().contains("gzip")) {
return true;
}
}
}
}
return false;
}
}
For corresponding code see: mg-library-
android/de.metagear.spring.web.GzipEnabledMappingJacksonHttpMessageConverter
We're simply overwriting readInternal(..), where we're decorating the RestTemplate's InputStream by a
java.util.zip.GZIPInputStream. Just like Spring's MappingJacksonHttpMessageConverter does, we're then letting
Jackson's ObjectMapper process the stream.
It's not possible by design to assign a HttpMessageConverter to the RestTemplate, thus we overwrite the latter:
public class GzipJsonRestTemplate extends RestTemplate {
protected List>();
allMessageConverters
.add(new GzipEnabledMappingJacksonHttpMessageConverter());
allMessageConverters.addAll(super.getMessageConverters());
}
@Override
public List
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 15/59
request.getHeaders().add("Accept-Encoding", "gzip,deflate");
}
// ...
}
}
For corresponding code see: mg-library-android/de.metagear.spring.web.GzipJsonRestTemplate
Basically, we're adding our GzipEnabledMappingJacksonHttpMessageConverter to the top of the
List
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 16/59
}
};
Message message = Message.obtain(asyncTaskHandler, callback);
message.sendToTarget();
}
@Override
public void onHttpGetTaskSucceeded(int requestCode, List pizzas) {
setListAdapter(pizzas); // this does not require employing a Handler
// for some reason
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
There are further ways to use Handlers and Messages, but I found this way to be most unobtrusive. For
more information on using Handlers see the article Creating Dialogs (search and find "Example
ProgressDialog with a second thread") and the Handler ApiDoc .
3.3.2. Excursus: Android Activity Lifecycle
Activities are the main building blocks of Android applications as they are for our sample application.
While this article does not discuss any details on that, it's most important for developers to be aware of the
Activity Lifecycle (so if you're unclear about that, you should read the linked document, at any rate).
For instance, in the sample application, the onPause(..) method is used to stop a LocationManager from
permanently requesting updates, the onPrepareOptionsMenu(..) method is used to enable or disable MenuItems
depending on the Activity's current state.
For lifecycle methods related to Menus see chapter Implementing Option Menus.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 17/59
3.3.3. PizzasListActivity: Faciliating Selecting From a List of Items
3.3.3.1. Displaying the Pizzas List
PizzasListActivity extends ListActivity, which displays a list of Views when a ListAdapter is assigned, which
provides a custom View for each list item.
After the List has been retrieved from the server (see chapter Retrieving the Pizzas List), such an
adapter gets assigned to the ListActivity:
setListAdapter(new PizzaViewAdapter(this, pizzas));
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 18/59
3.3.3.1.1. PizzaViewAdapter
The PizzaViewAdapter is defined as in the following code snippet (simplified for a start see chapter Caching
Views):
public class PizzaViewAdapter extends BaseAdapter {
private LayoutInflater inflater;
private Context context;
private List pizzas;
public PizzaViewAdapter(Context context, List pizzas) {
this.context = context;
this.pizzas = pizzas;
this.inflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return pizzas.size();
}
@Override
public Pizza getItem(int position) {
return pizzas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 19/59
convertView = inflater.inflate(
R.layout.pizzaslist_view_tablelayout, null);
}
customizeView(position, convertView);
return convertView;
}
protected void customizeView(int position, View view) {
Pizza pizza = getItem(position);
TextView nameView = (TextView) view.findViewById(R.id.pizzasList_name);
nameView.setText(pizza.getName());
// ...
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.PizzaViewAdapter
We're overriding Adapter.getView(..) to provide the ListActivity with a custom view for each list item. That
View is inflated by a LayoutInflater from the given XML layout. In our customizeView(..) method we assign
the current Pizza's properties to TextViews and other View widgets.
The customizeView(..) method has been externalized from the getView(..) method in order to be
potentially overwritten by subclasses (e.g. the PizzaLineItemViewAdapter of our shopping cart, which
additionally displays the pizza line item's user-selected quantity, a remove pizza from list button, etc.).
3.3.3.1.2. Root XML Layout
The following code snippet shows the PizzasListActivity's root XML:
The ListView's and TextView's android:ids are predefined by the Android framework and are mandantory when
used with Android ListActivitys.
The ListView will be used to hold the list item Views discussed in the next chapter. The TextView will be
displayed only if the list is empty.
3.3.3.1.3. View XML Layout
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 20/59
The XML layout for each (Pizza) view is defined as follows:
For corresponding code see: mg-pizzastore-android/res/layout/pizzaslist_view_tablelayout.xml
There is a TableLayout with two TableRows. The second table row contains fewer views than the first one, so
we're assigning an android:layout_span (that would translate to colspan in HTML tables).
android:stretchColumns takes a comma-separated list of zero-based column indexes (or, alternatively, a * to
stretch all columns). These columns will be stretched if the TableLayout gets wider than its cells require.
Analogously, there is the android:shrinkColumns property.
3.3.3.1.4. Caching Views
Getting back to our PizzaViewAdapter.getView(..) method discussed above, that method has been simplified in
the above code snippet for clarity.
Actually, it caches each View's child views (i.e., TextViews) in order to avoid unnessecary calls to
View.findViewById(int), by applying the ViewHolder pattern .
3.3.3.2. Selecting a Pizza into the Shopping Cart
In the PizzasListActivity's pizzas list, a pizza can be selected by clicking a list item. This functionality is
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 21/59
realized by assigning an AdapterView.OnItemClickListener to a ListActivity's ListView:
protected void preparePizzaClick() {
getListView().setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View view,
int position, long id) {
Pizza pizza = PizzasListActivity.getListAdapter()
.getItem(position);
appState.getOrder().add(pizza);
showPizzaSelectedDialog(pizza);
}
});
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
From within that overwritten onItemClick(..) method, we access the instance variable appState (of type
ApplicationState) and, next, implement a custom AlertDialog. Both tasks are discussed in the following two
chapters.
3.3.3.3. Excursus: Maintaining Global Application State
In chapter Data Model, we've discussed that our main model consists of an Order, which holds a list of
PizzaLineItems, plus a UserData instance along with its Address.
This model is used queried and
edited throughout most of our
application's activities, representing
the application's global state. This
state is held by our ApplicationState
class, extending
android.app.Application:
public class ApplicationState extends Application {
private Order order = new Order();
public void setOrder(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
// ...
}
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.ApplicationState
This Application is set up in AndroidManifest.xml:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 22/59
package="de.metagear.pizzastore"
... >
...
...
For corresponding code see: mg-pizzastore-android/AndroidManifest.xml
This Application can be accessed from all activities (but also services, etc.) for instance, by the following
code:
ApplicationState appState = (ApplicationState) getApplication();
appState.getOrder().add(pizza);
3.3.3.4. Excursus: Creating Custom UI Messages
3.3.3.4.1. Creating an AlertDialog
From the OnItemClickListener.onItemClick(..) method, which has
been discussed in chapter Selecting a Pizza into the Shopping Cart,
we're opening an OK/Cancel AlertDialog:
protected void showPizzaSelectedDialog(Pizza pizza) {
final AlertDialog alertDialog = new AlertDialog.Builder(
PizzasListActivity.this).create();
alertDialog.setTitle(getResources().getString(
R.string.pizzasList_pizzaSelectedTitle, pizza.getName()));
alertDialog.setMessage(getResources().getString(
R.string.pizzasList_pizzaSelectedMsg));
alertDialog.setIcon(R.drawable.cart);
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, getResources()
.getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
PizzasListActivity.showPizzasCartListActivity();
}
});
alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources()
.getString(R.string.no), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
alertDialog.cancel();
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 23/59
Toast.makeText(PizzasListActivity.this,
R.string.pizzasList_pizzaAdded, Toast.LENGTH_LONG)
.show();
}
});
alertDialog.show();
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.4.2. Creating a Toast Message
When the user clicks "OK", the application displays the
PizzasCartListActivity screen. Otherwise, if they click "Cancel",
there's just a concise message popping up, a Toast , telling, "Pizza
Added":
Toast.makeText(PizzasListActivity.this,
R.string.pizzasList_pizzaAdded, Toast.LENGTH_LONG).show();
3.3.3.5. Implementing Option Menus
The Activity lifecycle methods related to Menus are:
boolean onCreateOptionsMenu(Menu)
boolean onPrepareOptionsMenu(Menu), and
boolean onOptionsItemSelected(MenuItem)
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 24/59
3.3.3.5.1. Creating MenuItems
In boolean onCreateOptionsMenu(Menu), we're creating a set of MenuItems:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menuItemsMap = new HashMap();
menuItemsMap.put(
R.string.pizzasCart_pizzasList,
menu.add(R.string.pizzasCart_pizzasList).setIcon(
R.drawable.script_edit));
menuItemsMap.put(
R.string.pizzasList_viewShoppingCart,
menu.add(R.string.pizzasList_viewShoppingCart).setIcon(
R.drawable.cart));
// ...
return true;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
We're adding these MenuItems to the instance variable protected Map menuItemsMap
because subclasses (in this case PizzasCartListActivity) need to access and manipulate the entries. That
way, we can re-use our MenuItems.
3.3.3.5.2. Manipulating MenuItems Depending on the Current State
boolean onPrepareOptionsMenu(Menu) is the method that is called each time just before a Menu is about to be (re-
)drawn. This is the place to manipulate MenuItems to correspond to the current Activity state:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 25/59
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
setMenuItemState(R.string.pizzasCart_pizzasList, false, false);
setMenuItemState(R.string.pizzasList_viewShoppingCart, true,
!isShoppingCartEmpty());
// ...
return true;
}
protected void setMenuItemState(int itemTitleResID, boolean visible,
boolean enabled) {
MenuItem item = menuItemsMap.get(itemTitleResID);
item.setEnabled(enabled);
item.setVisible(visible);
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.5.3. Evaluating MenuItem Clicks
We're using boolean onOptionsItemSelected(MenuItem item) to react
on MenuItem clicks:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getTitle().equals(getString(R.string.pizzasList_viewUserData))) {
showUserDataActivity();
} else if (item.getTitle().equals(
getString(R.string.pizzasCart_pizzasList))) {
showPizzasListActivity();
} else if ...
}
return true;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.6. Starting Other Activities
When the user clicks the Show Shopping Cart MenuItem, the PizzasCartListActivity is started:
protected void showPizzasCartListActivity() {
Intent intent = new Intent(this, PizzasCartListActivity.class);
startActivity(intent);
}
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 26/59
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
See chapters Passing Data to an Activity and Passing Result Data Back to the Calling Activity for
corresponding, expanded, information.
3.3.3.7. Re-Using Layout Styles
Attributes that are to be commonly re-used in XML layout files can be defined in central resource files. By
convention, the default file name is styles.xml, however that's not mandantory.
The following code snippet shows the sample application's styles.xml:
@drawable/customshape
fill_parent
wrap_content
1
5dip
18sp
bold|italic
10dip
wrap_content
0dip
wrap_content
1
true
50
For corresponding code see: mg-pizzastore-android/res/values/styles.xml
The formField.text style inherits its attributes from the formField style, inducted by the dot-style naming
convention.
In XML layout files these styles are referenced by using the style attribute, for instance:
For further information see the article Applying Styles and Themes .
3.3.3.8. Drawing a View with Rounded Corners
You're surely wondering how this design pope managed to draw such cool rounded edges on that "Pizzas
List" TextView. ;-) Well, here it is:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 27/59
android:shape="rectangle">
For corresponding code see: mg-pizzastore-android/res/drawable/customshape.xml
This drawable is referenced in PizzasListActivity's main layout view definition ...
...
...
For corresponding code see: mg-pizzastore-android/res/layout/pizzaslist_linearlayout.xml
... which in turn references the corresponding style attribute:
@drawable/customshape
...
...
For corresponding code see: mg-pizzastore-android/res/values/styles.xml
Unfortunately, Android XML Drawables are not really documented at all; nevertheless, there's a third party's
documentation .
Apropos visual design and best practices, we'd need to note that our sample application uses icons that
don't comply with Android's Icon Design Guidelines .
3.3.4. PizzasCartListActivity: Faciliating Reviewing The User'sShopping Cart
The pizzas cart activity lists the pizzas that the user has selected into their shopping cart (represented by
PizzaLineItems: Pizzas plus their respective user-selected quantities).
3.3.4.1. Inheriting from PizzasListActivity
As PizzasCartListActivity shares most of its functionality with PizzasListActivity, it extends the latter, so
we're overriding a couple of methods.
There are differences to PizzasListActivity in terms of which menu items to show or hide:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 28/59
While in the PizzasListActivity the data model is a List,
PizzasCartListActivity's data model is a List.
Accordingly, the ListAdapter is of type PizzaLineItemViewAdapter.
The PizzaLineItemViewAdapter extends the PizzaViewAdapter, which
has been discussed in chapter Displaying the Pizzas List. It uses the
same XML layout (pizzaslist_view_tablelayout.xml), however
customizes certain elements (the quantity TextView and the
removeButton ImageView). That's where our PizzaViewAdapter's
protected void customize*(..) methods come in, which are
overridden in PizzaLineItemViewAdapter:
@Override
protected void customizeView(T pizza, ViewHolder holder) {
super.customizeView(pizza, holder);
customizeQuantityTextView(holder.getQuantityView(),
String.valueOf(((PizzaLineItem) pizza).getQuantity()));
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.PizzaLineItemViewAdapter
3.3.4.2. Adding and Removing Pizzas to and from the Cart
When the user clicks on a list item in the PizzasCartListActivity ...
@Override
protected void preparePizzaClick() {
getListView().setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View view,
int position, long id) {
Pizza pizza = getListAdapter().getItem(position);
showPizzaSelectedDialog(pizza);
}
});
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasCartListActivity
... a corresponding AlertDialog is shown.
See chapter Creating an AlertDialog for details.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 29/59
From this dialog, from method showPizzaSelectedDialog(Pizza), the
application's underlying Data Model is manipulated:
public void add(Pizza pizza) {
PizzaLineItem pizzaItem = getPizzaLineItem(pizza);
if (pizzaItem != null) {
pizzaItem.setQuantity(pizzaItem.getQuantity() + 1);
} else {
pizzas.add(new PizzaLineItem(pizza));
}
}
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model.order
After that, the ListActivity is bound to the data model (PizzasCartListActivity.setListAdapter()), the screen
state gets updated (ListActivity.setContentChanged()), and an OnClickListener gets newly re-attached
(PizzasCartListActivity.preparePizzaClick()):
protected void refreshGUI() {
setListAdapter();
onContentChanged();
preparePizzaClick();
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasCartListActivity
In effect, the new model's state (PizzaLineItem quantitys for instance) will be reflected in the GUI.
3.3.4.3. Finally, Placing the Order
If the user has filled in their user data, and if they've selected some pizzas into their shopping cart as well, the
Check Out Shopping Cart menu item gets enabled in PizzasListActivity.onPrepareOptionsMenu(Menu).
When the user clicks that menu item, and confirms the subsequent dialog,
the application HTTP POSTs the Order instance (containing a
List and a UserData) instance to the remote server.
Under the hood, there are techniques involved that mostly have been
discussed already: The HttpPostTask works equivalently to the
HttpGetPizzasListTask, not blocking the UI thread by extending a AsyncTask
and utilizing Spring's RestTemplate. When the task has finished
(succeeded or failed), the UI thread is updated to inform the user.
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.task/HttpPostTask
See chapter HttpGetPizzasListTask for a more detailed discussion.
3.3.5. UserDataFormActivity: A Validating Input Form
When the user clicks the user data MenuItem at the pizzas list or shopping cart view, the
UserDataFormActivity is started.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 30/59
This form can be divided into two areas: fields related to the user's
location to be used with Android location-based services (LBS)
and additional user data.
As the sample application does provide location-based services,
there is the AddressFormActivity, which considers the location-
based fields only. The AddressFormActivity can (could) be used
standalone, or for other purposes.
From there, the UserDataFormActivity extends the
AddressFormActivity by adding the additional user data form fields
and the action buttons.
3.3.5.1. XML Layout
3.3.5.1.1. User Data
The user data XML layout is defined as follows:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 31/59
android:id="@+id/userInfo_phone" style="@style/formField.text" />
For corresponding code see: mg-pizzastore-android/res/layout/userinfo_form_tablelayout.xml
We're using a TableLayout with its android:stretchColumns attribute (that takes a zero-based, comma-
separated, list of column indexes, and that's specifying which columns to stretch if the table is wider than its
columns require). There's also the android:layout_span attribute on Views.
The address layout is included from a separate file.
Empty Views are used as spacers.
There's the style attribute to apply a style that has been defined in /res/values/styles.xml. See chapter
Re-Using Layout Styles for details.
de.metagear.android.view.ValidatingEditText is a reference to a custom control of ours, which extends
Android's EditText. See chapter ValidatingEditText for more information.
3.3.5.1.2. Address
The address XML layout is defined as follows:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 32/59
For corresponding code see: mg-pizzastore-android/res/layout/address_form_merge.xml
The merge element is a container that does not represent a View (and therefore saves some resources). It's
used in conjunction with a parent XML layout's include declaration.
In addition to the aforementioned custom ValidatingEditText, we're also using a custom ValidatingSpinner.
3.3.5.2. A Custom, Optionally Selectable, Spinner
Like a ListActivity, a Spinner is populated with List items that are
provided via an Adapter.
By default, there is no "unselected" state, and the first list item is
preselected, even when the user hasn't interacted with the spinner.
This could be solved by re-arranging the source list to provide an empty first item, however we address the
issue in a generic, object-orientated, fashion.
3.3.5.2.1. Decorating a SpinnerAdapter
The OptionalSelectionSpinnerAdapter decorates Android's SpinnerAdapter implementation as follows:
public class OptionalSelectionSpinnerAdapter extends BaseAdapter implements
SpinnerAdapter {
protected Context context;
protected SpinnerAdapter adapter;
public OptionalSelectionSpinnerAdapter(Context context,
SpinnerAdapter adapter) {
this.context = context;
this.adapter = adapter;
}
@Override
public int getCount() {
return adapter.getCount() + 1;
}
@Override
public Object getItem(int position) {
if (position == 0) {
return new TextView(context);
} else {
return adapter.getItem(position 1);
}
}
@Override
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 33/59
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (position == 0) {
return new TextView(parent.getContext());
} else {
return adapter.getView(position 1, null, parent);
}
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
View newView;
if (position == 0) {
newView = new TextView(parent.getContext());
} else {
newView = adapter.getDropDownView(position 1, null, parent);
}
return newView;
}
}
For corresponding code see: mg-library-
android/de.metagear.android.view.OptionalSelectionSpinnerAdapter
We might have implemented Caching Views (the ViewHolder Pattern) in the above code sample.
3.3.5.2.2. Overwriting Spinner.setAdapter(..)
Basically, we're overriding setAdapter(SpinnerAdapter) to decorate the given SpinnerAdapter if that's not
already been done in the calling code:
public class OptionalSelectionSpinner extends Spinner {
// ...
@Override
public void setAdapter(SpinnerAdapter adapter) {
if (adapter instanceof OptionalSelectionSpinnerAdapter) {
super.setAdapter(adapter);
} else {
super.setAdapter(new OptionalSelectionSpinnerAdapter(context,
adapter));
}
}
}
For corresponding code see: mg-library-
android/de.metagear.android.view.OptionalSelectionSpinner
3.3.5.3. Populating and Saving the Form
3.3.5.3.1. Design and Workflow
Please note that this section does not perfectly comply with the sample project's code.
The following sequence diagram shows the involvement of different participants in managing the application's
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 34/59
central UserData instance:
Participants are the following classes:
ApplicationState: Central instance being available to all
Application-aware class instances within a certain session,
holding global application state.
It provides a UserData instance via getOrder().getUserData().
See also chapter Maintaining Global Application State.
UserDataFormActivity: Displays the user data form, from which
the UserData data model gets populated.
See also chapter UserDataFormActivity: A Validating Input
Form.
UserDataViewAdapter: A helper class that translates between the
UserData model and the corresponding EditText (etc.) fields of the
UserDataFormActivity.
See also chapter Adapting the User Data Form to the Data
Model.
SharedPrefsAdapter: A helper class that persists application state
between Application shutdowns and startups, utilizing the Android
SharedPreferences.
See also chapter Dealing with Android's SharedPreferences.
There's the following workflow involved:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 35/59
1. At application startup, the ApplicationState retrieves a UserData instance from the Android
SharedPreferences by using the SharedPrefsManager. If no UserData has been saved previously, the
SharedPrefsManager creates a new, empty, instance.
2. The UserDataFormActivity, when it comes up, queries the ApplicationState for a current UserData
instance and ...
3. ... lets the UserDataViewAdapter initialize its EditText (etc.) values (the form fields).
4. When the user clicks the Save button (provided that everything is valid), the UserDataFormActivity
utilizes the UserDataViewAdapter again this time to populate a UserData instance from its Views' values.
5. This state change gets propagated to the - globally accessible ApplicationState ...
6. ... and for future use beyond the current session persisted to the Android SharedPreferences using
the SharedPrefsAdapter.
The underlying data model gets populated from Android's SharedPreferences early at application startup because
existence and validity of the UserData is required for eventually checking out the shopping cart:
protected void initializeUserDataFromSharedPrefs() {
try {
UserData userData = SharedPrefsAdapter.retrieve(this, UserData.class);
appState.getOrder().setUserData(userData);
} catch (Throwable t) {
// ...
}
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.PizzasListActivity
When in the UserDataFormActivity the Save button gets clicked (and the form is valid), a UserData instance is
created from the form values and persisted to the Android SharedPreferences:
protected void saveUserData() {
UserData userData = UserDataViewAdapter.fromView(rootView);
UserDataViewAdapter.toSharedPrefs(this, userData);
appState.getOrder().setUserData(userData);
}
3.3.5.3.2. Dealing with Android's SharedPreferences
SharedPreferences are used to persist and retrieve data Strings and primitives between application
sessions.
In the sample application, we've standardized corresponding behavior into a separate class:
public class SharedPrefsAdapter {
public static T retrieve(Context context, Class valueType)
throws JsonParseException, JsonMappingException, IOException,
InstantiationException, IllegalAccessException {
String key = valueType.getName();
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
String value = prefs.getString(key, null);
if (value == null) {
return valueType.newInstance();
}
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 36/59
return new ObjectMapper().readValue(value, valueType);
}
public static void persist(Context context, T object)
throws JsonGenerationException, JsonMappingException, IOException {
String key = object.getClass().getName();
String value = new ObjectMapper().writeValueAsString(object);
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
Editor editor = prefs.edit();
editor.putString(key, value);
editor.commit();
}
}
For corresponding code see: mg-library-android/de.metagear.android.util.SharedPrefsAdapter
Amongst other options, SharedPreferences can be private to the corresponding Application, or publically
available to other installed applications. We're using the PreferenceManager.getDefaultSharedPreferences(..)
method, which considers private data only.
We've already mentioned that SharedPreferences only work with Strings and primitive data types. Therefore
we're using the Jackson/JSON ObjectMapper to marshal, resp. unmarshal, objects to, resp. from, JSON Strings.
(See chapter Using the JSON ObjectMapper with the Model Objects for implications on the model objects.)
When persisting data to the SharedPreferences, a kind-of-transactional SharedPreferences.Editor needs to be
employed (see the above code snippet).
3.3.5.3.3. Adapting the User Data Form to the Data Model
Getting back to the UserDataFormActivity that holds the user data form, this activity connects to its underlying
data model through employing the UserDataViewAdapter.
In addition to connecting the view to the data model, the UserDataViewAdapter also performs validation.
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.UserDataViewAdapter
For what it's worth to mention, adapters are used throughout many scopes of the sample application,
connecting different application layers or coercing transformation of different data structures implementing
the separation of concerns paradigm.
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.adapter
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.view.adapter
3.3.5.4. A Custom Form Field Validation Framework
Android does not really provide a framework to validate forms, and higher-level third-party validation
frameworks don't seem to exist. Thus we've implemented our own one just like other people did .
3.3.5.4.1. ViewValidator Interface
The central interface, which all validating views must implement, is the ViewValidator:
public interface ViewValidator {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 37/59
boolean validate(View view);
/**
* Returns a localized error message (even when validation has been successful).
* @param caption the display name of the TextView (i.e., "ZIP Code")
* @return localized error message; never null
*/
String getErrorMessage(String caption);
}
3.3.5.4.2. TextView Validators
Our framework provides a number of ViewValidator implementations that validate TextViews or derived classes:
For corresponding code see: mg-library-
android/de.metagear.android.view.validation.textview
The MinLengthValidator checks if the TextView's text has a given minimum length. The
MatchingTextViewValidator checks if the TextView's text matches the text of another TextView (this could be
used with Password and Password (repeat) form fields, for instance).
The other ViewValidators all extend RegexValidator, which is implemented as in the following code snippet:
public class RegexValidator implements ViewValidator {
protected Context context;
protected Pattern pattern;
public RegexValidator(Context context, String regex) {
context = context;
pattern = Pattern.compile(regex);
}
@Override
public boolean validate(View view) {
Matcher matcher = pattern.matcher(((TextView) view).getText());
return matcher.matches();
}
@Override
public String getErrorMessage(String caption) {
return context.getString(R.string.validation_regex, caption,
pattern.toString());
}
}
3.3.5.4.3. Self-Validating Views
Currently, our little framwork provides a TextView and an OptionalSelectionSpinner, which both self-validate at
certain user-initiated GUI events or, alternatively, are initiated programmatically.
Validating views are required to implement the ValidatingView interface, which is listed below:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 38/59
/**
* A View that validates itself by using the given
* ViewValidator.
* If not valid, the view shall display an error icon.
*/
public interface ValidatingView {
/**
* Registers the given validator.
*
* @param validator
* @param fieldDisplayNameForErrorMsg
* Localized display field name, i.e., "City" or "Postal Code".
*/
void setValidator(ViewValidator validator,
String fieldDisplayNameForErrorMsg);
/**
* Causes the view to show or hide an error icon depending on its validity.
*/
void flagOrUnflagValidationError();
/**
* Removes any error icons from the view.
*/
void unflagValidationError();
/**
* Returns whether the view's value is valid.
*/
boolean isValid();
}
3.3.5.4.4. ValidatingEditText
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 39/59
The AFAIK only facility that Android provides for form validation is
TextView.setError(CharSequence). This draws an error icon and if the
widget gains focus a descriptive text in popup box. Passing null to
that method unflags the error.
Design enthusiasts will instantly realize that the theme of Android's
rudimental form validation does not match Android's default or
even custom theme. We'd urgently need a comprehensive, high-level, validation framework for Android.
3.3.5.4.4.1. Performing Validation
In our ValidatingEditText, we extend EditText, implementing our ViewValidator:
public class ValidatingEditText extends EditText implements ValidatingView {
private ViewValidator validator;
private String fieldDisplayNameForErrorMsg;
// ...
@Override
public void setValidator(ViewValidator validator,
String fieldDisplayNameForErrorMsg) {
this.validator = validator;
this.fieldDisplayNameForErrorMsg = fieldDisplayNameForErrorMsg;
// ...
}
@Override
public void flagOrUnflagValidationError() {
String msg = isValid() ? null : validator
.getErrorMessage(fieldDisplayNameForErrorMsg);
setError(msg);
}
@Override
public void unflagValidationError() {
setError(null);
}
@Override
public boolean isValid() {
return validator.validate(this);
}
// ...
}
We also register a number of listeners to react on user-initiated GUI events: an OnLongClickListener, an
OnKeyListener (for capturing the Enter key press event), and an OnFocusChangeListener (to validate on lost
focus events).
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingEditText
3.3.5.4.4.2. Setting Soft Keyboard Behavior
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 40/59
As a side effect, we're
also setting the EditText's
InputType to reflect the
preferred input scheme,
i.e., numbers for ZIP Code
or characters for City.
This setting will get picked
up by the soft keyboard (if
available).
protected void initInputType(ViewValidator validator) {
if (validator instanceof EmailValidator) {
setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
} else if (validator instanceof PhoneNumberValidator) {
setInputType(InputType.TYPE_CLASS_PHONE);
} else if (validator instanceof ZipCodeValidator) {
setInputType(InputType.TYPE_CLASS_NUMBER);
} else if (!isPassword()) {
setInputType(InputType.TYPE_CLASS_TEXT);
} else if (isPassword()) {
setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
protected boolean isPassword() {
TransformationMethod method = getTransformationMethod();
return method != null && method instanceof PasswordTransformationMethod;
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingEditText
3.3.5.4.5. ValidatingSpinner
In contrast to TextViews, a Spinner does not provide a setError(..)
method and also does not provide means to show that error icon and
error message popup box.
We're overwriting Spinner (more specifically,
OptionalSelectionSpinner, see chapter A Custom, Optionally Selectable, Spinner) to implement our
ValidatingView interface, and we're drawing the error icon manually as shown below.
3.3.5.4.5.1. Drawing the Error Icon
By looking at Android's TextView source code, we found the error icon on disk (indicator_input_error.png)
and copied that into our project.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 41/59
The following code snippet draws this Bitmap on top of the Spinner's Canvas:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (errorIconEnabled && !isValid()) {
drawErrorIcon(canvas);
}
}
protected void drawErrorIcon(Canvas canvas) {
final int ICON_RIGHT_MARGIN = 40;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.indicator_input_error);
float left = getWidth() ICON_RIGHT_MARGIN bitmap.getWidth();
float top = (getHeight() bitmap.getHeight()) / 2;
// ...
canvas.drawBitmap(bitmap, left, top, new Paint());
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
Note the central instance variable errorIconEnabled, which gets discussed in the following chapter.
3.3.5.4.5.2. Events that Trigger Validation
The error icon is shown, resp., hidden, when either the user selects a Spinner item or when validation is
requested programmatically. Note that the Spinner View will be redrawn when the user selects an item, or
when invalidate() is called (this ought to sound familiar to Swing developers).
@Override
public void setSelection(int position, boolean animate) {
super.setSelection(position, animate);
errorIconEnabled = true;
}
@Override
public void setSelection(int position) {
super.setSelection(position);
errorIconEnabled = true;
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
@Override
public void flagOrUnflagValidationError() {
errorIconEnabled = true;
invalidate();
}
@Override
public void unflagValidationError() {
errorIconEnabled = false;
invalidate();
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 42/59
3.3.5.4.5.3. There's No Such Error Message Popup Box as a FreeLunch
Looking at the source code that enables TextViews' error message popups, we found that this ain't re-usable
at all. We'd have to re-implement that functionality from scratch and therefore resigned on it for now.
A comprehensive validation framework for Android like the ones that, e.g., JavaServer Faces or the
Spring Framework provide would be really desirable.
3.3.6. MapViewActivity: Interacting with Maps and Location-BasedServices
3.3.6.1. General Setup
Because of a least one blocking Bug related to location-based services and the Android emulator,
the functionality covered in this chapter currently cannot be used on Android 2.2 and 2.3 (anything > API level
7). That's why our project is Android 2.1-based.
Android projects that use MapView and related libraries need to have
the corresponding Google APIs on the classpath. These can be
installed via the Android SDK and AVD Manager and need to be
referenced in the Eclipse project setup:
The Google Maps library also needs to be setup in AndroidManifest.xml. Additionally, as it accesses Google
services on the internet, our application needs to claim the corresponding permissions:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 43/59
For corresponding code see: mg-pizzastore-android/AndroidManifest.xml
The sample application's map activities can be started via the user data form's menu.
Our MapViewActivity extends Google's MapActivity, which is used in conjunction with Google's MapView. The
MapActivity's main XML layout is shown in the following code snippet.
For using the MapView control, a Google Maps API Key is required.
The keytool mentioned in the linked article is part of the (Sun) Java SDK (i.e., not necessarily the keytool
that may be on the PATH on Linux distributions).
In contrast to what the linked article (and other sources on the web) tell, a Google account is not required to
generate the API key.
The following code snippet shows the MapViewActivity's XML layout file:
For corresponding code see: mg-pizzastore-android/res/layout/map_linearlayout.xml
Getting back to the menu shown in the screenshot above, there are two custom MapViewModes in the sample
application, which provide slightly differing functionality. These are discussed in the following chapters.
3.3.6.2. Assuring Preconditions Upon the Device's State
The Google MapView control requires accessing the internet. From our MapViewActivity we're calling our
AndroidDeviceUtils.isNetworkConnected(..) to assure that:
public static boolean isNetworkConnectedElseAlert(Context context,
Class callingClass) {
boolean isOnline = isNetworkConnected(context);
if (!isOnline) {
ErrorHandler.logAndAlert(context, callingClass,
context.getString(R.string.androidUtil_error_notOnline));
}
return isOnline;
}
public static boolean isNetworkConnected(Context context) {
ConnectivityManager manager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
return manager.getActiveNetworkInfo().isConnected();
}
For corresponding code see: mg-library-android/de.metagear.android.util.AndroidDeviceUtils
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 44/59
Additionally, location-based services require that - in our case GPS is enabled. Our application checks
whether GPS is enabled, and if it isn't, optionally displays the corresponding Android dialog and then re-checks
that condition:
private static boolean isGpsEnabledElseOptionallyEnableIt(Context context) {
boolean enabled = AndroidDeviceUtils.isGpsEnabled(context);
if (!enabled) {
AndroidDeviceUtils.showGpsDisabledAlert(context);
enabled = AndroidDeviceUtils.isGpsEnabled(context);
}
return enabled;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
The Android Location & security settings dialog is getting called by
starting an Activity with an implicit Intent:
context.startActivity(new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
For corresponding code see: mg-library-
android/de.metagear.android.util.AndroidDeviceUtils
3.3.6.3. Displaying a Given Location in the Map Viewer
From the Show Map menu action, we're starting our MapViewActivity
and display the location that bases on the data in the address
form.
Using our utility StringUtils.join(..), these values are translated to
the string "Am Hohen Ufer 3A, 30159, Hannover, Germany".
This String gets translated (geocoded) to a GeoPoint by using a
GeoCoder:
protected GeoPoint getGeoPointFromLocationName(String locationName) {
Geocoder geocoder = new Geocoder(this);
final int MAX_RESULTS = 1;
try {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 45/59
List addresses = geocoder.getFromLocationName(
locationName, MAX_RESULTS);
if ((addresses == null) || (addresses.size() == 0)) {
Toast.makeText(
this,
getResources().getString(
R.string.mapViewer_noResultsForAddress,
locationName), Toast.LENGTH_LONG).show();