Skip to content

Writing a Java App

Quickest path — scaffold with Gradle

Terminal window
./gradlew newApp -Pname=myapp

This creates examples/myapp/ with a starter MyApp.java, PicodroidManifest.xml, and build.gradle.kts. Then build and flash:

Terminal window
./scripts/build.sh --app myapp
./scripts/flash.sh --app myapp

Manual layout

  1. Create a Java source under examples/myapp/:
examples/myapp/java/myapp/MyApp.java
package myapp;
import picodroid.app.Application;
import picodroid.util.Log;
public class MyApp extends Application {
public void onCreate() {
Log.i("MyApp", "Hello from MyApp!");
}
}
  1. Create a PicodroidManifest.xml in the app directory:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="myapp" version="1.0">
<application application="myapp/MyApp" />
</manifest>
  1. Create a one-line build.gradle.kts in the app directory:
plugins {
id("picodroid-papk")
}
  1. Build and flash — no changes to settings.gradle.kts, Cargo.toml, or platforms/rp/src/app.rs needed:
Terminal window
./scripts/build.sh --app myapp
./scripts/flash.sh --app myapp
# Or for Pico (RP2040)
./scripts/build.sh --app myapp --board testbench_rp2040
./scripts/flash.sh --app myapp --board testbench_rp2040

The build pipeline is a Gradle multi-project with a custom picodroid-papk plugin (see buildSrc/). settings.gradle.kts auto-discovers every examples/<name>/ that has a PicodroidManifest.xml. The plugin compiles Java against the framework in :sdk, optionally rewrites framework references via the active shrink map, then packages the result into examples/myapp/build/papk/myapp.papk. scripts/build-apk.sh is a thin wrapper that invokes Gradle and copies the artifact to build/apks/myapp.papk where the firmware build embeds it.

IDE support

The build files in settings.gradle.kts and examples/*/build.gradle.kts are standard Gradle projects. Opening the repo in IntelliJ IDEA (“Open as Gradle project”) or VS Code (with the Red Hat Java + Gradle extensions) gives autocomplete, jump-to-def, and inline error reporting for all framework and app sources.

Pass --shrink (off by default) to apply the active class-name shrink map — build-apk.sh will rewrite framework class references inside your .class files (e.g. Lpicodroid/app/Application;La/B;). Your own class names stay unchanged, so the application= value in the manifest remains valid. See Class-name shrinker for details.

Application Lifecycle

All apps extend picodroid.app.Application and override onCreate(). The runtime instantiates your Application class and calls onCreate() as the entry point.

Console app

For apps that only use logging and peripherals:

package myapp;
import picodroid.app.Application;
import picodroid.util.Log;
public class MyApp extends Application {
public void onCreate() {
Log.i("MyApp", "Hello from MyApp!");
}
}

Display app

For graphical apps, create an Activity subclass and launch it with startActivity():

// MyApp.java — Application entry point
package myapp;
import picodroid.app.Application;
import picodroid.content.Intent;
public class MyApp extends Application {
public void onCreate() {
startActivity(new Intent(MyActivity.class));
}
}
// MyActivity.java — builds the UI
package myapp;
import picodroid.app.Activity;
import picodroid.debug.DisplayDebug;
import picodroid.graphics.Color;
import picodroid.widget.LinearLayout;
import picodroid.widget.TextView;
public class MyActivity extends Activity {
public void onCreate() {
DisplayDebug.calibrate();
LinearLayout root = new LinearLayout();
root.setOrientation(LinearLayout.VERTICAL);
root.setSize(320, 240);
TextView label = new TextView();
label.setText("Hello, Display!");
label.setTextColor(Color.WHITE);
root.addView(label);
setContentView(root);
}
}

The Activity’s onCreate() is called after the display is initialized. Build a widget tree, then call setContentView() to render it. See Graphics & UI for the full graphics and widget API.

Activity lifecycle and back stack

Beyond onCreate(), Activity exposes the full Android lifecycle: onStart / onResume / onPause / onStop / onDestroy / onBackPressed. The runtime also maintains a back stack — push a new screen with startActivity(new Intent(DetailActivity.class)) and pop it with finish():

import picodroid.content.Intent;
import picodroid.view.View;
import picodroid.widget.Button;
public class HomeActivity extends Activity {
public void onCreate() {
Button btn = new Button("Open detail");
btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { startActivity(new Intent(DetailActivity.class)); }
});
setContentView(btn);
}
}

The widget tree set via setContentView() is preserved across pause — when control returns from a popped Activity, the saved tree is restored automatically. See api/ui.md → Lifecycle, api/ui.md → Back stack, and examples/navdemo/.

Toasts and dialogs

import picodroid.content.DialogInterface;
import picodroid.app.AlertDialog;
import picodroid.widget.Toast;
Toast.makeText(this, "Saved.", Toast.LENGTH_SHORT).show(); // first arg is a Context
new AlertDialog.Builder()
.setTitle("Erase data?")
.setMessage("This cannot be undone.")
.setPositiveButton("Erase", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { eraseAll(); }
})
.setNegativeButton("Cancel", null)
.show();

See api/ui.md → Toast, → AlertDialog, and examples/dialogdemo/.

Theme and drawables

Override the global palette before any UI is built (typically in Application.onCreate). Apply GradientDrawable for rounded fills, gradients, and strokes:

import picodroid.graphics.Color;
import picodroid.graphics.Theme;
import picodroid.graphics.drawable.GradientDrawable;
Theme.colorPrimary = Color.argb(255, 80, 180, 120);
Theme.colorBackground = Color.argb(255, 24, 24, 28);
view.setBackground(new GradientDrawable()
.setColor(Theme.colorSurface)
.setCornerRadius(16)
.setStroke(1, Theme.colorOutline));

See api/ui.md → Theme, → GradientDrawable, and the themed-widgets section of examples/displaydemo/.

Gestures and animations

Wire a GestureDetector to recognize taps, long presses, and flings, and use view.animate() for short property animations:

import picodroid.view.GestureDetector;
import picodroid.view.MotionEvent;
view.setOnTouchListener(new GestureDetector(new GestureDetector.OnGestureListener() {
public void onSingleTap(MotionEvent e) { view.animate().alpha(0.3f).setDuration(120).start(); }
public void onLongPress(MotionEvent e) { showContextMenu(); }
public void onFling(MotionEvent down, MotionEvent up, float vx, float vy) { /* ... */ }
}));

