5 tips for building usable cross-platform apps

PhoneGap, and the new PhoneGap build, make it really easy to publish an app. So easy, in fact, that if your app is simple enough, you can create and publish within a day. Last week, I did just that on request from a user.

This ease of publishing, however, can lure you into building terrible, unintuitive apps. In working on my side project, I ran into a number of problems, one after another. Until I hit the problems, they were hard to see or easy to justify around. Don’t make my mistakes.

1.) Respect the Android back button

Your sexy HTML5 app has menus, and screens, and all sorts of things the user can play with. And then the user hits the hard back button on their sexy new android device. Your app closes. Despite how sexy your app is, it’s about to get removed. As an iPhone user, this wasn’t immediately obvious to me. I put in soft back buttons, so what’s the issue, right?

Well, Android users really like that button, and when it doesn’t work like they expect, they’re 1.) angry and 2.) confused. You don’t want angry and confused customers, do you? For a good discussion of how the back button is supposed to work and flow, check out the responses to my question on ux.stackexchange.com. The images and descriptions are better than what I could summarize here, and the rabbit hole is deeper than you imagine.

2.) Support landscape

This one through me for a loop, and I’m still working on this for my own app. Why landscape, you ask? I’ve had several users complain that they can’t interact with several parts of the app because of dead spots on their android device. Dead spots? DEAD SPOTS? This isn’t the 90s, no, but the quality of android devices vary wildly, and not everyone replaces their expensive phone-computer-toy-thing when it gets damaged. Supporting landscape mitigates the old-phone issue and expands your user base. The facebook app responds surprisingly well to orientation change, and there’s a lot going on in there. Are you doing more than facebook? I doubt it. Support landscape.

3.) Build feedback directly into your app

But wait, what about those support emails the damned app-stores make me provide? Well, users don’t give a shit about those support addresses. This isn’t unfounded hyperbole; statistically, they really don’t give a shit.

Before I added feedback links into my app, I had a feedback rate of about 0.05%-0.2% of the user base per week. Now, it’s more like 0.2% per day, more than a 7x increase in feedback. This list is the result of direct user feedback, and feedback is good. Does someone think your update is shit and can’t use it any more? Maybe you should find out why. Instead of an uninstall and a 1-star review, respond directly, fix the problems, and keep going. User retention is possible.

4.) Stop explaining and start redesigning

Now that you’ve setup a feedback system for your app (you have already, haven’t you?), users are pestering you about feature X. Feature X is so simple, but you keep needing to explain how it works for some reason. What gives? Well, to be blunt, your design sucks. In my case, my design sucked. Admit to it. Own your failure, and do something about it. In my case, users were getting trapped on an edit screen, not knowing that the save button ended “edit mode”. The concept was bad, and by removing the button and making every edit action end edit mode, the problem reports stopped.

But what about mass editing, you say? Stupid question. That’s not a real use case that aligns with my particular application, and you should be thinking about your own design. Build your application around how people use it, rather than at people using it. Imaginary use cases don’t help your application, so try not to spend time supporting imaginary users. Imaginary users don’t have credit cards or watch your ads, right? Right.

But how do I know what my users are doing…?

5.) ANALYTICS!

Analytics is the only way to really know how users use your app. I tried to skirt around this by relying on feedback, and in a single update, I made the app unusable for a significant portion of the user base. I removed an option which I thought absolutely-at-odds with the purpose of the app, but I was wrong. Really wrong. Angry “You suck at apps” comments wrong.

We already do this for our web applications, so what makes mobile apps different? Analytics tells you what’s used and when, so you know when you’re wrecking your application on an update. And hey, maybe the graphs of user patterns will let you more strategically jam ads into your application, or something like that.


So go forth, brave developer, and build something worth using. Even if you fail…and you might, own your failure and keep going!

Fast automated tests for your HTML5 app

HTML5 is sexy, and the promise of the cross-compatible (ish) app is alluring. If you’re making more than a soundboard, though, You Need Tests.

