Wednesday 16 February 2011

Real estate application for Android(RSS) part 2

Continuing with the RSS android application, we are going to take a closer look at LazyAdapter class which has the most relevant code of the application.
To begin with, notice that the class implements OnScrollListener. The reason why is that we need some scrolling behaviour on the list view and the most reasonable way to do that is making this class an implementation of that interface. That is why this class acts as an adaper and as a scroll listener to the list view.
public View getView(int position, View convertView, ViewGroup parent) {
        View vi = convertView;
        ViewHolder holder;
        
        if (convertView == null) {
            vi = inflater.inflate(R.layout.item, null);
            holder = new ViewHolder();
            holder.text = (TextView) vi.findViewById(R.id.text);;
            holder.image = (ImageView) vi.findViewById(R.id.image);
            vi.setTag(holder);
        } else {
            holder = (ViewHolder) vi.getTag();
        }
        
        if (data.size() <= position) {
         readRss(10);
        }
        
        lastViewedPosition = position;
        
        holder.text.setText(data.get(position).getTitle());
        holder.image.setTag(data.get(position).getImage());
        imageLoader.DisplayImage(data.get(position).getImage(), activity, holder.image);
        
        return vi;
    }
More in particular the method getView(mandatory because it is extending android.widget.Adapter) is called every time a ListView's item will be shown. Here is where you must return a view of the element. On our case, a thumbnail and a brief description of the property.
@Override
 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  if (totalItemCount - lastViewedPosition <= 3) {
   readRss(10);
  }
 }
On the other hand, we have onScroll method which is mandatory since we implemented OnScrollListener interface. This method will be called every time the user tries to view an element that is not on the screen at that moment in particular. This is the appropriate moment to check if we have enough elements to continue scrolling. That way, we can manage list view scrolling on the fly.
private boolean connect() {
     try {
      if (xpp == null) {
    URL url = new URL(feedString);
    URLConnection connection = url.openConnection();
    
    HttpURLConnection httpConnection = (HttpURLConnection)connection; 
    int responseCode = httpConnection.getResponseCode(); 
    
    if (responseCode == HttpURLConnection.HTTP_OK) {
     InputStream in = httpConnection.getInputStream();
     
        factory = XmlPullParserFactory.newInstance();
           factory.setNamespaceAware(true);
           xpp = factory.newPullParser();
           
           xpp.setInput(in, "ISO-8859-1");
    }
      }
   
   return true;
   
     } catch (XmlPullParserException xe) {
      Log.e("RssReader", "Connection", xe);
      return false;
      
     } catch (IOException eio) {
      Log.e("RssReader", "Connection", eio);
      return false;
      
     }
    }
    
    private void readRss(int itemsToRead) {
     boolean insideItem = false;
     boolean insideTitle = false;
     boolean insidePubDate = false;
     boolean insideDescription = false;
     Property property = null;
     
     try {
      if (connect()) {
          int eventType = xpp.getEventType();
          
          while (eventType != XmlPullParser.END_DOCUMENT && itemsToRead >= 0) {
           if(eventType == XmlPullParser.START_TAG) {
      if (xpp.getName().equalsIgnoreCase("item")) {
       insideItem = true;
       property = new Property();
      } else if (insideItem && xpp.getName().equalsIgnoreCase("title")) {
       insideTitle = true;
      } else if (insideItem && xpp.getName().equalsIgnoreCase("pubDate")) {
       insidePubDate = true;
      } else if (insideItem && xpp.getName().equalsIgnoreCase("description")) {
       insideDescription = true;
      }
      
     } else if(eventType == XmlPullParser.END_TAG) {
      if (xpp.getName().equalsIgnoreCase("item")) {
       insideItem = false;
       data.add(property);
       --itemsToRead;
      }
         
     } else if(eventType == XmlPullParser.TEXT && insideItem) {
      if (xpp.getText() != null) {
       if (insideTitle) {
        property.setTitle(xpp.getText());
        insideTitle = false;
       } else if (insidePubDate) {
        property.setDate(xpp.getText());
        insidePubDate = false;
       } else if (insideDescription) {
        getDetails(property, xpp.getText());
        insideDescription = false;
       }
      }
      
     }
     
     eventType = xpp.next();
          }
      }
      
  } catch (XmlPullParserException e) {
      Log.e("RssReader", "Connection", e);
  } catch (IOException e) {
      Log.e("RssReader", "Connection", e);
  }
    }
