Getting started with J2ME development
RetroRelay is a J2ME app.
J2ME is a stripped-down version of the Java platform and was the backbone of pre-smartphone mobile devices. It shipped on billions of phones between 2003 and 2013.
There's still interest in the platform, the phones that use it, and in developing for them, so we have written a guide on how to get started, including some of the things we learned during the three months development of the initial release of RetroRelay.
The good news is that J2ME is small, well documented, and completely frozen, which means nothing you learn about it will go out of date. The hard part is assembling the toolchain.
The goal is one loop
Don't try to learn the platform first. Aim for a single loop that works end to end.
install the toolkit → run a bundled sample project in the emulator → write your own app → package it → install it on a real phone
Once that loop closes, everything after it is just writing Java.
Downloads
The files are still published by Oracle today in their archive sections. You need a free oracle.com account to download them.
Download the exact files listed below. Each page lists a lot of near-neighbours, and picking the wrong one can cost you time and effort.
- JDK 6, 32-bit for Windows
From the Java SE 6 archive, under "Java SE Development Kit 6u45" on the Windows x86 row take jdk-6u45-windows-i586.exe, 69.85 MB.
Ignore the Windows x64 row directly beneath it. The toolkit is a 32-bit toolchain and won't drive an x64 JDK.
- Sun Java Wireless Toolkit 2.5.2_01
From the Java ME archive, under "Sun Java Wireless Toolkit 2.5.2_01" take sun_java_wireless_toolkit-2.5.2_01-win.exe, 37.36 MB.
This is the compiler wrapper, emulator, packager and documentation in one install. It's the whole development environment.
Further down that same page is a section for "Sun Java Wireless Toolkit for CLDC 2.5.2 ML". ML is the multi-language build, which bundles the toolkit's interface and documentation in additional languages. Look at the version though. It's 2.5.2, not 2.5.2_01. Oracle describes _01 as purely a security update, closing a hole that let an untrusted application execute arbitrary code, with no feature changes at all. So the ML build is the same toolkit without that fix. Unless you specifically need the translations, match the filename above exactly.
Both pages carry an Oracle warning that these releases are old, unpatched, and not recommended for production. That's fair and worth reading. It's also not quite our situation, since this is a build toolchain for hardware that stopped receiving updates over a decade ago, running on your own machine.
Linux builds of both sit on the same pages if you would rather not use Windows. This guide is written against the Windows toolchain, which is what RetroRelay is built with.
A word about Java ME SDK 3.0
You will also see Java ME SDK 3.0 on the Java ME page, and Oracle's own toolkit page recommends it over the one this guide uses. That recommendation is worth taking seriously rather than talking around. The toolkit was folded into the newer SDK, which adds a proper editor, a project IDE and a much larger set of emulated devices. The toolkit has no editor at all.
RetroRelay still builds with 2.5.2_01, and the reason is the baseline rather than nostalgia. Our build links against the toolkit's CLDC 1.0 and MIDP 2.0 libraries, and that surface is the compatibility guarantee, the thing that stops a 2005 phone meeting a method it has never heard of. The newer SDK's stock devices sit a tier above that, on CLDC 1.1 and MIDP 2.1.
It's worth being clear that this isn't a performance argument. Both tools hand your code to the same javac from the same JDK and then run the same preverifier, so the bytecode a phone executes is identical either way. The newer SDK is a better place to write code, not a better compiler. Pick on reach. If you want the widest possible range of phones, take the toolkit and the older baseline, which is what the rest of this guide describes. If you're aiming at later phones and would rather have an IDE, the newer SDK is a perfectly reasonable place to start.
Where everything goes
Decide this before you install anything. The toolkit writes absolute paths into its own files during setup, so rearranging afterwards is far more annoying than getting it right first time.
Keep both halves of the toolchain under one parent folder:
C:\J2ME\
├─ Java\
│ └─ jdk1.6.0_45\
└─ WTK2.5.2_01\
├─ apps\ your projects, one folder each
├─ bin\ ktoolbar, the emulator, the preverifier
├─ docs\ the full offline API documentation
├─ lib\ the API jars you compile against
└─ wtklib\ device skins and toolkit configuration
One folder holding the JDK and the toolkit means the whole environment copies to another machine, or onto a backup drive, as a single unit. Three rules apply to where you put it, and all three are easy to get wrong.
- Install the JDK first
The WTK installer asks you to point at a JDK, so the JDK has to already exist.
- Not under Program Files
WTK keeps projects inside its own install folder by default and writes settings and emulator state back there too, so Windows permissions block it. The failures don't say "permission denied", they show up as builds that quietly produce nothing.
- No spaces anywhere in the path
WTK's launchers call java.exe by absolute path and don't quote it. A space breaks them, and the error you get points nowhere useful.
On the "decide first" point, every one of the nine .vm launcher files in WTK2.5.2_01\bin has the absolute path to your JDK baked into it at install time. Move the JDK afterwards and nothing runs until you have hand-edited all nine.
Inside a project
Projects live in the toolkit's apps folder, one folder each, and that's the only place ktoolbar looks for them. A project looks like this:
apps\YourProject\
├─ src\ your Java scripts
├─ res\ files bundled into the JAR verbatim
├─ lib\ third-party jars to bundle
├─ bin\ build output: the .jad and .jar
└─ project.properties Project API settings
There will also be classes\, tmpclasses\ and tmplib\. Those are working directories, regenerated on every build. Don't modify them, and don't put anything of your own in them.
res\ deserves a warning. Everything in it is copied into the JAR exactly as it is. It's very easy to leave a source image, a spare icon, or a helper script in there and never notice, and it quietly inflates the JAR. That matters more than it sounds, because some early Nokia phones refuse to install a JAR over 128 KB, so a forgotten file can be the whole difference between your app installing and not.
The emulator's saved data lives in your user profile
Look in %USERPROFILE%\j2mewtk\2.5.2\appdb\, which holds one folder per emulated device. If your app stores anything and you need to examine the files your app created during an emulator run, that's the folder to check.
If you need to clear the emulator's files, WTK has a feature for this at:
File → Utilities → Clean Database
You don't have to keep your projects inside the toolkit
WTK2.5.2_01\wtklib\Windows\ktools.properties contains a kvem.apps.dir setting. Point it at any folder you like and ktoolbar reads projects from there instead of apps\. This is how RetroRelay keeps its source in an ordinary Git repository rather than buried inside a 2008 install directory, and it's one line. Worth doing on day one, before you have anything to move.
There's one caveat with doing this. When you open a project WTK will show an "Importing demo" loading bar. Close it as quickly as possible, as in our experience if left for more than a few seconds it overwrites project settings and may lead to importing files you don't want. It's a bug in this project storage path.
If you don't need a perfectly clean codebase, leave your projects where they are by default inside the WTK files, and WTK will load them without this bug as normal.
Prove the install
Launch ktoolbar.exe from the WTK toolkit's bin folder, open the bundled UIDemo project, then Build and Run.
If the emulator opens and the demo works, your toolchain is correct. If it doesn't, fix that before writing a line of your own code. Debugging your first app and a broken install at the same time is miserable.
The toolkit ships 27 sample projects. They're the best learning material available for this platform, better than anything currently online. UIDemo covers the screen widgets, NetworkDemo covers HTTP.
Creating your own app
Create a new project and give it a name and a class name. A settings dialog opens straight after. On its API Selection tab, set Target Platform to Custom first, then choose CLDC 1.0 and MIDP 2.0. The order matters, because the preset targets lock the configuration dropdown, and the most common preset pins you to CLDC 1.1.
CLDC 1.0 and MIDP 2.0 is the widest baseline the platform has, and it's what RetroRelay targets so the app runs on as many phones as possible.
The toolkit creates the project folders for you. One thing to know up front is that ktoolbar has no editor. You write your Java in whatever editor you like, drop it in the project's src folder, and the toolkit just compiles it.
An app is called a MIDlet. It's a class the phone starts, pauses, and destroys, and Display.setCurrent is how you put a screen on it. That's genuinely most of the model:
import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.*;
public class HelloMidlet extends MIDlet implements CommandListener {
private Form form;
private Command exitCommand;
public HelloMidlet() {
form = new Form("Demo");
form.append(new StringItem(null, "Hello from J2ME."));
exitCommand = new Command("Exit", Command.EXIT, 1);
form.addCommand(exitCommand);
form.setCommandListener(this);
}
protected void startApp() {
Display.getDisplay(this).setCurrent(form);
}
protected void pauseApp() { }
protected void destroyApp(boolean unconditional) { }
public void commandAction(Command c, Displayable d) {
if (c == exitCommand) {
notifyDestroyed();
}
}
}
Build compiles and preverifies. Run launches the emulator. The device dropdown in the toolbar switches which phone it pretends to be.
Packaging your app
Before you create a new package, it's always good to clean the project first.
Project → Clean
Then build the project to construct your app fresh.
Project → Build
Finally, you can create the actual package you will distribute to your phone.
Project → Package → Create Package
That produces two files in the project's bin folder.
- .jad
A small text file containing information about your app, like the name, icon, size, version, etc.
- .jar
Your actual app, packaged and compressed into a file.
Those two files are what a real phone installs. In our experience, a lot of phones can install the .jar by itself. This is helpful for when you're testing your app and want to get it onto a phone quickly during iteration.
If you ever distribute your app or install a final version, make sure to put both files onto a phone and use the .jad to install the app, as this gives the phone more information about your app, makes it more likely to trust it, and lets users check for updates if you give the .jad a URL parameter and host your app's files somewhere on the internet.
Getting it onto a real phone
This is where a working app meets a decade-old installer, and it's the least forgiving part of the whole loop. Almost every failure here is silent. The phone declines, offers no reason, and leaves you guessing which of the half-dozen possible causes it was.
Serve it over HTTP, not over a cable
Put both files on a small web server on your machine and open the URL in the phone's browser. This is the one route that works on essentially every phone, and on some it's the only route at all. One of our test phones has no USB, no Bluetooth and no infrared, so over the air is the only way anything reaches it.
The server has to send the right content types. The .jad needs text/vnd.sun.j2me.app-descriptor and the .jar needs application/java-archive.
This is worth spelling out because the obvious shortcut doesn't work. python -m http.server sends both files as application/octet-stream, and some BlackBerry firmware simply refuses a .jad served that way. Writing a tiny dedicated server that sets those two types is a ten-minute job and it removes an entire category of mystery.
Which file you open depends on the phone. Nokia's Series 40 installer wants the .jad, which it reads before fetching the archive itself. BlackBerry is happier if you visit the .jar directly, falling back to the .jad if that's refused.
The .jad points at the archive. The MIDlet-Jar-URL line inside the .jad tells the phone where to go for the .jar. If you serve the pair from a different address than the one written in there, the phone looks in the wrong place. The nastier version of this is when something valid but stale is still sitting at the old address, and you install an old build without realising.
Watch the browser cache. Rebuild, re-serve at the same URL, and the phone may cheerfully reinstall the copy it already had. You can lose an hour debugging a fix that was correct the first time. Send Cache-Control: no-store and the problem disappears.
Cables are less reliable than they sound
USB or Bluetooth transfer followed by opening the file on the phone does work on some phones, but it's not the route to reach for first. On the Series 40 phones we tested, sending the files over Bluetooth produced "unknown file type" and went nowhere. BlackBerry over USB exposes only the memory card slot, and there's no dependable install-from-file-manager path on it either.
Three things in the .jad that fail silently
- MIDlet-Jar-Size must match the .jar size exactly
It's a byte count, and the phone's installer compares it. WTK writes it for you, so a straight Create Package is always consistent. It bites the moment the two drift apart from editing the .jad by hand, or putting the JAR through anything after packaging. Our own deploy shrinks the JAR after WTK has written the .jad, so it has to rewrite the size afterwards or nothing installs.
- Every section of MIDlet-Version must be between 0 and 99
The format is Major.Minor.Micro and the specification caps each component at 99. Nokia's Series 40 installer enforces that strictly, so the .jad fails to parse, you get "Invalid application", and the archive is never fetched. Motorola and BlackBerry accept out-of-range values without complaint, which is the dangerous part.
We originally versioned RetroRelay as 2026.1.0, and it shipped happily to every phone except the Nokias.
The fix was 26.1.0. Still readable as a year, and works until the year 2100.
- Keep the line endings in the .jad as LF
This is the opposite of what the specification asks for, and we found it the hard way. Converting the file to Windows-style CRLF breaks installation on BlackBerry with error 904, reported as a size mismatch, because the parser appears to include the trailing carriage return in the size value and then fails its own comparison. If your editor or version control is "helpfully" normalising line endings, that's worth checking before you blame anything else.
What the emulator won't tell you
The emulator is the right tool for checking your toolchain works and for iterating quickly. It's a poor guide to whether your app is any good on an actual phone. Everything below only shows up once you're on real hardware.
There's no console, and no debugger
This one catches everybody. System.out.println works fine in the emulator and goes absolutely nowhere on a phone. There's no console to read, no stack trace when something throws, and nothing to attach a debugger to. Your app either works or it silently doesn't.
The answer is to build your diagnostics into the app itself. A hidden screen that reports internal state, a self-test you can trigger on the device, timings drawn to the display instead of written to a stream. RetroRelay has all of that behind a flag that compiles out of release builds, so it costs nothing to ship.
Plan for this early. Adding it once you already have a bug you can't see is considerably less fun.
The emulator is far more forgiving than any phone
It will happily render layouts that a real phone mangles, and tolerate timing that a real phone won't. Different phones and their varying platforms have their own rendering quirks that only testing on real hardware will reveal.
Every quirk we know about came from hardware, and none of them reproduced in the emulator first. If you're going past a hello-world, get hold of one cheap phone early rather than later.
Soft keys are mirrored between manufacturers
Nokia puts back and dismiss on the right soft key. Motorola puts it on the left. Both are correct on their own hardware, and a user of either will find the other arrangement wrong.
MIDP handles this for you as long as you describe intent rather than position. Say "this is the back action" and let the phone decide where it belongs. Hardcode a side and your app is backwards on half the phones you support.
Screens are small, and they vary
The smallest in our test set is 128×128. Design against the smallest screen you intend to support, because a layout that survives that will be fine everywhere.
The emulator's device dropdown gives you a rough check across a few sizes, which is better than nothing but no substitute for the real thing.
Use the built-in widgets, not a custom Canvas
MIDP lets you take a Canvas and draw every pixel yourself. If you've come from game development that will feel like the obvious move, and for a game it often is. For anything form-shaped it's a trap.
Drawing UI yourself means solving text layout, line wrapping, scrolling, focus, input and soft keys again, for every screen size and every phone you have never tested on. The built-in Form, List and Item widgets hand all of that to the phone's own renderer, so your app looks native on each device and adapts to screens you have never seen.
RetroRelay is built entirely on the standard widgets, and that decision is most of why it runs across a decade of hardware with minimal per-platform special cases.
What Java you can actually write
Two separate limits apply here, and it helps to keep them apart. One is the language your compiler is allowed to emit. The other is the library that exists on the phone. Neither is a matter of style or preference. Both are hard walls.
The language: you're writing Java 1.3
The toolkit compiles to class file version 47, which is Java 1.3. Every .class file records this in its header as major_version, the field straight after the magic number and the minor version. Ours reads 47.
That single number decides a lot. Every one of the following needs class file version 49, which is Java 5, so the compiler physically can't produce them for this target:
- Generics
- Enums
- Annotations
- The enhanced for loop
- Autoboxing
- Varargs
These aren't discouraged, they're unreachable. If you find yourself wondering whether some modern convenience is allowed, the answer is almost always no, and the reason is always this.
The library: 104 classes
That's the entire CLDC 1.0 standard library, internals included. For comparison, desktop Java of the same era shipped thousands. What you get is:
- java.lang
Object, String, StringBuffer, Math, System, Thread, Runnable, the integer wrapper types, and the exceptions.
- java.io
Streams, readers and writers. No files.
- java.util
Vector, Hashtable, Stack, Enumeration, Calendar, Date, Random, TimeZone. MIDP adds Timer and TimerTask.
- The javax.microedition packages
Everything phone-shaped, with lcdui for the screen, rms for storage, io for networking, and media for sound.
If one class captures the whole platform, it's java.lang.Math. Here it is, complete:
public final class java.lang.Math {
public static int abs(int);
public static long abs(long);
public static int max(int, int);
public static long max(long, long);
public static int min(int, int);
public static long min(long, long);
}
Six methods. No sqrt, no sin, no PI, because CLDC 1.0 has no floating point at all. There's no float and no double, so there's nothing for those methods to return. If you need fractions, you do fixed-point arithmetic with integers.
What is missing, and what to reach for instead
- float and double
Integer or fixed-point maths. This one shapes your whole design.
- StringBuilder
StringBuffer. Same idea, and it's synchronised, which costs a little but rarely matters.
- The entire collections framework
No ArrayList, HashMap, List, Map, Set, Iterator, Collections or Arrays. You get Vector, Hashtable and Enumeration, and you write your own sorting and searching.
- Comparable
It doesn't exist. Write your own comparison method and call it directly.
- java.io.File
There's no filesystem access. Persistence is javax.microedition.rms, a small record store keyed by integer, which is a genuinely different way to think about saving things.
- Reflection
Class.forName exists and little else. Worth avoiding anyway. Tools that strip unused code out of your JAR can't tell whether a class looked up by name is still needed, so reflection quietly costs you size on a platform that has very little to spare.
There's one escape hatch worth knowing about. CLDC 1.1 adds floating point, along with the Math methods that need it. Most phones from around 2006 onwards support it, and the toolkit will happily target it. RetroRelay stays on 1.0 because the older phones matter to us, but that's a decision you get to make rather than inherit. Everything else above is true either way.
Where are the docs?
The WTK toolkit ships the complete API documentation, offline, on your own disk. Look in the docs/api folder. That's the full reference for both layers of the platform, and it's the primary source. There's nothing better online. The docs folder also holds the user guide for the toolkit itself.
For anything the documentation doesn't answer, read the bundled sample projects. For questions about specific phones, the community wikis are what survived, the vendor developer portals of the era are all long dead.
The documentation is frozen and will never change. MIDP 2.0, the version worth building against, was finalised in 2002 and never changed after, so the references are complete and stay accurate. The scarce thing isn't really documentation, it's people who have done this recently.
Give it a go!
Get the WTK toolkit and the JDK installed, open one of the bundled sample projects, build it, run the emulator, then put it on a real phone. That whole loop working end to end is the main hurdle. Everything past that is writing Java with a 2003 rulebook, against a platform that will never move underneath you.
It can be frustrating working with older software and technology, but when you figure things out and get it working it feels great to have overcome the hurdles.
When working with retro phones you get to experience a massive piece of history. They once connected everyone, and billions of them were produced and sold across the world. We should never forget the technology that paved the way for the world we live in today.
Get in touch
If you get stuck on any of the above, don't hesitate to get in touch. If you find anything that's incorrect or needs changing, please let us know and we can keep the guide up to date for future readers.