Saturday, 22 December 2012

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

There are so many popular Reddit clients for Android out there like Reddit News, Bacon Reader, and Reddit is Fun. You might have sometimes wondered how these are made. Well, today we will be making a very basic Reddit client.

First of all, you need to know that Reddit exposes an API using which you can perform activities like fetching posts, fetching comments, voting, commenting et cetera. Today however, we will just be using the fetching posts part of the API.

Pre-requisites
  1. Basic knowledge of Android development
    Not asking for much here. You just need to know how to create an Android project in Eclipse, and know what activities and fragments mean.

  2. Knowledge of JSON
    I guess anybody who knows XML will understand JSON. If you understand what key value pairs are, you already know some JSON.
Let us get started now

To access Reddit using its JSON API, all you need to do is append .json to the URL, before the query string begins, i.e., before the ? in the URL (if any.)
For instance,
http://www.reddit.com/r/AskReddit/
becomes
http://www.reddit.com/r/AskReddit/.json
You will get some JSON data like,
{

 "kind": "Listing", 
 "data": {"modhash": "", 
          "children": [{"kind": "t3", "data": {"domain": "self.AskReddit", "banned_by": null, "media_embed": {}, "subreddit": "AskReddit", "selftext_html": null, "selftext": "", "likes": null, "link_flair_text": null, "id": "15a8k4", "clicked": false, "title": "What do you predict will be the biggest news story of 2013?", "num_comments": 5974, "score": 1641, "approved_by": null, "over_18": false, "hidden": false, "thumbnail": "", "subreddit_id": "t5_2qh1i", "edited": false, "link_flair_css_class": null, "author_flair_css_class": null, "downs": 2867, "saved": false, "is_self": true, "permalink": "/r/AskReddit/comments/15a8k4/what_do_you_predict_will_be_the_biggest_news/", "name": "t3_15a8k4", "created": 1356222561.0, "url": "http://www.reddit.com/r/AskReddit/comments/15a8k4/what_do_you_predict_will_be_the_biggest_news/", "author_flair_text": null, "author": "brostep19", "created_utc": 1356193761.0, "media": null, "num_reports": null, "ups": 4508}}],
          "after": "t3_15a8k4",
          "before": null
         }

}
We are interested in the children array and the after string. The children array contains all the posts (with various details about them like the post title, the number of comments, its score, et cetera.)

Now create your Android project, and then we write some code.

First, let us create a utility class that connects to the network and reads data from a URL. I have named it RemoteData.java
package com.jdepths.alien;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import android.util.Log;

/**
 * This class shall serve as a utility class that handles network
 * connections.
 * 
 * @author Hathy
 */
public class RemoteData {
    
    /**
     * This methods returns a Connection to the specified URL,
     * with necessary properties like timeout and user-agent
     * set to your requirements.
     * 
     * @param url
     * @return
     */    
    public static HttpURLConnection getConnection(String url){
        System.out.println("URL: "+url);
        HttpURLConnection hcon = null;
        try {            
            hcon=(HttpURLConnection)new URL(url).openConnection();
            hcon.setReadTimeout(30000); // Timeout at 30 seconds
            hcon.setRequestProperty("User-Agent", "Alien V1.0");
        } catch (MalformedURLException e) {
            Log.e("getConnection()",
                  "Invalid URL: "+e.toString());
        } catch (IOException e) {
            Log.e("getConnection()",
                  "Could not connect: "+e.toString());
        }
        return hcon;        
    }
    
    
    /**
     * 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){        
        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();                        
            return sb.toString();
        }catch(IOException e){
            Log.d("READ FAILED", e.toString());
            return null;
        }
    }    
}
The class is pretty self-explanatory. The readContents() method accepts a URL and returns a String, which contains the contents of the URL. This method of course, cannot be used to download binary data like images, but for the JSON API, it works perfectly.

Next, we need to create a class that represents a post. It should have properties like those that are present in the JSON data.
So, here is Post.java
package com.jdepths.alien;

/**
 * This is a class that holds the data of the JSON objects
 * returned by the Reddit API.
 * 
 * @author Hathy
 */
public class Post {
    
    String subreddit;
    String title;
    String author;
    int points;
    int numComments;
    String permalink;
    String url;    
    String domain;
    String id;
    
