Tuesday, 25 December 2012

How to create an Android client for Reddit (Part II)

Please do read Part I before you proceed to read this.

Today, we will add a caching mechanism to our app. This will save a lot of network bandwidth for the user, and our app will seem much more responsive in situations like,
  • when the user changes the orientation of her device
  • closes the app accidentally and reopens it.
Let us get started right away.

Firstly, change your manifest so that it has the following permissions,
<uses-permission
         android:name="android.permission.INTERNET"/>
<uses-permission
         android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
The WRITE_EXTERNAL_STORAGE permission is necessary because we will be caching data in the SD card.

Our simple caching algorithm
For each URL, we perform the following steps,
  1. Convert the URL into a simpler, and unique String representation, that we shall use as a filename. This file will hold the contents of the URL.
  2. Next, we check if this file exists. If it exists, and it is not too old, we use the contents of this file.
  3. If the file does not exist, we make the network connection and fetch the contents from the URL. Then, we create a new file, and store those contents in the file.
Let us now create a method that converts the URL to a unique filename. The simplest way to do this is to use a message digest. We will use MD5 for this tutorial. This method should be fairly self-explanatory. All it does is generate an MD5 of the URL, and create a filename of the format mycache_<MD5>.cac.
    static public String convertToCacheName(String url){
        try {            
            MessageDigest digest=MessageDigest.getInstance("MD5");
            digest.update(url.getBytes());
            byte[] b=digest.digest();
            BigInteger bi=new BigInteger(b);
            return "mycache_"+bi.toString(16)+".cac";            
        } catch (Exception e) {
            Log.e("MD5", e.toString());
            return null;
        }
    }
The next thing to do is write a method, that checks if a file is too old, given the file's last modified timestamp. The following method will say a file is too old if it is older than 5 minutes.
    private static boolean tooOld(long time){
        long now=new Date().getTime();
        long diff=now-time;
        if(diff>1000*60*5) // 5 minutes
            return true;
        return false;
    }
Then we just need to do standard I/O operations to read and write to this file. Here is our entire cache implementation. I have named it, MyCache.java.
package com.jdepths.alien;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Date;
import android.os.Environment;
import android.util.Log;

/**
 * Implements the caching mechanism used by our app
 *
 * author Hathy
 */
public class MyCache {
    
    // The cache directory should look something like this
    // Replace com.jdepths.alien with your own package name
    static private String cacheDirectory = 
                         "/Android/data/com.jdepths.alien/cache/"; 
    
    /*
     * Make sure the SD Card is available and we can write to it
     * Once confirmed, create the cache directory if it does not
     * exist yet
     */
    static {
        if(Environment.getExternalStorageState()
                      .equals(Environment.MEDIA_MOUNTED)){
            cacheDirectory=Environment.getExternalStorageDirectory()
                           +cacheDirectory;
            File f=new File(cacheDirectory);
            f.mkdirs();
        }
    }    
    
    static public String convertToCacheName(String url){
        try {            
            MessageDigest digest=MessageDigest.getInstance("MD5");
            digest.update(url.getBytes());
            byte[] b=digest.digest();
            BigInteger bi=new BigInteger(b);
            return "mycache_"+bi.toString(16)+".cac";            
        } catch (Exception e) {
            Log.d("ERROR", e.toString());
            return null;
        }
    }
    
    private static boolean tooOld(long time){
        long now=new Date().getTime();
        long diff=now-time;
        if(diff>1000*60*5)
            return true;
        return false;
    }
    
    public static byte[] read(String url){
        try{
            String file=cacheDirectory+"/"+convertToCacheName(url);
            File f=new File(file);
            if(!f.exists() || f.length() < 1) return null;
            if(f.exists() && tooOld(f.lastModified())){ 
                // Delete the cached file if it is too old
                f.delete();
            }
            byte data[]=new byte[(int)f.length()];
            DataInputStream fis=new DataInputStream(
                                   new FileInputStream(f));
            fis.readFully(data);
            fis.close();
            return data;
        }catch(Exception e) { return null; }
    }
    
    public static void write(String url, String data){
        try{
            String file=cacheDirectory+"/"+convertToCacheName(url);
            PrintWriter pw=new PrintWriter(new FileWriter(file));
            pw.print(data);
            pw.close();
        }catch(Exception e) { }
    }
}
I have used getExternalStorage() here, to support SDK versions older than Froyo. You can use getExternalCacheDir() too.

Now that we have implemented the caching mechanism, we need to use it. So, this requires changes in the RemoteData.java file. The method below changes as follows,
  • Instead of directly making the network connection, check if our cache has data.
  • If our cache does not return null, then simply return whatever it returned.
  • If it does return null, make the network connection, fetch the data, store it to our cache, and then return the data.
The comments below should aid you if this sounds confusing.
    /**
     * A very handy utility method that reads the contents of a URL
     * and returns them as a String.
     * 
     * @param url
     * @return
     */
    public static String readContents(String url){
        
        //Check if the cache contains data for this URL
        
        byte[] t=MyCache.read(url);
        String cached=null;
        if(t!=null) {
            cached=new String(t);
            t=null;
        }
        if(cached!=null) {
            Log.d("MSG","Using cache for "+url);
            return cached;
        }
        
        //The following will be executed only if the
        //cache did not contain data for this URL
        
        HttpURLConnection hcon=getConnection(url);
        if(hcon==null) return null;
        try{
            StringBuffer sb=new StringBuffer(8192);
            String tmp="";
            BufferedReader br=new BufferedReader(
                                new InputStreamReader(
                                        hcon.getInputStream()
                                )
                              );
            while((tmp=br.readLine())!=null)
                sb.append(tmp).append("\n");
            br.close();    
            
            // We now add this data to the cache
            MyCache.write(url, sb.toString());
            return sb.toString();
        }catch(IOException e){
            Log.d("READ FAILED", e.toString());
            return null;
        }
    }    
That is all you need to do. Build and deploy your app, and you should see a marked improvement in the performance of the app. However, you still need to add in your own logic to clear up this cache.

Do write back to me if you find anything confusing, or think something is wrong. Thanks for reading. Do come back for the next part of this tutorial.

4 comments:

  1. Hey How to open post from list view.
    Which url should we use?

    ReplyDelete
  2. hello can you tell how to open the reddit when clicks on the post

    ReplyDelete
    Replies
    1. Hi, you have to get the link from one of the properties and use a event click to launch a webview. That´s the most simple approach. I guess.

      Delete
  3. Greate tutorial man. Thanks a lot.

    ReplyDelete