See api/ui.md → GestureDetector, → ViewPropertyAnimator, and examples/gesturedemo/ / examples/animdemo/.

Soft keyboard

Tapping any EditText pops up a system soft keyboard at the screen bottom by default — no setup needed. For custom placement or styling, instantiate Keyboard explicitly and pair with EditText.setShowKeyboardOnTouch(false). See api/ui.md → Keyboard and examples/keyboarddemo/.

Input and idle power

On boards with hardware buttons, install OnKeyListeners to receive KeyEvents — see api/ui.md → Key events. After 60 seconds with no button or touch input, the runtime puts the display panel to sleep (backlight off, DISPOFF, SLPIN) and blocks on a GPIO semaphore. The next button edge wakes the panel and is swallowed by the framework — it is not delivered to your listener.

Posting work between threads

To run short-lived async work without spawning a dedicated Thread, use the executors in api/system.md → Executors:

import picodroid.concurrent.Executors;
Executors.backgroundExecutor().execute(() -> {
int value = readSensorBlocking();
Executors.mainExecutor().execute(() -> label.setText("value=" + value));
});

backgroundExecutor() runs on a shared worker pool (configurable in board.toml); mainExecutor() hops back to the UI thread. Both are non-blocking and drop on queue saturation.

Porting to a New Platform

The pico-jvm crate is hardware-independent (no_std + alloc only). To use it on a different platform, implement the HAL modules and NativeMethodHandler trait. See Porting guide for the full guide, including HAL function signatures, FreeRTOS configuration, and build system setup.

Supported Language Features

The table below is written for Java. Kotlin apps are supported too — same JVM, same features where the languages overlap; see the Kotlin guide for the Kotlin-specific subset, divergences, and idioms.

