This blog post was written under the Pusher Guest Writer program.

When I was a kid, my parents had a Kodak camera that they only used on vacations or special events. It used film rolls, and you had to take them to a specialty shop to have them developed so you could get your photos a few days later. Sometimes, we couldn’t even fill a 100-pocket photo album in an entire year.

Nowadays, the number of the photos we take has exploded exponentially. With cameras in even the most basic mobile phones, you can easily take hundreds of photos in a day without any issues. And sites like Instagram, Flickr, and 500px, among others, made specifically to share, comment, and like photos are very popular.

So why not build a feed to track a stream of photos in our Android device in realtime?

In this tutorial, we’re going to get the photos from Reddit (in particular, from the r/pics subreddit), taking advantage of the Pusher Realtime Reddit API.

To keep things simple, we’ll implement the feed without any other feature. This is how the final app will look:

This tutorial assumes a basic knowledge of how to make Android apps. If you need it, the source code of the final version of the application is available on Github.

Let’s get started!

Using the Pusher Realtime Reddit API

You can learn more about the Pusher Realtime Reddit API here, but basically the idea is that any subreddit has its own Pusher channel to which you can subscribe to get new listings events.

You can see an interactive code example of this on JSBin.

For our needs, we can try this simple Javascript snippet:

// Open a Pusher connection to the Realtime Reddit API
var pusher = new Pusher("50ed18dd967b455393ed");

// Subscribe to the pics subreddit (lowercase)
var subredditChannel = pusher.subscribe("pics");

// Listen for new stories
subredditChannel.bind("new-listing", function(listing) {
  // Output listing to the browser console
  console.log(listing);
});

The Pusher app key you have to use is 50ed18dd967b455393ed. Here’s a sample of the information that we can get from Reddit:

{
  approved_by: null,
  archived: false,
  author: "PHIL-yes-PLZ",
  author_flair_css_class: null,
  author_flair_text: null,
  banned_by: null,
  brand_safe: true,
  clicked: false,
  contest_mode: false,
  created: 1489494725,
  created_utc: 1489465925,
  distinguished: null,
  domain: "i.redd.it",
  downs: 0,
  edited: false,
  gilded: 0,
  hidden: false,
  hide_score: true,
  id: "5za4q7",
  is_self: false,
  likes: null,
  link_flair_css_class: null,
  link_flair_text: null,
  locked: false,
  media: null,
  media_embed: [object Object] { ... },
  mod_reports: [],
  name: "t3_5za4q7",
  num_comments: 0,
  num_reports: null,
  over_18: false,
  permalink: "/r/pics/comments/5za4q7/the_beauty_of_budding_stained_glass/",
  post_hint: "image",
  preview: [object Object] {
    enabled: true,
    images: [[object Object] {
  id: "rqR81Yj7Fud7Y8P94e8ZftEZyTEO4Q3ufVQ7f-9QNSM",
  resolutions: [[object Object] {
  height: 81,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=108&s=c174d33e47fa3e585c46622dfca12dd5",
  width: 108
}, [object Object] {
  height: 162,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=216&s=22711bde5e57c38f93e99de27bb2f1ee",
  width: 216
}, [object Object] {
  height: 240,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=320&s=a22e5a2857b40d0205e07724a89d4182",
  width: 320
}, [object Object] {
  height: 480,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=640&s=7dc827127272f6aa530faa8b29a8298f",
  width: 640
}, [object Object] {
  height: 720,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=960&s=8402002d064b3283742f8bc86163d552",
  width: 960
}, [object Object] {
  height: 810,
  url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=1080&s=ac9b66669198d0eb4bb8a47e1cc79e48",
  width: 1080
}],
  source: [object Object] {
    height: 2448,
    url: "https://i.redditmedia.com/Lw666GULI7dJMKrB3IKn8G95A0MuP-ztXwsmvIdhlsE.jpg?s=50a1044924ba1e0aa39a7f5f5ab33d8e",
    width: 3264
  },
  variants: [object Object] { ... }
}]
  },
  quarantine: false,
  removal_reason: null,
  report_reasons: null,
  saved: false,
  score: 1,
  secure_media: null,
  secure_media_embed: [object Object] { ... },
  selftext: "",
  selftext_html: null,
  spoiler: false,
  stickied: false,
  subreddit: "pics",
  subreddit_id: "t5_2qh0u",
  subreddit_name_prefixed: "r/pics",
  subreddit_type: "public",
  suggested_sort: null,
  thumbnail: "https://b.thumbs.redditmedia.com/JlIMJkuHQsCnp4Gn7h_OT2AedCJd_QQ-otJm1PUi1cc.jpg",
  title: "The beauty of budding stained glass.",
  ups: 1,
  url: "https://i.redd.it/vo690nyiwaly.jpg",
  user_reports: [],
  visited: false
}