Nevertheless, the issue now is how can we read a resource from Internet on demand. There is a solution to this too. We can read the xml by creating an instance of XmlPullParserFactory. Through this parser you can get elements from the xml, one by one, to show as many elements as you need. Moreover, you can get the instance of the parser and read a set of elements again to get into the view. The parser's instance will preserve pointing where you are, so you will always can get it and read another set of elements til the end of the document. Then we have ImageLoader class that is just an in-memory cache implementation. The idea of this is show a default image while the final one is bring from the internet. This allows us to continue adding elements to the list view independently of the images completition. Lets go back to the MainActivity to see what happens when we click on an element. As we saw in the last post, a click listener was created in order to attend this event.
list.setOnItemClickListener(new OnItemClickListener() {
         public void onItemClick(AdapterView _av, View _v, int _index, long arg3) {
          selectedProperty = properties.get(_index);
          showActivity();
         }
     });
        list.setOnScrollListener(adapter);
        list.setAdapter(adapter);
The selected property is set and the activity that show it is created. Lets show the code for the method showActivity.
private void showActivity() {
     Intent imageView = new Intent(this, ImageFromUrl.class);
     
 String propertyDescription = selectedProperty.getTitle() + "\n" +
              "Bedrooms " + selectedProperty.getBedrooms()  + "\n" +  
               "Bathrooms " + selectedProperty.getBathrooms();
     
     Bundle bundle = new Bundle();
     bundle.putString("imageUrl", selectedProperty.getImage());
     bundle.putString("propertyDescription", propertyDescription);
     imageView.putExtras(bundle);
     
     startActivity(imageView);
    }
As you can see, an Intent is created based on the ImageFromUrl class. This class will take care of how a property should be shown. The idea is that the Activity can be shown even if the image haven't been loaded yet.
public class ImageFromUrl extends Activity {
    
    private String url = null;
    private String description = null;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image_viewer);
        
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
         url = extras.getString("imageUrl");
         description = extras.getString("propertyDescription");
        }

        ImageView imgView = (ImageView) findViewById(R.id.ImageView);
        TextView txtView = (TextView) findViewById(R.id.DescriptionView);
        
        Drawable drawable = LoadImageFromWebOperations(url);
        imgView.setImageDrawable(drawable);
        txtView.setText(description);
    }

   private Drawable LoadImageFromWebOperations(String url) {
 try {
     InputStream is = (InputStream) new URL(url).getContent();
     Drawable d = Drawable.createFromStream(is, "src name");
     return d;
  
 }catch (Exception e) {
     return null;
 }
   }
}
Lastly I want to add something I forgot to mention. The application was based on the real estate site http://www.suburbview.com and I use in particular I used the housing rental from Melbourne RSS. This information was written down into a resource file named strings.xml.


    House Rental RSS
 http://www.suburbview.com/files/vic/3000/rent/melbourne.rss

Because we are not going to see the entire application here, I will give you this link for you to download the eclipse project so you can play with it as much as you want.

HouseRentalRSS.zip

And if you want to try it here you have the application.
I may forget some other things so please, don't hesitate asking questions.

Have fun!!

Monday 7 February 2011

Real estate application for Android(RSS) part 1

Android is an open-source operating system for smartphones. Android was the name of the company that began the challenge of making this operating system for smartphones and Google bought the company to develop the project on their own. The first Android come to light only a few month after the IPhone, back in 2007. By those days the event was announced that way: "Google Android, a new software platform designed to provide open access to mobile phones for application developers." Google's idea was always the same, bring people closer to content on the Web in a easy and organised way. Anyway, this is just history.

I will try to do a little piece of work that you can find useful to learn and enough challenging to keep reading.

I will assume that you have a working environment base on eclipse. If not, you can go here and follow the recipe.

To create an Android application, you will need to create a corresponding Android
project. The project will hold all of your source code, resources, third-party JARs, and related materials.

The application we will develop is very simple. As I plan to go to Australia on holiday, I thought it will be great to use this material to make a useful application.

I've been using suburbview as part of my investigation and they have a great service of RSS that we can use as an excuse to make some piece of work.

The RSS publishes the properties in rent or for sale. We are going to use this to pull properties and make a lazy list showing them. The idea is to use Layouts, Widgets, and Internet access to make a useful application that allows us to look rentals.

I will suppose that you have a clean project with an Activity named MainActivity, and a layout named main.xml. Here is the first problem for newbies. There is no a good tool to make layouts and you probably going to deal with xmls.



 


This is the main layout and basically holds the application on a LinearLayout. There inside, its adds a ListView in which the properties will be shown.

