A 5K Twitter Client


So as part of preparations for the 5K App I'm writing a few example apps. The first one so far is a 5K Twitter Client, which makes it smaller than the Twitter logo itself (at 5.4K).

Check out the demo video to see it in action or download 5KTwit (requires Java):



The features are:

  • Update twitter status
  • View friends timeline (new tweets checked for every 3 minutes)
  • Easily change logged-in user
  • Count of characters remaining when typing status message

It doesn't handle viewing direct messages, but I don't tend to use them. Plus I wanted to make sure the features I had were done fairly well.

Anyway, I thought it would be a good idea to walk through making a 5K app, as there's a fair bit of stuff just needs to be done to make it feasible. It's the sort of stuff you don't normally encounter daily when working on "real" software - unless you've been working on mobile or embedded apps. Nowadays most programmers don't really need to worry about the size of the code they write, so how to make something that's less that 5K in size isn't immediately obvious.

The Twitter app (which I've lovingly christened "5KTwit") is written in Java and runs as a standard client-side application. For some good pointers on how to make a Java app that's really tiny, but packs a punch you should probably start with these Java 4K tips.

Now for a few numbers:

  • Final (obfuscated) app size: 5119 bytes
  • Final (un-obfuscated) app size: 6400 bytes
  • Final Source-code size: 10398 bytes
  • Lines of code: 310

As you can see the difference between the obfuscated and un-obfuscated versions is about 1K, so obviously obfuscation is pretty key. So it's worth starting of with a good obfuscator. For those going "what's an obfuscator?" it's basically a tool to optimise, shrink and scramble your code. Originally they were intended to make code harder to decompile (hence "obfuscate"), but also have the added benefit of shrinking the compiled code to. I used Proguard and that's probably your best bet too.

Proguard does a pretty good job of shrinking your code, but it's worth reading up on how it works, so you can get an idea of what code changes you can make to get the most effect. For example Proguard will inline (and remove) small private functions for you (so they effectively have no overhead), but only if they are only called in one place. In general you probably want to use a source-control system/backup changes so you can rollback when a change doesn't result in smaller obfuscated code.

Here's the config file I used for Proguard (running on OS X):

-injars      dist/5KTwit.jar
-outjars     obfuscated/5KTwit.jar
-libraryjars <java.home>/../Classes/classes.jar
-optimizationpasses 2
-allowaccessmodification
-keep public class T  {
    public static void main(java.lang.String[]);
}

Proguard does a great job, but it won't stop you writing code that's bigger than it needs to be. In order to get the app under 5K I only used two classes (one for the main window and one for the username/password dialog) and one resource file. Each additional file has some overhead associated with it. Not only will each file tend to have some standard header info etc, but even if it was empty it would increase the size of the packaged jar file. So really cutting down on the number of files used makes a big difference. During testing I made a simple HelloWorld application and that was 700 bytes or so in size with a single class file and no real code. Most of that size would have been taken up with this sort of overhead.

As this app used swing for the GUI it also meant I had to be a bit creative about event handling. The norm is to have small event handling classes, but that would have incurred too much overhead. Instead I resorted to tricks like this:

public void actionPerformed(ActionEvent e) {
    if ( e.getSource() == prefs ) {
        start();
    }
    else {
        String status = this.status.getText();
        if ( status.length() <= MAX_CHAR ) {
            tweetsDoc = loadDocument("http://twitter.com/statuses/update.xml", status);
            loadTweets();
            this.status.setText("");
        }
    }
}

Where I checked the source of the event to see which button had been pressed, rather than having a separate event listener for each button.

I also had to deal with listening for window resize events, as there was some manual intervention required to make tweets wrap inside the scroll pane. So for that I circumvented the listener approach entirely and overrode processComponentEvent. In this code I had to try to estimate how big the panel containing the tweets would be, when I knew the width I wanted. So I resorted to a bit of nasty hackery and repeatedly set/unset the preferred size a few times so it would settle on the right height...


protected void processComponentEvent(ComponentEvent e) {
    if ( e != null ) { super.processComponentEvent(e); }

    // force width of tweets to match width of window
    for ( int i = 0; i < 3; i++ ) {
        tweets.invalidate();
        tweets.setPreferredSize(null);
        int height = tweets.getPreferredSize().height;
        tweets.setPreferredSize(new Dimension(scrollPane.getViewportBorderBounds().width,height));
        scrollPane.validate();
    }
}

Whereas in "real" code this would probably be done with a bit more finesse (and a few more classes).

Another tricky spot involved the background thread used to check for new tweets. This ran on another thread (rather than the main GUI thread), so as not to lock up the GUI when it runs (once every three minutes). However updating the GUI had to occur on the GUI thread. Normally this would simply be done by creating a few Runnable tasks, but that would have meant more classes. Instead I end up checking to see whether we're running on the GUI thread and change behaviour if that's the case:


public void run() {
    if ( SwingUtilities.isEventDispatchThread() ) {
        // update UI as running on the right thread
        loadTweets();
    }
    else {
        while( currentThread == Thread.currentThread() ) {
            String friendsTimelineURL = "http://twitter.com/statuses/friends_timeline.xml?count=200";
            if ( lastTweetID != null ) {
                friendsTimelineURL += "&since_id=" + lastTweetID;
            }
            tweetsDoc = loadDocument(friendsTimelineURL, null);
            try {
                // invoke and wait, to run loadTweets()
                // on awt thread
                SwingUtilities.invokeAndWait(this);
                Thread.sleep(1000*60*3); // 3 mins
            }
            catch( Exception e ) {
                // break loop
                break;
            }
        }
    }
}

There are probably quite a few other places where I had to make this sort of "design" choice. It's feasible to do for this little code, but I wouldn't want to do this kind of thing in a big project. Though I guess if it was really needed you could write something that would take nice well written code and convert it to use these sorts of tricks...

Apart from that, most of the rest of the work came down to carefully weighing up which features I wanted and carefully tweaking and re-organising the code to make it smaller. I also found that the single image (for the preference button) was a lot smaller when saved as a gif rather than a png (90 bytes vs ~200 bytes).

Anyway, for 5K it's not a bad little client. I've actually been using it for the past couple of days (as I've been writing it). I doubt it would really survive under the hands of a more active user, but it does the job.