I’ve been using cucumber for a while, since it most easily integrates with RSpec, and to run tests, all I need to do is type cucumber.

I don’t want rails integration.
I don’t want database migration support.
I do want a testing framework that can run against a single HTML file.

So I use cucumber.

After adding my last feature, though, I was smacked in the face by this:

After every new feature, I either need to run a 6-minute (and growing) test-run. That’s crazy. Surely there’s an easy solution to get tests to run in parallel, right?

Well…no. Not for us. There are gems out there, but they all implode when they can’t find rails, because everyone is running rails, right?

Then I wondered if cucumber processes could properly manage different chrome instances via chromedriver in parallel, and a quick terminal test proved they could. Huzzah! Now we’re getting somewhere.

Since I already use gradle to manage my build activity, all I needed to do was add this:

task test() << {
    List tests = []
    new File('./test').eachFileRecurse {
        if (it.name.endsWith('.feature')) {
            tests << it
        }
    }

    GParsPool.withPool(4) {
        tests.eachParallel { File test ->
            exec {
                commandLine= ['cucumber', test.getAbsolutePath(), '-f', 'progress']
                workingDir = new File('./test')
            }
        }
    }
}

And voila! 2 minutes 2.3 seconds when running in parallel. No gems required, just some simple gradle-foo. With tests completing 3x faster, I can hold off setting up a grid of machines until another day.

PEBKAC my ass. Your software is terrible.

It seems popular in developer circles to attack users for being stupid, slow, or unable to use software. I can’t even count the number of times I’ve heard things like…

This is your product.

  • She was clicking the image!
  • Well they should be able to figure out the syntax from the help menu. That’s what it’s for. Duh.
  • They keep clicking the wrong button. What idiots!
  • Is the “back” button so hard to use?
  • Why did he put the window on a second monitor?

Have you heard anything like that recently? Users are terrible, and the Problem Exists Between Keyboard And Chair, right?

Nah, it’s just that your product blows donkey ass.

Off the top of your head, can you think of the smoothest software product you’ve ever used?

The question is pretty tough, because it’s hard to recall applications that get out of the way while we do work. KdenLive and Gimp are excellent examples of products with terrible users. Want to align some text, or crop a video? Good luck. I’d wager you’re “too dumb” to do it.

Gmail, for all its debated new UX decisions, goes out of its way to make sure the user doesn’t feel dumb, or terrible, or unable to use the service.

Oh, now I remember! Thanks, Gmail.

How nice is that?

This one small bit of information greatly alleviates a serious pain point – changing a password and forgetting you did it. Users aren’t dumb for forgetting they changed their passwords. No, gmail is smart for helping users remember they did.

But…but…but…development time costs money, and we have features to ship! If we don’t get out the 3D rotating spinner-loaders, our competitors will steal all our customers!

Maybe, but I want you to ask yourself some questions, and answer them honestly.

  • How much time/money am I spending on customer support?
  • How much time/money am I spending on training users to use the software?
  • How much time/money is my customer losing working around our usability problems?
  • Do I want to use my own software?
  • Do I want to use my competitor’s software?

So here’s the point I want to emphasize: usability is a feature. You may delude yourself into thinking that users like shiny or feature X, and that’s why you’re losing your customer base, but maybe there’s something more systematic going on. Every time a usability issue isn’t addressed, and every time you respond to a user problem with an elaborate explanation of how the software works, or paid training, you’re making your product just a little bit shittier. I’m going to make this next point big and clear.

If a feature of your product is impossible to find or frustrating to use, it will do less good than if it didn’t exist at all.

Will quickly and shittily implementing a certain feature win big contracts? Quite possibly, but is this model sustainable? I don’t think so. New products succeed because they address a different problem space than others in the market or they make an existing concept or product much simpler, easier, or cheaper to use. By continuously pushing features without rigorously controlling the user experience, you’re putting a giant target on your product. You’re telling every developer and entrepreneur that you’re just waiting for someone to sweep in and steal your customer base. Where once you were disruptive, you’re now primed for disruption.

