You need the App-Engine backend in order to create the Android frontend. If you have not yet read part one, please do so before continuing.
Part One:
How to create a ListView in Android populated by data from Google App-Engine: Part One
Now that you have the Google App-Engine server up and running (you need to deploy your application before accessing the data from an Android application) we can start writing our Android application which will read data from our App-Engine Datastore.
Create a new Android Project in Eclipse.
In this example, I'm going to write a simple ListActivity that calls upon our App-Engine backend to serve the XML data our ListView needs in order to display the correct information.
First, edit res/layout/main.xml in your Android project so it looks like such:
//main.xml
<?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="@+id/android:list" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <TextView android:id="@+id/android:empty" android:layout_width="fill_parent" android:layout_height="fill_parent" android:text=""/> </LinearLayout>
Also, create a new XML file in your layout folder called "row.xml".
//row.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:padding="6dip"> <ImageView android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_marginRight="6dip" android:src="@drawable/icon" /> <LinearLayout android:orientation="vertical" android:layout_width="0dip" android:layout_weight="1" android:layout_height="fill_parent"> <TextView android:id="@+id/toptext" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center_vertical" /> <TextView android:layout_height="0dip" android:layout_weight="1" android:id="@+id/bottomtext" android:singleLine="true" android:ellipsize="marquee" android:layout_width="wrap_content"/> </LinearLayout> </LinearLayout>
Main.xml defines the layout for the main screen activity (the ListActivity), and the row.xml defines the layout for each individual row in the ListView. This particular layout has an icon on the left side, with two lines of text. Here is roughly what the layout will look like without any data:
That should take care of the layout, now we can move onto the application code. First, we need another Item model that mirrors our Item model from the App-Engine code.
Create a new class called Item.java
//Item.java
package net.jfierstein.ListViewSampleApp; public class Item { private String title; private String description; private String pkg; private String imageKey; //image? public Item() { this.title = ""; this.description = ""; } public String getTitle() { return this.title; } public void setTitle(String t) { this.title = t; } public String getDescription() { return this.description; } public String getImageKey() { return this.imageKey; } public String getPkg() { return this.pkg; } public void setDescription(String d) { this.description = d; } public void setImageKey(String k) { this.imageKey = k; } public void setPkg(String p) { this.pkg = p; } public String toString() { return "Item (Title:" + this.title + ", Description: " + this.description + ")"; } }
Now we can create our main Activity, which will extend a ListActivity. I called my Activity MainList.java
This class is a bit complicated. Below is the entirety of MainList.java. I have tried to comment this code as best as I can, since explaining it piece by piece is difficult. The entry point is the onCreate method which calls fetchContent(). From there, we start a new thread to fetch our data online from the servlet, then we parse and return a list of Item objects for use with a standard ListView Adapter.
//MainList.java
package net.jfierstein.ListViewSampleApp; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import com.cox.AppPackDemo.R; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; public class MainList extends ListActivity { private ProgressDialog m_ProgressDialog = null; private ArrayList<Item> m_items = null; private ItemAdapter m_adapter; private Runnable viewItems; private Runnable returnRes = new Runnable() { @Override public void run() { if(m_items != null && m_items.size() > 0) { m_adapter.notifyDataSetChanged(); m_adapter.clear(); for(int i=0;i<m_items.size();i++) m_adapter.add(m_items.get(i)); } m_ProgressDialog.dismiss(); m_adapter.notifyDataSetChanged(); } }; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); fetchContent(); } public void fetchContent() { m_items = new ArrayList<Item>(); //set ListView adapter to basic ItemAdapter //(it's a coincidence they are both called Item) this.m_adapter = new ItemAdapter(this, R.layout.row, m_items); setListAdapter(this.m_adapter); //create a Runnable object that does the work //of retrieving the XML data online //This will be run in a new Thread viewItems = new Runnable() { @Override public void run() { //this is where we populate m_items (ArrayList<Item>) //which we can get from XML //the XML can be updated via Google App-Engine try { getData(); } catch (Exception e) { Log.e("ListViewSampleApp", "Unable to retrieve data.", e); } //This executes returnRes (see above) which will use the //ItemAdapter to display the contents of m_items runOnUiThread(returnRes); } }; //Create a new Thread to run viewItems Thread thread = new Thread(null, viewItems, "MagentoBackground"); thread.start(); //Make a popup progress dialog while we fetch and parse the data m_ProgressDialog = ProgressDialog.show(this, "Please wait...", "Retrieving data ...", true); } private void getData() throws IOException { try { // Create a URL we want to load some xml-data from. URL url = new URL("http://coxapm.appspot.com/serve"); // Get a SAXParser from the SAXPArserFactory. SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); // Get the XMLReader of the SAXParser we created. XMLReader xr = sp.getXMLReader(); // Create a new ContentHandler and //apply it to the XML-Rea der XMLHandler xmlHandler = new XMLHandler(); xr.setContentHandler(xmlHandler); InputSource xmlInput = newInputSource(url.openStream()); Log.e("ListViewSampleApp", "Input Source Defined: " + xmlInput.toString()); /* Parse the xml-data from our URL. */ xr.parse(xmlInput); /* Parsing has finished. */ /* XMLHandler now provides the parsed data to us. */ m_items = xmlHandler.getParsedData(); } catch (Exception e) { Log.e("ListViewSampleApp XMLParser", "XML Error", e); } } //OPTIONS MENU STUFF @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.refresh: fetchContent(); return true; default: return super.onOptionsItemSelected(item); } } /* * PRIVATE ADAPTER CLASS. Assigns data to be displayed on the listview */ private class ItemAdapter extends ArrayAdapter<Item> { //Hold array of items to be displayed in the list private ArrayList<Item> items; public ItemAdapter(Context context, int textViewResourceId, ArrayList<Item> items) { super(context, textViewResourceId, items); this.items = items; } //This method returns the actual view //that is displayed as a row (we will inflate with row.xml) @Override public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; if (v == null) { LayoutInflater vi = (LayoutInflater)getSystemService(Context LAYOUT_INFLATER_SERVICE); //inflate using res/layout/row.xml v = vi.inflate(R.layout.row, null); } //get the Item corresponding to //the position in the list we are rendering Item o = items.get(position); if (o != null) { //Set all of the UI components //with the respective Object data ImageView icon = (ImageView) v.findViewById(R.id.icon); TextView tt = (TextView) v.findViewById(R.id.toptext); TextView bt = (TextView) v.findViewById(R.id.bottomtext); if (tt != null) { tt.setText("Title: "+o.getTitle()); } if(bt != null) { bt.setText(o.getDescription()); } if(icon != null) { URL imageURL = null; try { //use our image serve page to get the image URL imageURL = new URL("http://yourapp.appspot.com/serveBlob?id=" + o.getImageKey()); } catch (MalformedURLException e) { e.printStackTrace(); } try { //Decode and resize the image then set as the icon BitmapFactory.Options options = new BitmapFactory .Options(); options.inJustDecodeBounds = true; options.inSampleSize = 1/2; Bitmap bitmap = BitmapFactor .decodeStream((InputStream)imageURL .getContent()); Bitmap finImg = Bitmap .createScaledBitmap(bitmap, 50, 50, false); icon.setImageBitmap(finImg); } catch (IOException e) { e.printStackTrace(); } } } //returns the view to the Adapter to be displayed return v; } } }//XMLHandler.java
package net.jfierstein.ListViewSampleApp; import java.util.ArrayList; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import android.util.Log; public class XMLHandler extends DefaultHandler { // =========================================================== // Fields // =========================================================== private boolean in_item = false; private boolean in_title = false; private boolean in_description = false; private boolean in_imageKey = false; private Item item = null; private ArrayList<Item> items = null; // =========================================================== // Getter & Setter // =========================================================== public ArrayList<Item> getParsedData() { return this.items; } // =========================================================== // Methods // =========================================================== @Override public void startDocument() throws SAXException { Log.e("XMLHandler", "Initiating parser..."); this.items = new ArrayList<Item>(); } @Override public void endDocument() throws SAXException { // Nothing to do } /** Gets be called on opening tags like: * <tag> * Can provide attribute(s), when xml was like: * <tag attribute="attributeValue">*/ @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if (localName.equals("Item")) { this.in_item = true; item = new Item(); Log.e("XMLHandler", "Found an Item"); } else if (localName.equals("Title")) { this.in_title = true; } else if (localName.equals("Image")) { this.in_imageKey = true; } else if (localName.equals("Description")) { this.in_description = true; } } /** Gets be called on closing tags like: * </tag> */ @Override public void endElement(String namespaceURI, String localName, String qName) throws SAXException { if (localName.equals("Item")) { this.in_item = false; items.add(item); } else if (localName.equals("Title")) { this.in_title = false; } else if (localName.equals("Image")) { this.in_imageKey = false; } else if (localName.equals("Description")) { this.in_description = false; } } /** Gets be called on the following structure: * <tag>characters</tag> */ @Override public void characters(char ch[], int start, int length) { if(this.in_item) { String textBetween = new String(ch, start, length); if(this.in_title) item.setTitle(textBetween); else if(this.in_imageKey) item.setImageKey(textBetween); else if(this.in_description) item.setDescription(textBetween); } } }
Hopefully, the commenting is enough to give you a general idea of how we go about parsing the XML data online and using the data to populate a ListView.
Lastly, we need to make one quick change to the AndroidManifest.xml. Add the following line of code before the </manifest> tag at the end.
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
Here is what the app should look like:
Enjoy :)
-Josh
Hi.. Thanks for the tutorial.! It was exactly wat i was looking for.
ReplyDeleteI want to generate the XML code when i click on the Add item button. Can u pls provide the code for tht?