Each item of the ListView needs its own Layout that we named item.xml:



  
  


The next stage to define is the click event on the ListView. An Activity named ImageFromUrl will be responsible. Let's see the layout:



 
  
 
 
  
 


This layout is base on a TableLayout. It's pretty similar the the HTML tag so there is not much to tell. The idea is display the details of the property in a more extended way.

Now that we have the GUI, we have to start programming. An Android program starts by executing on an Activity. The start-up Activity needs to be specifically set in the AndroidManifest.xml along with other things that we won't explain here. Let's see the code:



 
  
   
    
    
   
  
 
 
 
  
 


As you can see above, there are two activities inside the application tag. The one that has the intent-filter inside is that main activity. We are telling this to Android by adding the action and category tags.


    
    
   

I'm not going to give a detailed explanation, but you can ask for answers.

Let's move forward and see some java code. We are going to start by coding the main activity. All custom activities has to extend from Activity android class. Then you have to override onCreate method and there you have to do your staff. That means that you have to create your application(mainly graphically) because Android will invoke this method:

@Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  list = (ListView) findViewById(R.id.list);
  adapter = new LazyAdapter(this, getString(R.string.rental_feed), properties);
  
  list.setOnItemClickListener(new OnItemClickListener() {
   public void onItemClick(AdapterView _av, View _v, int _index, long arg3) {
    selectedProperty = properties.get(_index);
    showActivity();
   }
  });
  list.setOnScrollListener(adapter);
  list.setAdapter(adapter);
 }

First two lines are almost default. We are going to focus on the rest of the method. The third line gets the list in which the process will add the properties. As you may see, there's a static class that Android auto-generate and has all the resources constants. In this case we use the one that represents the list we define in the layout "R.id.list".

Then we create a class LazyAdapter. This class has the behavioural responsibility of the list.

Here is where the first challenge appear. The main goal of the application is to get properties from a RSS. We don't know how much properties this file is going to have. If the file is really big we should spend a lot of resources and time trying to read it entirely before showing the user some information. That is why we have to get a more efficient approach to give the user a better experience. There's a way of doing this and consist in reading the file as necessary.

To do this we need the logic that allows us to parse the file as needed and we can do this, implementing OnScrollListener interface. That will force us to implement two methods, onScroll and onScrollStateChanged. In this case, I made both things in the same class, meaning extends BaseAdapter and implement OnScrollListener.

Because we are extending from BaseAdapter we need to implement some more methods: getCount(), getItem(), getItemId(), getView().

Let's see the class.

public class LazyAdapter extends BaseAdapter implements OnScrollListener {
 
 private int lastViewedPosition = 0;
 
 private XmlPullParserFactory factory = null;
 private XmlPullParser xpp = null;
 
 private Activity activity;
 private ArrayList data;
 
 private static LayoutInflater inflater = null;
 public ImageLoader imageLoader;
 private String feedString;
 
 public LazyAdapter(Activity a, String feedString, ArrayList d) {
  this.feedString = feedString;
  activity = a;
  data = d;
  inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  imageLoader = new ImageLoader(activity.getApplicationContext());
 }

 public int getCount() {
  return data.size();
 }

 public Object getItem(int position) {
  return position;
 }

 public long getItemId(int position) {
  return position;
 }
 
 public static class ViewHolder{
  public TextView text;
  public ImageView image;
 }

 public View getView(int position, View convertView, ViewGroup parent) {
  View vi = convertView;
  ViewHolder holder;
  
  if (convertView == null) {
   vi = inflater.inflate(R.layout.item, null);
   holder = new ViewHolder();
   holder.text = (TextView) vi.findViewById(R.id.text);;
   holder.image = (ImageView) vi.findViewById(R.id.image);
   vi.setTag(holder);
  } else {
   holder = (ViewHolder) vi.getTag();
  }
  
  if (data.size() <= position) {
   readRss(10);
  }
  
  lastViewedPosition = position;
  
  holder.text.setText(data.get(position).getTitle());
  holder.image.setTag(data.get(position).getImage());
  imageLoader.DisplayImage(data.get(position).getImage(), activity, holder.image);
  
  return vi;
 }
 