I think your industry needs disruption

Every dollar spent on customer support and training is another straw on the camel’s back of your product. Good user experience is a real feature, and it’s worth more than every 3D animated spinner you’ll create to sell to a Fortune 500. Hire designers, do usability research, and iterate. For the love of god iterate. Shitty products can be made un-shitty, and it’s never too late to change direction and focus on user experience.

At the end of the day, you can build a sustainable awesome product, or you can shovel shit and exit before your company or product inevitably fails. 37 Signals does it right, but don’t take my word for it.

Now go build something cool.

HTML5 in, binaries out. Playing with the PhoneGap:Build API

If you haven’t heard of the new sexiness that is PhoneGap:Build, it’s a cloud service for building your HTML5/JS phonegap applications (phonegap optional) in the best way possible. To create a new application, you setup your signing certificates, setup a git remote for the private git repository created for you, and then any pushes to the phonegap remote trigger builds.

That’s awesome, but there’s still the sticky business of needing to login, click through menus, and choose download locations for your binaries. With iPhone and Android, that’s 2, and with Windows Phone support possible in the future, that’s 3 separate binaries you need to wait for and download individually.

That sucks, especially if you’re iterating quickly or have automated test systems setup; my guess is that one of these two, or both, is true for just about everyone.

But wait! There’s an API.

First, we need a token to make any authenticated calls. Simple enough.

curl -u stefankendall@gmail.com -X POST -d “” https://build.phonegap.com/token

returns

{"token":"a1234ca3"}

Looking through the read API, it looks like the ‘apps’ endpoint will give what I’m looking for.

curl https://build.phonegap.com/api/v1/apps?auth_token=a1234ca3

returns

{
    "apps": [
        {
            "build_count": 88,
            "debug": false,
            "description": "Helps track and manage the 5/3/1 lifting program designed by Jim Wendler. Set your one-rep maxes, and just get\n        lifting.",
            "download": {
                "android": "/api/v1/apps/59123/android",
                "ios": "/api/v1/apps/59123/ios",
                "symbian": "/api/v1/apps/59123/symbian",
                "webos": "/api/v1/apps/59123/webos"
            },
            "error": {
                "blackberry": "Invalid config.xml - failed to parse the version attribute in the  element"
            },
            "icon": {
                "filename": "icon-114x114.png",
                "link": "/api/v1/apps/59123/icon"
            },
            "id": 59123,
            "link": "/api/v1/apps/59123",
            "package": "com.stefankendall.wendler531",
            "phonegap_version": "1.3.0",
            "private": true,
            "repo": "git@git.phonegap.com:stefankendall/59123_Wendler531.git",
            "role": "admin",
            "status": {
                "android": "complete",
                "blackberry": "error",
                "ios": "complete",
                "symbian": "complete",
                "webos": "complete"
            },
            "title": "Wendler 5/3/1",
            "version": "21"
        }
    ],
    "link": "/api/v1/apps"
}

Huzzah! Download locations. Let’s try ‘em.

curl https://build.phonegap.com/api/v1/apps/59123/android?auth_token=a1234ca3

returns

{"location":"http://s3.amazonaws.com/android.phonegap/slicehost-production/apps/59123/Wendler5_3_1-release.apk"}

And there we have it. So all we need to do is codify the requests, and we get our build.

def execAndGetResult(def command){
    def output = new ByteArrayOutputStream()
    exec {
        commandLine = command
        standardOutput = output
    }
    return new JsonSlurper().parseText(output.toString())
}

final String PHONEGAP_BUILD_API_URL = "https://build.phonegap.com"
String authToken = null
task getAuthToken() {
    def authenticationResponse = execAndGetResult(['curl', '-u', 'stefankendall@gmail.com', '-X', 'POST', '-d', '""', "${PHONEGAP_BUILD_API_URL}/token"])
    authToken = authenticationResponse.token
}