    String getDetails(){
        String details=author
                       +" posted this and got "
                       +numComments
                       +" replies";
        return details;    
    }
    
    String getTitle(){
        return title;
    }
    
    String getScore(){
        return Integer.toString(points);
    }
}
Let us now create a class that makes use of the above two classes to actually use the API. This is probably the most important class for today. It maintains a list of posts of a given subreddit, and also the after string, so that if necessary it can load more posts. I call this class PostsHolder.java
package com.jdepths.alien;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

import android.util.Log;

/**
 * This is the class that creates Post objects out of the Reddit
 * API, and maintains a list of these posts for other classes
 * to use.
 * 
 * @author Hathy
 */
public class PostsHolder {    
        
    /**
     * We will be fetching JSON data from the API.
     */
    private final String URL_TEMPLATE=
                "http://www.reddit.com/r/SUBREDDIT_NAME/"
               +".json"
               +"?after=AFTER";
    
    String subreddit;
    String url;
    String after;
    
    PostsHolder(String sr){
        subreddit=sr;    
        after="";
        generateURL();
    }
    
    /**
     * Generates the actual URL from the template based on the
     * subreddit name and the 'after' property.
     */
    private void generateURL(){
        url=URL_TEMPLATE.replace("SUBREDDIT_NAME", subreddit);
        url=url.replace("AFTER", after);
    }
    
    /**
     * Returns a list of Post objects after fetching data from
     * Reddit using the JSON API.
     * 
     * @return
     */
    List<Post> fetchPosts(){
        String raw=RemoteData.readContents(url);
        List<Post> list=new ArrayList<Post>();
        try{
            JSONObject data=new JSONObject(raw)
                                .getJSONObject("data");
            JSONArray children=data.getJSONArray("children");
            
            //Using this property we can fetch the next set of
            //posts from the same subreddit
            after=data.getString("after");
            
            for(int i=0;i<children.length();i++){
                JSONObject cur=children.getJSONObject(i)
                                    .getJSONObject("data");
                Post p=new Post();
                p.title=cur.optString("title");
                p.url=cur.optString("url");
                p.numComments=cur.optInt("num_comments");
                p.points=cur.optInt("score");
                p.author=cur.optString("author");
                p.subreddit=cur.optString("subreddit");
                p.permalink=cur.optString("permalink");
                p.domain=cur.optString("domain");
                p.id=cur.optString("id");
                if(p.title!=null)
                    list.add(p);
            }
        }catch(Exception e){
            Log.e("fetchPosts()",e.toString());
        }
        return list;
    }
    
    /**
     * This is to fetch the next set of posts
     * using the 'after' property
     * @return
     */
    List<Post> fetchMorePosts(){
        generateURL();
        return fetchPosts();
    }
}
Okay, till now this has been pure Java, and you can use these classes anywhere to make a Java based client.
Let us put in the Android based code now. We will need a FragmentActivity and a Fragment. First, the XML layouts for these. The activity (for this part of the tutorial) has no functionality. It merely acts a fragment holder. Here is its layout (activity_main.xml),
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fragments_holder"
    >

</LinearLayout>
As you can see, it is an empty LinearLayout. The fragments will be added to this layout. Next, the XML for the fragment has to be created. I have named it posts.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/posts_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

</LinearLayout>
The last XML will represent the items of this list. Each item, being a post, will have three TextViews. One each for the title, details and score. I have named this XML post.xml post_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/post_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@+id/post_score"
        android:layout_alignParentTop="true"
        android:textAppearance="?android:attr/textAppearanceMedium"
        />

    <TextView
        android:id="@+id/post_details"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@+id/post_score"
        android:layout_below="@+id/post_title" 
        android:textAppearance="?android:attr/textAppearanceSmall"
        />

