Pages

Tuesday, May 10, 2011

How to create a ListView in Android populated by data from Google App-Engine: Part Two

In the last article, I went over how to create a Web Application using Google's App-Engine Datastore and Blobstore that served data in the form of XML.

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

1 comment:

  1. Hi.. Thanks for the tutorial.! It was exactly wat i was looking for.
    I want to generate the XML code when i click on the Add item button. Can u pls provide the code for tht?

    ReplyDelete