task binaries(dependsOn: [getAuthToken]) << {
    def allAppsResult = execAndGetResult(['curl', "${PHONEGAP_BUILD_API_URL}/api/v1/apps?auth_token=${authToken}"])

    String getIosDownloadUrlRequest = allAppsResult.apps[0].download.ios
    String getAndroidDownloadUrlRequest = allAppsResult.apps[0].download.android

    [getIosDownloadUrlRequest, getAndroidDownloadUrlRequest].each {
         def downloadLocationResult = execAndGetResult(['curl', "${PHONEGAP_BUILD_API_URL}${it}?auth_token=${authToken}"])
         exec {
             commandLine = ['wget', downloadLocationResult.location, '-P', 'binaries']
         }
    }
}

task buildStatus(dependsOn: [getAuthToken]){
    def allAppsResult = execAndGetResult(['curl', "${PHONEGAP_BUILD_API_URL}/api/v1/apps?auth_token=${authToken}"])
    println JsonOutput.prettyPrint(JsonOutput.toJson(allAppsResult.apps[0].status))
}

And then...

gradle binaries

Binaries delivered right to ./binaries. How cool is that? If you're really ambitious, you could tie together the buildStatus and binaries task to wait on pending builds, or tie a deploy task to both and have end-to-end build-to-binaries.

It's surprisingly refreshing to desire a feature of a product and have it both 1.) exist and 2.) be easy to use.

In closing:

Using AppMobi? Stop immediately.

Up until this week, I’ve been using AppMobi to turn my Sencha Touch application into a packagable app. I had to deal with bloated file size and weird hacks to make startup work, but it’s much worse than that, friends.

PhoneGap Build is a cloud service for building Android, iOS, and some other platforms you’ve never heard of before, but unlike appMobi, it doesn’t do things in the worst way possible.

So what can I do with PhoneGap Build that I couldn’t do with AppMobi?

  • Use my own Android apk signing certificates
  • But what does this mean, exactly? Well, if you ever way to switch from appMobi to someone else, you can’t. In order for the Android market to allow an APK to be published as an “upgrade”, it has to be signed with the same private key as the first APK uploaded for a given application. This makes sense from a security standpoint, but maybe the Android Market support team could fix this? Nope.

    Thank you for your note. If you’ve lost or don’t have access to your keystore you’ll have to publish the app with a new package name and a new key. There is no alternate workaround available. You should also update the description of the original app and unpublish it. Please note that we do not support the deletion of apps or the re-use of package names.

    If we can assist you further, please let us know.

    Regards,
    The Android Market Team

    To the support team’s credit, I received a response within an hour of inquiry; appMobi still hasn’t responded.

  • Publish for tablets, or Android 2.2
  • The official appMobi stance is we don’t support tablets. Built applications also seem to crash randomly on Android 2.x devices, an issue phonegap doesn’t have.

  • Publish to the Amazon app store
  • Up until now, I’ve been unable to get through the Amazon application testing required to publish to the Amazon app store. HTC devices crash, along with the Galaxy Tab. Since Amazon doesn’t allow for exclusion lists, using appMobi will cut off an entire market of your users.

  • One-click builds
  • With appMobi, you literally need to click through 10-15 HTML pages to publish an app, and then you need to wait on a page for an APK or ZIP file to become available, for android or iOS devices respectively. Whenever I want to update my application, it takes around 10-15 minutes just to get binaries.

    How do I do it with PhoneGap Build? I’ll show you.

    gradle deploy

    I’ve setup my build directory to point at the PhoneGap Build git repository from which builds pull.

    task deploy(dependsOn: [build]) << {
        exec {
            commandLine = ['git', 'add', '.']
            workingDir = new File('./build')
        }
        exec {
            commandLine = ['git', 'commit', '-m', '"Building"']
            workingDir = new File('./build')
        }
        exec {
            commandLine = ['git', 'push', 'build', 'master']
            workingDir = new File('./build')
        }
    }
    

    With almost no code at all, I can build for 5 platforms at once. Check it out:

    Clicking each of these buttons triggers a download of the respective binary. Isn't that beautiful? There's even an API to work with the build system, and one of the v1.0 endpoints lets you download binaries. My brain is going to explode.

  • Control your app icon
  • With appMobi, you submit a 72x72 app icon for the app store. End of story. But wait, what about the iPhone 4S or newer Android devices? Don't they support higher resolution icons?

    Yup. If you're using appMobi, you're fucked. PhoneGap Build lets you define your resources in a config.xml file, and it's easy. Really easy.

    <icon src="icons/icon-57x57.png" width="57" height="57"/>
    <icon src="icons/icon-72x72.png" width="72" height="72"/>
    <icon src="icons/icon-114x114.png" width="114" height="114"/>

  • Explicitly request your own android permissions
  • Don't need access to contacts or the camera? Yeah, me neither, but appMobi forces you to request everything. There's no way to control the access list. With PhoneGap Build, you can specify permissions in the config.xml file mentioned earlier.

    <preference name="permissions" value="none"/>

  • Set the Android Market version number
  • If you care about your branding, this may be important. With appMobi, your application's visible version is always 3.4.0. This is sloppy and unprofessional at best, and confusing for users at worst. "Why should I download the upgrade if the version hasn't changed?"

  • Use PhoneGap!
  • There are a number of tools built to work with phonegap, and building with phonegap gives you access to all of it.

  • Tie myself to an established company
  • While cruel, this is crucial for any app that hopes to be sustainable long-term. PhoneGap Build is a direct competitor to appMobi, and I don't think they can handle the competition. With Adobe's recent purchase of PhoneGap, the direction of the company is clear, and I don't see it failing in this market space. In a year, I want to be able to build my application. With AppMobi, I just don't know if that's the case.