With this in mind, let’s create the Android app.

The Android app

Open Android Studio and create a new project:

We’re not going to use anything special, so we can safely support a low API level:

Next, create an initial empty activity:

And use the default name of MainActivity with backward compatibility:

Once everything is set up, let’s install the project dependencies. In the dependencies section of the build.gradle file of your application module add:

dependencies {
    ...
    compile 'com.android.support:recyclerview-v7:25.1.1'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.pusher:pusher-java-client:1.4.0'
    compile 'com.google.code.gson:gson:2.4'
    ...
}

At the time of writing, the latest SDK version is 25, so that’s my target SDK version.

We’re going to use the RecyclerView component from the Support Library, so make sure you have it installed (in Tools -> Android -> SDK Manager -> SDK Tools tab the Android Support Repository must be installed).

To download the images we’re going to use Glide, one of the most popular open-source Android libraries for loading images.

By default, Glide uses a custom implementation of HttpURLConnection to load images over the network. This is what we’ll be using here. However, Glide also provides plugins to other popular networking libraries such as Volley or OkHttp, you just need to add the corresponding dependencies:

dependencies {
    ...
    compile 'com.github.bumptech.glide:glide:3.7.0'
    ...
    // Volley
    compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar'
    compile 'com.android.volley:volley:1.0.0'

    // okhttp 3
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
    compile 'com.squareup.okhttp3:okhttp:3.6.0'

    // okhttp 2
    compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar'
    compile 'com.squareup.okhttp:okhttp:2.7.2'
    ...
}

Sync the Gradle project so the modules can be installed and the project built.

Also, don’t forget to add the INTERNET permission to the AndroidManifest.xml file. This is required so we can connect to Pusher and get the events in realtime:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pusher.photofeed">

    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        ...
    </application>

</manifest>

Now, modify the layout file activity_main.xml so it looks like this:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.pusher.photofeed.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycler_view" />
</RelativeLayout>

We’re going to use a RecyclerView to display the images, which we’ll store in a list. Each item in this list is displayed in an identical manner, so let’s define another layout file to inflate them.

Create the file item.xml with the following content:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/photo"
        android:adjustViewBounds="true"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:layout_margin="2dp"
        android:layout_width="match_parent"/>

</LinearLayout>

Here, we’re just using an ImageView component to display the image, with a height of 200dp and a scaleType equal to centerCrop, to scale the image uniformly (maintain the image’s aspect ratio) so both dimensions (width and height) will be equal to or larger than the corresponding dimension of the view (minus padding), among other properties.

Now, to store the information for each image, which right now is just its URL, let’s create a class, com.pusher.photofeed.Photo:

public class Photo {

    private String url;

    public Photo(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }
}

RecyclerView works with an Adapter to manage the items of its data source (in this case a list of Photo instances), and a ViewHolder to hold a view representing a single list item, so first create the class com.pusher.photofeed.PhotoAdapter with the following code:

public class PhotoAdapter extends RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder> {

    private List<Photo> photos;
    private Context context;

    public PhotoAdapter(Context context, List<Photo> photos) {
        this.photos = photos;
        this.context = context;
    }

    public void addPhoto(Photo photo) {
        // Add the event at the beggining of the list
        photos.add(0, photo);
        // Notify the insertion so the view can be refreshed
        notifyItemInserted(0);
    }

    @Override
    public int getItemCount() {
        return photos.size();
    }
}

We initialize the class with a list of Photo instances and a Context (Glide will need it), provide a method to add Photo instances at the beginning of the list (addPhoto(Photo)) and then notify the insertion so the view can be refreshed, and implement getItemCount so it returns the size of the list.

Then, let’s add the ViewHolder as an inner class. It references the ImageView component for each item in the list:

public class PhotoAdapter extends RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder> {

    ...

    public static class PhotoViewHolder extends RecyclerView.ViewHolder {

        public ImageView photoImageView;

        public PhotoViewHolder(View v) {
            super(v);
            photoImageView = (ImageView) v.findViewById(R.id.photo);
        }
    }
}

And implement the methods onCreateViewHolder and onBindViewHolder:

public class PhotoAdapter extends RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder> {
    ...

    @Override