FeatureExample
Arraysbyte[] buf = new byte[16]; — allocation, .length, index read/write
Inheritanceclass Dog extends Animal — field inheritance, @Override, super() constructor chaining, virtual dispatch
Interfacesinterface Speakable / implementsinvokeinterface polymorphic dispatch
Floating-pointfloat/double arithmetic, f2i/f2d casts
Long integerslong arithmetic, i2l/l2i type conversions
Double precisiondouble arithmetic, i2d/d2i type conversions
Exceptionsthrow new AppException(), try/catch, custom exception classes
Try-with-resourcestry (Gpio gpio = pm.openGpio("GP25")) { ... }AutoCloseable peripherals; close() called on normal exit and on exception
Switch statementsswitch/case on integer and other supported types
Static fieldsstatic field declarations and access via getstatic/putstatic
Null checksNull reference detection (ifnull/ifnonnull)
Threadingnew Thread(runnable).start() — spawns a FreeRTOS task per thread, pinned to core 0; stack reclaimed when run() returns; priority set via setPriority(1–10) before start()
Lambdas() -> expr, (x) -> expr — non-capturing and capturing lambdas, method references (Class::method), callbacks; compiled via invokedynamic
Anonymous classesnew Interface() { ... } — anonymous inner classes implementing interfaces, with local variable capture
Static initializersstatic { ... } blocks, static field initializers, cross-class <clinit> chaining; each class initializer runs exactly once on first use
Synchronized blockssynchronized (lock) { ... }monitorenter/monitorexit bytecodes, reentrant locking on the same object
Arithmetic opssubtraction, division, remainder, negation for int, long, double
Bitwise / shifts<<, >>, >>>, |, ^ for int and long
Cross-type castsi2f, i2c, i2s, l2f, l2d, f2l, f2d, d2l, d2f
Dense switchconsecutive-case switch compiled to tableswitch
Type checkinginstanceof, checkcast
Reference arraysnew SomeClass[n], element store and load (aastore, aaload)
String predicatesequals, equalsIgnoreCase, startsWith, endsWith, contains, isEmpty, compareTo
String searchindexOf(char/String), lastIndexOf(char/String)
String transformssubstring(int), substring(int,int), trim(), toUpperCase(), toLowerCase()
String factoryString.valueOf(int/long/boolean/char/float/double)
StringBuildernew StringBuilder("seed"), append(String/int/long/float/boolean/char), length(), charAt(int), toString()
ArrayListnew ArrayList(), add, get, size, isEmpty, set, remove(int), clear, contains — dynamic list backed by heap
HashMap / HashSetjava.util.HashMap and HashSetput, get, containsKey, remove, size, key iteration; works with autoboxed keys
Iterator / for-eachIterable / Iterator and the enhanced for (T x : collection) loop — backed by ArrayList, HashMap, HashSet
EnumsJava enum declarations — values(), name(), ordinal(), and switch over enum constants
AutoboxingInteger, Boolean, Long, Float, DoublevalueOf / intValue etc.; enables storing primitives in ArrayList<Integer> etc.
String (extended)split, replace, concat, toCharArray, hashCode in addition to the predicates / search / transform methods listed above
Sorting / list utilitiesArrays.sort / Arrays.toString (stable mergesort over Comparable[]), Collections.sort / Collections.reverse over java.util.List (ArrayList implements it), java.lang.Comparable<T>
System.currentTimeMillis()Boot-elapsed milliseconds — convenience for the common long now = System.currentTimeMillis() Android idiom

Garbage Collection

The JVM runs a stop-the-world mark-sweep collector automatically. After every 256 heap allocations it scans all roots (frame locals, operand stacks, static fields), marks every reachable object, array, and string, then frees everything unreachable. There is no action needed from app code — GC fires transparently between bytecode instructions.

To introspect GC behavior from Java code, use picodroid.os.Runtime:

import picodroid.os.Runtime;
int count = Runtime.gcCount(); // cycles run since boot (or last reset)
int freed = Runtime.gcFreed(); // entries freed across all cycles
long ns = Runtime.gcTimeNanos(); // cumulative time spent in GC
Runtime.resetGcStats(); // zero all counters

Where next

Once your first app runs, work through these in order:

  1. Tutorial: a multi-screen app with a back stack — Activities, navigation, and lifecycle in a real app.
  2. Tutorial: a background service bound from an Activity — long-lived work that survives screen changes.
  3. Embedded gotchas — the Android idioms that behave differently here. Read this before you write much UI.
  4. System limits & memory budgets — how much your app can do before it runs out of room.
  5. Debugging — symptom-driven playbooks for when something goes wrong.

For touchless hardware-button boards, also read Button-only navigation. The full manifest schema lives in the Manifest reference.