I spent most of my day today moving everything over to PhoneGap Build, and I'm not disappointed. This service is so good that I would pay for it, and coming from me, there's no higher praise.

Get off of appMobi while you still can.

You will save time fighting the asinine build system, and you will have more control over the product into which you're investing your time and money.

Please read: A personal appeal to Wikipedia

Wikipedia: Please, for the love of god, sell out and get ads.

For the past several months, I’ve been completely unable to use Wikipedia. Every time I view in article, I’m slapped in the face with someone’s smug image and a giant banner telling me I’m a terrible person for not donating. Too dramatic? I don’t think so:

Ward Cunningham is a tool.

You claim you’re opposed to advertising, but this is supposedly better?

  • You know what I’m searching for.
  • You know what I’m editing.

You have all the information necessary to target ads to show me something I might be interested in.

I’d much rather see an ad for, say, Mass Effect 3 or Crucible.

Shepard’s mug is a bit prettier, I think:

So please, Wikipedia. Get rid of your pleas for donations and serve me up some ads. Every time I open a link and get a picture of some Wikipedia-beggar, I ragequit. Enough.

Your hard drive is too big

In the past year or two, I’ve come to the conclusion that developers on the whole are the most terrible consumers on the planet.

I’ve had this exchange so many times I want to vomit.

Me: Oh, you’re buying a computer for development? Neat. What are the specs?
You: Well, I really want a devastatingly performant machine that will last a while. I’m thinking…
…Latest Quad-Core i7
Me: Sure.
You: At LEAST 8GB of RAM, and fast RAM too.
Me: Yeah, gotta get that 1333MHz+
You: A huge, high density display. Need those pixels for development.
Me: Yup.
You: One, or two, absurdly expensive graphics cards.
Me: Yeah, games are important.
You: Something like a 1TB hard drive.
Me: …you fucking idiot.

Somehow, developers have built the notion into their heads that spending $2-3k on a Ferrari computer is acceptable, but only so long as they use 10″ tires, or run the car on its hubcaps.