    public PhotoViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item, viewGroup, false);
        return new PhotoViewHolder(v);
    }

    @Override
    public void onBindViewHolder(PhotoViewHolder holder, int position) {
        Photo photo = photos.get(position);
        String url = photo.getUrl();

        Glide.with(context)
                .load(url)
                .asBitmap()
                .error(R.drawable.logo)
                .fitCenter()
                .into(holder.photoImageView);
    }
}

In the onCreateViewHolder method, we inflate the layout with the content of the event_row.xml file we created earlier, and in onBindViewHolder, we use Glide to fetch the image and display it in the ImageView of the item with the following method calls:

  • with(Context) initializes the loading processing passing the context.
  • load(String) loads the image from the specified URL.
  • asBitmap() makes sure that Glide receives an image that can be converted to a bitmap, otherwise the load will fail (for example if the URL represents an HTML page) and the Drawable passed to the error method will be shown instead.
  • error(Drawable) shows the Drawable if the load fails (in the GitHub version of this app, the Pusher logo, but you can add your own error image).
  • fitCenter() scales the image uniformly (maintaining the image’s aspect ratio) so the image will fit in the given area.
  • into(ImageView) specifies the target image view into which the image will be placed.

In the class com.pusher.photofeed.MainActivity, let’s start by defining the private fields we’re going to need:

public class MainActivity extends AppCompatActivity {
    private RecyclerView.LayoutManager lManager;
    private PhotoAdapter adapter;
    private Pusher pusher = new Pusher("50ed18dd967b455393ed");
    private static final String CHANNEL_NAME = "pics";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ....
    }
}

RecyclerView works with a LayoutManager to handle the layout and scroll direction of the list. We declare the PhotoAdapter, the Pusher object and the identifier for the Pusher channel.

Inside the onCreate method, let’s assign a LinearLayoutManager to the RecyclerView and create the EventAdapter with an empty list:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Get the RecyclerView
        RecyclerView recycler = (RecyclerView) findViewById(R.id.recycler_view);

        // Use LinearLayout as the layout manager
        lManager = new LinearLayoutManager(this);
        recycler.setLayoutManager(lManager);

        // Set the custom adapter
        List<Photo> photoList = new ArrayList<>();
        adapter = new PhotoAdapter(this, photoList);
        recycler.setAdapter(adapter);
    }
}

For the Pusher part, we first subscribe to the channel:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        Channel channel = pusher.subscribe(CHANNEL_NAME);
}

Then, we create the listener that will be executed when a photo arrives:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    ...
    SubscriptionEventListener eventListener = new SubscriptionEventListener() {
            @Override
            public void onEvent(String channel, final String event, final String data) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Received event with data: " + data);
                        Gson gson = new Gson();
                        Photo photo = gson.fromJson(data, Photo.class);
                        adapter.addPhoto(photo);
                        ((LinearLayoutManager)lManager).scrollToPositionWithOffset(0, 0);
                    }
                });
            }
        };
    }
}

Here, the JSON string that we receive is converted to a Photo object and is added to the adapter. Finally, we move to the top of the list.

Next, bind the events to this listener and call the connect method on the Pusher object:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        channel.bind("new-listing", eventListener);

        pusher.connect();
    }
}

The connect method can take a listener that can be helpful to debug problems you might have:

pusher.connect(new ConnectionEventListener() {
    @Override
    public void onConnectionStateChange(ConnectionStateChange change) {
        System.out.println("State changed to " + change.getCurrentState() +
            " from " + change.getPreviousState());
    }

   @Override
    public void onError(String message, String code, Exception e) {
        System.out.println("There was a problem connecting!");
        e.printStackTrace();
    }
});

Finally, MainActivity also needs to implement the onDestroy() method so we can have the opportunity to unsubscribe from Pusher when the activity is destroyed:

public class MainActivity extends AppCompatActivity {
    ...

    @Override
    public void onDestroy() {
        super.onDestroy();
        pusher.disconnect();
    }
}

And that’s it. Let’s test it.

Testing the app

Execute the app, either on a real device or a virtual one:

You’ll be presented with an almost blank screen:

When a new image is uploaded to Reddit, it will show up in the app (it may take a while, depending on the amount of activity at the time):

Conclusion

Remember that you can find the final version of the Android app here.

Hopefully, this tutorial has shown you how simple it is to build a realtime photo feed in Android and Pusher. You can improve the app by changing the design, showing more information, or saving it to a database. Remember that your forever free Pusher account includes 100 connections, unlimited channels, 200k daily messages, SSL protection, and there are more features than just Pub/Sub Messaging. Sign up here.

Further reading