    <TextView
        android:id="@+id/post_score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:textAppearance="?android:attr/textAppearanceLarge"
        />    

</RelativeLayout>
So, those were the XMLs. Like I said, this is a very basic (and possibly not very good-looking) client. Now, we will create the Fragment. All it does is, use the PostsHolder class to fetch the posts and then render those posts in a ListView. We need to separate the UI related parts of the code from the functionality so that our app can handle orientation changes efficiently.

We make use of setRetainInstance(true). This maintains the fragment and its state in the memory when the activity gets destroyed on orientation changes. This means, any posts that we have loaded stay in the memory. But the UI is rendered again, so, on orientation changes, we must only map the data to the UI (more like, set the Adapter to the ListView.) Here is the class, PostsFragment.java
package com.jdepths.alien;

import java.util.ArrayList;
import java.util.List;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

/**
 * While this looks like a lot of code, all this class
 * actually does is load the posts in to the listview.
 * 
 * @author Hathy 
 */
public class PostsFragment extends Fragment{
        
    ListView postsList;
    ArrayAdapter<Post> adapter;
    Handler handler;
    
    String subreddit;
    List<Post> posts;
    PostsHolder postsHolder;
    
    PostsFragment(){
        handler=new Handler();
        posts=new ArrayList<Post>();
    }    
    
    public static Fragment newInstance(String subreddit){
        PostsFragment pf=new PostsFragment();
        pf.subreddit=subreddit;
        pf.postsHolder=new PostsHolder(pf.subreddit);        
        return pf;
    }
    
    @Override
    public View onCreateView(LayoutInflater inflater,
                             ViewGroup container,
                             Bundle savedInstanceState) {
        View v=inflater.inflate(R.layout.posts
                                , container
                                , false);
        postsList=(ListView)v.findViewById(R.id.posts_list);
        return v;
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
    
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {    
        super.onActivityCreated(savedInstanceState);
        initialize();
    }
    
    private void initialize(){
        // This should run only once for the fragment as the
        // setRetainInstance(true) method has been called on
        // this fragment
        
        if(posts.size()==0){
            
            // Must execute network tasks outside the UI
            // thread. So create a new thread.
            
            new Thread(){
                public void run(){
                    posts.addAll(postsHolder.fetchPosts());
                    
                    // UI elements should be accessed only in
                    // the primary thread, so we must use the
                    // handler here.
                    
                    handler.post(new Runnable(){
                        public void run(){
                            createAdapter();
                        }
                    });
                }
            }.start();
        }else{
            createAdapter();
        }
    }
    
    /**
     * This method creates the adapter from the list of posts
     * , and assigns it to the list.
     */
    private void createAdapter(){
        
        // Make sure this fragment is still a part of the activity.
        if(getActivity()==null) return;
        
        adapter=new ArrayAdapter<Post>(getActivity()
                                             ,R.layout.post_item
                                             , posts){
            @Override
            public View getView(int position,
                                View convertView,
                                ViewGroup parent) {

                if(convertView==null){
                    convertView=getActivity()
                                .getLayoutInflater()
                                .inflate(R.layout.post_item, null);
                }

                TextView postTitle;
                postTitle=(TextView)convertView
                          .findViewById(R.id.post_title);

                TextView postDetails;
                postDetails=(TextView)convertView
                            .findViewById(R.id.post_details);

                TextView postScore;
                postScore=(TextView)convertView
                          .findViewById(R.id.post_score);

                postTitle.setText(posts.get(position).title);
                postDetails.setText(posts.get(position).getDetails());
                postScore.setText(posts.get(position).getScore());
                return convertView;
            }
        };
        postsList.setAdapter(adapter);
    }
        
}
There, we just created a reusable Fragment which can become a part of any Activity. Now, let us create a simple FragmentActivity that houses this fragment. I will name it MainActivity.java
package com.jdepths.alien;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

/**
 * As of now, all this activity does is create and
 * render a fragment.
 * 
 * @author Hathy
 */
public class MainActivity extends FragmentActivity {
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        addFragment();
    }
    
    void addFragment(){
        getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.fragments_holder
                 , PostsFragment.newInstance("askreddit"))
            .commit();
    }
}
For this part of the tutorial, I am just passing a string askreddit to our fragment. We are now pretty much done. Just don't forget to add the INTERNET permission in your manifest file. Run it, and you will see something like this on the emulator,
A basic Reddit Client for Android
Phew! That was a lot of coding, for albeit simple functionality. Java is a verbose language, and so is the Android SDK. Let me know what you think of this tutorial. There are a lot of optimizations that can be done, and a lot more functionality is to be added. Part II of this tutorial is coming soon, where we will implement features like a caching mechanism and a slide to change the subreddit. Read Part II of this tutorial now.