Pop Quiz: If your day to day activity involves writing code in an IDE, keeping browser windows open, running virtual machines, browsing a database, combing hacker news, and playing games, which one of your amazingly expensive hardware components is going to bottleneck performance?

Answer: Your shitty 1TB 7200RPM hard drive.

This has been the bottleneck for at least a year or two now, but still I see developers crippling their machines, refusing to buy a reasonably priced 128GB or 180GB SSD. If you spent over two grand on a computer and you still need to turn off features of your IDE or “manage what’s running”, you’re an idiot. Developer time, as you probably know, is pretty expensive, and every minute of every day lost to waiting and doing nothing is a direct loss of productivity.

But wait, you’re saying. You really need that 1TB. How else will you store every movie, music, document, or file you’ve ever read or looked at since 1999?

My answer? You won’t, and you shouldn’t.

Be honest with yourself. Are you really every going to watch your pirated copy of 2001: A Space Odyssey again? Would it be so hard to obtain if it wasn’t stored in some obscure folder on your hard drive? You’re probably already paying for Netflix, so use it. How often do you really need to lookup your 400GB library of music you started accumulating when you were 7? Probably never. If you get nostalgic, there’s always spotify.

Or you could buy an external drive, and never use that.

So tell Santa you want an SSD. A nice one, and not a shitty off-brand drive either.

Applications start faster
Branching is faster
Installing is faster
Game load times become instantaneous
Intelli-J becomes usable again on large projects
Your computer starts and stops faster
Your builds are faster

…and most importantly….


You get faster.


Invest in yourself, and replace the spinning platter in your would-be-powerful desktop or laptop with an SSD. You won’t regret it.

Time Warner is the worst company in the world

And here’s why.

Tonight, I wanted to work on building rspec/cucumber tests for my mobile application, Wendler 5/3/1.

Instead, I was thwarted from doing anything productive by my run-in with the worst company on the planet.

Let me illustrate:

This is the point where you run at me and shout “But wait, password haystacks! And it doesn’t even look like they’re hashing passwords!

Oh, my good friend, but it’s so much worse. Look at that last rule.

No undesirable words or phrases

Not only are passwords clearly being stored as plain-text, but DBAs or random employees are looking at passwords so frequently that they noticed a trend in profanity and decided to ban it.

What the living fuck.

Well, this isn’t so bad. So my account with my personal and banking information can easily be compromised. No big deal, right? I’ll just go pay my bill online, now.

Wait, what the fuck is this?

The whole reason I’m on this godforsaken website is to avoid paper billing. So, big surprise, I need a paper bill to access my account information. But wait, 4 digits? I wonder if they even…

Nope, they don’t. They don’t throttle requests in any way to verify the customer code. Well, I guess it’s time to break into my own freaking account. Will I need arcane magic exploits, or maybe intimate knowledge of HTTP to get the job done? Nah.

Here’s my selenium HTML script:

hacking
store 0 p
while storedVars['p'] < 10000
open /account/settings/verify/
type name=cpniValue javascript{var s =storedVars['p']+"";while(s.length<4){s="0"+s};s}
store javascript{parseInt(storedVars['p'])+1} p
clickAndWait css=button.ctaButton
endWhile

This took all of 10 minutes to craft (requiring the tricky selenium-flow extension), but I got the job done. Now I just need to wait until I break into my own account; at about 1 attempt per 2 seconds, I’m looking at a maximum wait time of 20,000 seconds, or 333 minutes, or about 5.5 hours. Phone support won’t be available until 8am or 9am, so I’m beating customer support by 5 hours and 45 minutes of hold time.

I wanted to play tonight, but instead I had to work for Time Warner. Why?!

The iPhone, circa 1988

1988 "iPhone" design

In Donald Norman’s The Design of Everyday Things, Norman describes a usability problem that results from adding complexity and functionality to an existing system that he would give to design students. The problem is as follows:

“You have been employed by a manufacturing company to design their new product. The company is considering combining the following into one item:

  • AM-FM radio
  • Cassette player
  • CD player
  • Telephone
  • Telephone answering machine
  • Clock
  • Alarm clock
  • Desk or bed lamp

The company is trying to decide whether to include a small (two-inch screen) TV set and a switched electrical outlet that can turn on a coffee maker or toaster.”

The iPhone can do all these things. Throughout the book, Norman continually goes back to the state of mobile phones in the late 80s as one of the worst examples of design in the marketplace; new phones were being developed before old ones has sold, so manufacturers had no feedback on what to produce. Each phone, then, was radically different in terms of functionality and design, which lead to a number of unusable, bad phones. Oddly enough, that’s exactly what the iPhone has and is doing:

First gen iPhone:

First Gen iPhone

iPhone 4:

iPhone 4

Neat.

Honestly, could Apple have taken the words of Norman more to heart? Each iPhone revision is a direct upgrade to the previous revision, with very little changing radically. With each generation, you know the hardware specs will improve, and there might be some modest improvement in software. In this manner, the design isn’t in constant flux or churn, unlike the current state of Android devices, which seem to epitomize the antithesis of iterative design. Magnets? We got that. QWERTY keyboards? Sure. Rotatey-flippy-hinge-thingy? Why not. HDMI-output on a phone? Damn straight!

That’s not iteration or innovation; that’s throw everything at a wall and see what, if anything, sticks. That’s not to say anything against the software, per say, as the G1 launched with Norman’s other wild-fantasy: software that would auto-sync across any device you were using over the air (“or infrared”), such that you could use a “calendar” or a “planner” instead of a computer. That is, the computer disappears and only the actual activity remains. Even now, the iPhone struggles to support multiple syncing sources, which leads to broken or confusing integration across the desktop and mobile experience. By forcing google integration immediately and absolutely, the hardware can disappear and the applications themselves can emerge.

So yes, Don Norman predicted the iPhone and current wave of mobile devices over 20 years ago, in a way. And I thought Jobs and a team of engineers had a stroke of brilliance; turns out they were just following instructions from a textbook.

Compressing CSS and JavaScript with Gradle – quickly.

Compressing CSS and JS is pretty common-place right? Sure, most web containers and frameworks can do this for you these days, but what about those of us without those tools at our disposabl? We need compression too, and we need builds to do this for us. Since Gradle is groovy and popular, of course you chose it for your build system; Unfortunately, there doesn’t seem to be a pre-configured task anywhere to compress JS or CSS.

Since gradle is just groovy, you can use GPars for parallel collection processing. With the use of GPars, I created compressJs and compressCss. You will need to download the yuicompressor jar from here and place it in the lib/ directory at the root of your project, as the version in maven central doesn’t seem to be runnable as a jar. Then just run gradle compressJs compressCss.

import groovyx.gpars.GParsPool

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'org.codehaus.gpars:gpars:0.12'
    }
}

def compress(String type) {
    def elementsToMinify = []
    fileTree {
        from type
        include "**/*.$type"
    }.visit { element ->
        if (element.file.isFile()) {
            elementsToMinify << element
        }
    }

    GParsPool.withPool(8) {
        elementsToMinify.eachParallel { element ->
            println "Minifying ${element.relativePath}"
            def outputFileLocation = "build/$type/${element.relativePath}"
            new File(outputFileLocation).parentFile.mkdirs()
            ant.java(jar: "lib/yuicompressor-2.4.6.jar", fork: true) {
                arg(value: "$type/${element.relativePath}")
                arg(value: "-o")
                arg(value: outputFileLocation)
            }
        }
    }
}

task compressJs {
    inputs.dir new File('js')
    outputs.dir new File('build/js')

    doLast {
        compress('js')
    }
}

task compressCss {
    inputs.dir new File('css')
    outputs.dir new File('build/css')

    doLast {
       compress('css')
    }
}