 private boolean connect() {
  try {
   if (xpp == null) {
    URL url = new URL(feedString);
    URLConnection connection = url.openConnection();
    
    HttpURLConnection httpConnection = (HttpURLConnection)connection; 
    int responseCode = httpConnection.getResponseCode(); 
    
    if (responseCode == HttpURLConnection.HTTP_OK) {
     InputStream in = httpConnection.getInputStream();
     
     factory = XmlPullParserFactory.newInstance();
     factory.setNamespaceAware(true);
     xpp = factory.newPullParser();
     
     xpp.setInput(in, "ISO-8859-1");
    }
   }
   
   return true;
   
  } catch (XmlPullParserException xe) {
   Log.e("RssReader", "Connection", xe);
   return false;
   
  } catch (IOException eio) {
   Log.e("RssReader", "Connection", eio);
   return false;
   
  }
 }
 
 private void readRss(int itemsToRead) {
  boolean insideItem = false;
  boolean insideTitle = false;
  boolean insidePubDate = false;
  boolean insideDescription = false;
  Property property = null;
  
  try {
   if (connect()) {
    int eventType = xpp.getEventType();
    
    while (eventType != XmlPullParser.END_DOCUMENT && itemsToRead >= 0) {
     if(eventType == XmlPullParser.START_TAG) {
      if (xpp.getName().equalsIgnoreCase("item")) {
       insideItem = true;
       property = new Property();
      } else if (insideItem && xpp.getName().equalsIgnoreCase("title")) {
       insideTitle = true;
      } else if (insideItem && xpp.getName().equalsIgnoreCase("pubDate")) {
       insidePubDate = true;
      } else if (insideItem && xpp.getName().equalsIgnoreCase("description")) {
       insideDescription = true;
      }
      
     } else if(eventType == XmlPullParser.END_TAG) {
      if (xpp.getName().equalsIgnoreCase("item")) {
       insideItem = false;
       data.add(property);
       --itemsToRead;
      }
      
     } else if(eventType == XmlPullParser.TEXT && insideItem) {
      if (xpp.getText() != null) {
       if (insideTitle) {
        property.setTitle(xpp.getText());
        insideTitle = false;
       } else if (insidePubDate) {
        property.setDate(xpp.getText());
        insidePubDate = false;
       } else if (insideDescription) {
        getDetails(property, xpp.getText());
        insideDescription = false;
       }
      }
      
     }
     
     eventType = xpp.next();
    }
   }
   
  } catch (XmlPullParserException e) {
   Log.e("RssReader", "Connection", e);
  } catch (IOException e) {
   Log.e("RssReader", "Connection", e);
  }
 }
 
 private void getDetails(Property property, String description) {
  int start = description.indexOf("");
  String imageURL = description.substring(start, end);
  property.setImage(imageURL);
  
  String desc = description.replaceAll("\\<.*?\\>", "");
  String[] parts = desc.split("\n");
  
  for (String line : parts) {
   for (Filter filter : Filter.values()) {
    if ((line.toUpperCase()).startsWith(filter.name())) {
     switch (filter) {
      case CATEGORY:
       property.setCategory((line.substring(filter.name().length() + 1)).trim());
       break;
       
      case BEDROOMS:
       property.setBedrooms(Integer.valueOf((line.substring(filter.name().length() + 1)).trim()));
       break;
       
      case BATHROOMS:
       property.setBathrooms(Integer.valueOf((line.substring(filter.name().length() + 1)).trim()));
       break;
     }
    }
   }
  }
 }

 @Override
 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  if (totalItemCount - lastViewedPosition <= 3) {
   readRss(10);
  }
 }

 @Override
 public void onScrollStateChanged(AbsListView view, int scrollState) {
 }

}
To hold the property we need to create a class. We called it Property(yes, terrible creative) and here we are going to save the information parsed from the rss.
public class Property {
 
 private String title;
 private String date;
 private String category = null;
 private int bedrooms = 0;
 private int bathrooms = 0;
 private String image = null;
 
 public String getTitle() {
  return title;
 }
 
 public void setTitle(String title) {
  this.title = title;
 }
 
 public String getDate() {
  return date;
 }
 
 public void setDate(String date) {
  this.date = date;
 }
 
 public String getCategory() {
  return category;
 }
 
 public void setCategory(String category) {
  this.category = category;
 }
 
 public int getBedrooms() {
  return bedrooms;
 }
 
 public void setBedrooms(int bedrooms) {
  this.bedrooms = bedrooms;
 }
 
 public int getBathrooms() {
  return bathrooms;
 }
 
 public void setBathrooms(int bathrooms) {
  this.bathrooms = bathrooms;
 }
 
 public String getImage() {
  return image;
 }
 
 public void setImage(String image) {
  this.image = image;
 }
 
 @Override
 public String toString() {
  return title;
 }

}

I will give you this to start thinking on and I will be back with the rest as soon as a can!!

Continue...