Recently I got an idea for a mobile app which has sparked my motivation to learn how to make apps for Android. So I headed to the developer page and downloaded Android Studio. It’s been so long since I attempted programming anything for Android, the last time I did it was still done with ADT and Eclipse. So I’m pretty much starting at square one here. Unfortunately I ran into several problems just trying to get a simple “Hello World!” app to build and run. I could probably write a post just about setting that up.
The first test program I wanted to do was to try reading data from a website. The front page of Reddit seemed like a good candidate, so I went for that.
After I got my Hello World app working the first thing I did was check out the Getting Started section of the Android developer website. While it had a lot of good information it definitely seemed to assume that the reader has a pretty firm grasp on programming to begin with. A lot of the contents felt like they were more design or stylistically oriented than I was looking for. I wanted information of the “how do I read text from a text box?” variety rather than “how to structure my directories for accommodating screens of different sizes and pixel densities?” Luckily there was enough of the former to get started.
Thing 1: Change visible items with code
Once I got my fill of that orientation material I wanted to know how to change the contents on screen dynamically/programmatically. Easy enough to test out, just give a TextView
a name, and then in the onCreate
method just grab it and alter the contents.
activity_main.xml:
<RelativeLayout 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:padding="@dimen/activity_margin" tools:context=".MainActivity"> <TextView android:id="@+id/blank_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout>
MainActivity.java:
package com.example.travisl.reddittest; ... public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = (TextView) findViewById(R.id.blank_textview); textView.setText("Testing test"); } ... }
The layout code for the visual components in the activity_main.xml file is both strange and familiar looking at the same time. I’m used to creating WPF/Silverlight interfaces using XAML so the general structure is pretty similar. But some of the way things are done in this environment are different. Like declaring the ID of an element (ex: android:id="@+id/blank_textview"
) and accessing resource values (ex: @dimen/activity_margin
).
That wasn’t too painful. Here are the results on an emulator:
Thing 2: Add a library
Luckily there already exist several libraries for interacting with Reddit’s API. I just picked the first one in the list for Java: JRAW. Here was the first hurdle: how do I import a library for use in an Android application?
I found out that Android projects use Gradle for building and handling dependencies. I took a look at the “Quick Start Guide” hoping that it was as easy as adding a reference to a .dll file in C#. It unfortunately was a bit of a longer read than that. I took a quick scan through and it was starting to look like it was going give me as much trouble as CMake had given me a while back. I have no doubts about the usefulness and power of these tools, but when I just want to make a small proof-of-concept program the effort to learn a build system like Gradle feels disproportionate to what I’m actually trying to get at. I was afraid it was going to be a big roadblock in this ideally small project, turning it into this:
I needed a TL;DR
Luckily the JRAW project had some shorter instructions on the Quickstart page. After Googling a bit to find out where I need to put those pieces of code I found that
repositories { jcenter() }
Is actually already included in the build.gradle
file at the root of the Android Studio project. Then you need to add the compile "net.dean.jraw:JRAW:$jrawVersion"
line into the dependencies
section in the build.gradle
file inside the app folder.
I almost got away with it until this error popped up:
Error:duplicate files during packaging of APK E:\programming\android\RedditTest\app\build\outputs\apk\app-debug-unaligned.apk
Path in archive: META-INF/LICENSE
A quick search for the answer told me to add the following packagingOptions
to the build.gradle
file of my app (not the root one) to exclude the duplicate files:
android { packagingOptions { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' } }
Thing 3: Setup account and client info
From what I saw on the Quickstart page setting up a RedditClient
instance to let me read the first page would be a five-line process:
UserAgent myUserAgent = UserAgent.of("<platform>", "<appId>", "<version>", "<reddit username>"); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script("<username>", "<password>", "<clientId>", "<clientSecret>"); OAuthData authData = redditClient.getOAuthHelper().easyAuth(credentials); redditClient.authenticate(authData);
It looks simple enough but through lots of trial/error/Google I found a couple of important facts:
- You need to use a valid username/password combo.
- You need to supply a valid client ID/client secret combo.
The first requirement is easy enough if you just make a throwaway account. The second is a little tougher. I found out that you need to go to the authorized app preferences for the account and add an app to it. First click “create an app…” near the bottom and then 1) enter the name of the app 2) select the “script” radio button 3) enter a redirect URL.
You’ll get a client id/client secret from when you add the application:
So I had all my information and entered it into the method calls. I was all ready to go:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); UserAgent myUserAgent = UserAgent.of("android", "[REDACTED]", "0.0.1", "[REDACTED]"); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script("[REDACTED]", "[REDACTED]", "[REDACTED]", "[REDACTED]"); try { OAuthData authData = redditClient.getOAuthHelper() .easyAuth(credentials); redditClient.authenticate(authData); } catch (OAuthException e) { e.printStackTrace(); } }
I ran it and got an exception. android.os.NetworkOnMainThreadException
.
Thing 4: fix exceptions
Turns out that exception is caused by, just as it says, not running your network calls inside another thread. So I fixed that up and was ready to test again:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { @Override public void run() { UserAgent myUserAgent = UserAgent.of("android", "[REDACTED]", "0.0.1", "[REDACTED]"); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script("[REDACTED]", "[REDACTED]", "[REDACTED]", "[REDACTED]"); try { OAuthData authData = redditClient.getOAuthHelper() .easyAuth(credentials); redditClient.authenticate(authData); } catch (OAuthException e) { e.printStackTrace(); } } }).start(); }
Ran it again and got another exception: java.lang.SecurityException: Permission denied (missing INTERNET permission?)
That one is pretty easy to fix. It’s just one line in the AndroidManifest.xml file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.travisl.reddittest" > <uses-permission android:name="android.permission.INTERNET"/> ... </manifest>
So I ran it again and… no exceptions! Woooo! Now to move on to…
Thing 5: display post titles
I already proved I could change the text on a TextView dynamically in Thing 1. So I should just be able to grab the first post out of the list and set the contents of the TextView accordingly:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { @Override public void run() { UserAgent myUserAgent = UserAgent.of("android", "[REDACTED]", "0.0.1", "[REDACTED]"); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script("[REDACTED]", "[REDACTED]", "[REDACTED]", "[REDACTED]"); try { OAuthData authData = redditClient.getOAuthHelper() .easyAuth(credentials); redditClient.authenticate(authData); } catch (OAuthException e) { e.printStackTrace(); } SubredditPaginator paginator = new SubredditPaginator(redditClient); Listing<Submission> submissions = paginator.next(); Submission first = submissions.get(0); TextView textView = (TextView) findViewById(R.id.blank_textview); textView.setText(first.getTitle()); } }).start(); }
And another exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
The solution to that came from the android API guide about processes and threads. Like the Dispatcher
in C#/WPF you need to pass a callback to the appropriate thread for the UI to get updated. Luckily views have a post
method that takes in callbacks for what you want, since we need to get the view to update the text anyway we can just call it there.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { final String username = getString(R.string.username); final String password = getString(R.string.password); final String clientId = getString(R.string.client_id); final String clientSecret = getString(R.string.client_id); final String appId = getString(R.string.app_id); @Override public void run() { UserAgent myUserAgent = UserAgent.of("android", appId, "0.0.1", username); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script(username, password, clientId, clientSecret); try { OAuthData authData = redditClient.getOAuthHelper() .easyAuth(credentials); redditClient.authenticate(authData); } catch (OAuthException e) { e.printStackTrace(); } SubredditPaginator paginator = new SubredditPaginator(redditClient); Listing<Submission> submissions = paginator.next(); final Submission first = submissions.get(0); final TextView textView = (TextView) findViewById(R.id.blank_textview); textView.post(new Runnable() { @Override public void run() { String linkStr = String.format("%s↑ [/r/%s] - %s", first.getScore(), first.getSubredditName(), first.getTitle()); textView.setText(linkStr); } }); } }).start(); }
There are a few changes in this chunk of code. First of all I started grabbing the username/password/clientid/etc from the strings dictionary. Not just because it’s good practice but because I kept copying/pasting the actual values into this post while writing it and didn’t want to forget to change them to "[REDACTED]"
. The second change is that the first
and textView
variables are now declared as final
. This is necessary to prevent the values from being changed before the callback handed to post
actually runs. I also formatted the text that should display on screen to be a little prettier. I added in score and subreddit along with the title of the post.
And best of all it actually works!
Which leaves just one last thing I want to do for this proof of concept.
Thing 6: show all links on the front page
This part feels like the wind-down step. Most of the hard/new/difficult things have already been accomplished. There are really only two things needed for this part.
- Create TextViews in the code behind and insert them into…
- A scrollable area to see every link since they likely won’t all render on a single screen.
I tackled part 2 first. I looked up info about ScrollViews
in the API docs, then modified the layout accordingly:
activity_main.xml:
<RelativeLayout 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" tools:context=".MainActivity"> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/link_textview_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="@dimen/activity_margin" android:orientation="vertical"> </LinearLayout> </ScrollView> </RelativeLayout>
Then the code behind, MainActivity.java:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread(new Runnable() { final String username = getString(R.string.username); final String password = getString(R.string.password); final String clientId = getString(R.string.client_id); final String clientSecret = getString(R.string.client_secret); final String appId = getString(R.string.app_id); @Override public void run() { UserAgent myUserAgent = UserAgent.of("android", appId, "0.0.1", username); RedditClient redditClient = new RedditClient(myUserAgent); Credentials credentials = Credentials.script(username, password, clientId, clientSecret); try { OAuthData authData = redditClient.getOAuthHelper() .easyAuth(credentials); redditClient.authenticate(authData); } catch (OAuthException e) { e.printStackTrace(); } SubredditPaginator paginator = new SubredditPaginator(redditClient); final Listing<Submission> submissions = paginator.next(); final LinearLayout layout = (LinearLayout) findViewById(R.id.link_textview_holder); layout.post(new Runnable() { @Override public void run() { float scale = getResources() .getDisplayMetrics().density; for(Submission link : submissions) { String linkStr = String.format( "%s↑ [/r/%s] - %s", link.getScore(), link.getSubredditName(), Html.fromHtml(link.getTitle())); TextView textView = new TextView(getContext()); textView.setText(linkStr); textView.setPadding(0, 0, 0, (int)(8*scale)); layout.addView(textView); } } }); } }).start(); } public Context getContext() { return this; }
If you want to do padding measured in dp in the code behind you need to get the density at runtime, hence lines 39-40. Also notice the getContext()
method that gets called when constructing each TextView
. I tried calling getApplicationContext()
but that caused the foreground color of the TextView
to be white. It probably has something to do with themes but I didn’t want to get into that in this project.
It worked out pretty well, there were no build issues or exceptions. Here’s a screen capture of the result: