Android-Apps in Ruby schreiben, 2026: JRuby 10 und Ruboto
Mit Ruboto schreibt man Android-Apps in Ruby. Auf dem Gerät läuft JRuby, und Ihre Activities sind Ruby-Klassen, die die Android-API direkt aufrufen. Das Projekt ist eingeschlafen: Der letzte Commit im Original ist vom Mai 2023 und zielt auf JRuby 9.4.
Ich wollte es zurück und habe es geforkt: github.com/Largo/ruboto. Der Fork läuft mit JRuby 10.1 (kompatibel mit Ruby 3.4) auf Android 6.0 bis Android 16, auf Handys, Tablets und Wear-OS-Uhren. Android Studio brauchen Sie nicht, nur ein JDK, das Android SDK und Ruby.
Die erste richtige App damit läuft auf einem digitalen Bilderrahmen von Frameo. Dazu weiter unten mehr.
Schnellstart
Sie brauchen JDK 17 oder neuer, das Android SDK (ANDROID_HOME setzen), Gradle und Ruby.
git clone https://github.com/Largo/ruboto.git
cd ruboto && bundle install && cd ..
# App erzeugen. Lädt auch die neusten JRuby-Jars nach libs/.
ruby -Iruboto/lib ruboto/bin/ruboto gen app --package org.example.todo --name Todo --path todo
# Die Beispiel-Todo-Liste als App verwenden
cp ruboto/assets/samples/todo_activity.rb todo/app/src/main/resources/todo_activity.rb
cd todo && gradle assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
Starten Sie ruboto gen app mit einfachem ruby, nicht mit bundle exec. Bundler versteckt das Gem jruby-jars, und dann wird kein JRuby in die App kopiert.
Die ganze App ist eine Ruby-Datei in app/src/main/resources. Bearbeiten, neu bauen, neu installieren. Das ist der ganze Entwicklungszyklus.
So sieht der Code aus
Eine Activity ist eine Ruby-Klasse. ruboto/widget bringt eine kleine DSL für Layouts mit:
require 'ruboto/widget'
require 'ruboto/toast'
ruboto_import_widgets :Button, :EditText, :LinearLayout, :ListView
class TodoActivity
def onCreate(bundle)
super
set_title 'Ruboto Todo'
@todos = []
self.content_view =
linear_layout orientation: :vertical, fits_system_windows: true do
@new_todo = edit_text hint: 'What needs doing?'
button text: 'Add', on_click_listener: proc { add_todo }
@list = list_view list: @todos.dup
end
end
def add_todo
@todos << @new_todo.text.to_s
@list.reload_list(@todos.dup)
toast 'Added'
end
end
Sie können die DSL auch weglassen und die Android-Klassen so aufrufen wie in Java: java_import 'android.widget.TextView', dann TextView.new(self). Die ganze Android-API ist da, auf eine Wrapper-Schicht müssen Sie nicht warten.
Was ich reparieren musste
JRuby 10 ist für eine moderne JVM geschrieben. Androids Laufzeit ART ist keine. Ruboto schreibt das JRuby-Jar um, während es in die App installiert wird, mit der ASM-Bibliothek, die JRuby schon mitbringt. Die wichtigsten Punkte:
- Keine Klassen zur Laufzeit. ART kann zur Laufzeit erzeugten Bytecode nicht laden. JRuby macht das normalerweise für Methodenaufrufer, dynamische Scopes und Implementierungen von Java-Interfaces. Der Fork erzeugt die Aufrufer schon beim Bauen, nutzt eine feste Scope-Klasse und implementiert Java-Interfaces mit
java.lang.reflect.Proxy. Blöcke, Procs,Runnable.impl { }undinclude SomeJavaInterfacefunktionieren alle als Callbacks. - Fehlende JDK-Klassen.
java.lang.ModuleundSwitchPointgibt es auf Android nicht.ClassValuegibt es erst ab Android 14, und JRuby 10.1 braucht es beim Start. Aufrufe von JDK-Methoden, die neuer sind als das Gerät, etwaInputStream.readAllBytesoderBigInteger.TWO, gehen an Backports. - Android 6 bis 12. JRuby 10 nutzt überall
java.lang.invoke, undVarHandleist erst ab Android 13 öffentliche API. Bei einem tieferen--min-sdkleitet Ruboto diese Aufrufe an einen kleinen Ersatz um, der auf Reflection undsun.misc.Unsafeaufbaut. Core Library Desugaring ergänztjava.nio.fileundjava.timeunter Android 8. - Fibers. JRuby führt Fibers und
Enumerator#nextauf virtuellen Threads aus. Android hat keine, also laufen sie auf normalen Threads. - Die Standardbibliothek. Die Java-Teile von
stringio,strscan,jsonundpsychliegen als Jars in JRuby, und Android kann zur Laufzeit keine Jars laden. Ihre Klassen werden in die App gemischt. Seit Ruby 3.4 sindbigdecimal,csv,logger,base64,rexmlund andere Bundled Gems, die normalerweise über RubyGems gefunden werden. Ruboto kopiert sie in den Ladepfad. - RubyGems ist aus. Es kann Gem-Spezifikationen in einem APK nicht lesen. Gems in reinem Ruby gehen trotzdem: Gem entpacken und den
lib-Ordner nachapp/src/main/resourceskopieren. Das Todo-Beispiel macht das mit dem Gem humanize.
Gems mit C-Erweiterung funktionieren nicht, openssl auch nicht. Eine Java-Klasse in Ruby ableiten (class MyList < java.util.ArrayList) braucht weiterhin Klassen zur Laufzeit und scheitert. Für HTTPS funktioniert java.net.HttpURLConnection aus Ruby problemlos.
Jede Änderung ist getestet: mit JVM-Tests, die kein Android SDK brauchen, und mit einem Smoke-Test auf dem Gerät. Er prüft in 65 Punkten Ruby-Features, die Standardbibliothek, die Bundled Gems, Sockets, net/http und die Java-Anbindung, und besteht auf Emulatoren mit Android 13 und Android 6.
Der Frameo-Bilderrahmen
Frameo-Rahmen sind günstige WLAN-Bilderrahmen. Meiner hat einen Bildschirm mit 1920×1200 Pixeln, einen 32-Bit-Rockchip RK312x mit vier Cortex-A7-Kernen, 1 GB RAM und Android 6.0.1. Er führt die Frameo-App aus und sonst nichts. Aber es gibt einen versteckten Schalter: Einstellungen › Über › Beta-Programm, dann ADB-Zugriff. Danach funktioniert adb install.
Ich wollte einen Rahmen, der die Frameo-Cloud nicht braucht. Also habe ich einen geschrieben, in Ruby, rund 1300 Zeilen:
- Fotos erscheinen im Vollbild und blenden alle 15 Sekunden bis 30 Minuten über, gemischt oder der Reihe nach.
- Ein kleiner Webserver auf Port 8080 lässt Handys und Computer im selben WLAN Fotos hochladen, anzeigen und löschen. Er ist mit einem Passwort geschützt. Der Browser verkleinert die Fotos vor dem Hochladen auf die Bildschirmgrösse, und der Rahmen prüft sie noch einmal.
- Fotos belegen höchstens einen Viertel des Speichers und lassen immer 1 GB frei.
- Eine kleine Einblendung zeigt die Uhrzeit in zwei Städten und das heutige Wetter von MeteoSchweiz.
- Ein Tipp auf den Bildschirm zeigt die Webadresse, das Passwort und Knöpfe für das nächste Foto und zum Schliessen der App.
Der Webserver ist rund 160 Zeilen Ruby auf java.net.ServerSocket. Er bedient eine Verbindung nach der anderen und schreibt Uploads direkt auf den Speicher, denn das 1 GB RAM teilt er mit der Frameo-App. Fotos dekodiert Androids BitmapFactory auf einem Hintergrund-Executor, dann gehen sie mit einer Handler-Nachricht an den UI-Thread:
@handler = Handler.new(Looper.getMainLooper, proc { |message| handle_message(message) })
@decoder = java.util.concurrent.Executors.newSingleThreadExecutor
def display(name)
path = PhotoFrame.store.path(name)
@decoder.execute(proc do
bitmap = decode(path)
post_ui(:fade_to, [name, bitmap]) if bitmap
end)
end
Die Frameo-Firmware lässt Apps beim Booten nicht selbst starten. Der Ausweg: Die Activity meldet sich mit einem Intent-Filter android.intent.category.HOME als Startbildschirm-App an. Android fragt dann, welche Startbildschirm-App es nehmen soll, und der Rahmen startet nach einem Neustart meine App. «Close photo frame» schickt einen Home-Intent an die Frameo-App, die ihre eigene Diashow fortsetzt.
Schnell ist es nicht. JRuby braucht auf dieser Hardware rund 20 Sekunden zum Starten, hinter einem Splash-Screen. Unter Android 13 arbeitet der Ersatz für java.lang.invoke mit Reflection, Instanzvariablen und Java-Aufrufe sind deshalb langsamer. Für einen Bilderrahmen spielt das keine Rolle: Er startet einmal und läuft dann wochenlang. Sobald JRuby läuft, sind das Überblenden von Fotos in 1920×1200 und die Uploads flüssig.
Wegen dieser App unterstützt der Fork Android 6. Vorher war das Minimum Android 13.
Wear OS und IRB
Dieselben Apps laufen auf Wear OS. Ich habe damit einen EPUB-Reader für meine Uhr gebaut, und der Fork hat zwei IRB-Beispiele: eines für Handys, das mit gem install direkt im Prompt Gems in reinem Ruby installieren kann, und eines in Uhrengrösse, mit Diktierknopf, weil Ruby auf einer Uhr zu tippen eine Qual ist.
Zwei Dinge sollten Sie für Wear OS wissen. Zurückwischen ruft nie das alte onBackPressed auf. Setzen Sie android:enableOnBackInvokedCallback="true" und registrieren Sie eine Proc bei on_back_invoked_dispatcher. Und Java-Felder behalten ihre genauen Namen: display_metrics.widthPixels funktioniert, width_pixels nicht. JRuby legt snake_case-Aliase nur für Methoden an.
Lohnt es sich?
Für ein persönliches Werkzeug, einen Kiosk, ein eigenes Gerät oder eine App, die Sie an einem Nachmittag schreiben wollen: ja. Sie schreiben Ruby, bekommen die ganze Android-API und sparen sich Kotlin, Compose und Android Studio. JRuby wird auf Android interpretiert, der Start dauert (rund 20 Sekunden auf einer Uhr oder einem alten Bilderrahmen), und ein Debug-APK ist etwa 20 MB gross. Für eine App mit einer Million Nutzern würde ich weiterhin Kotlin nehmen.
Der Fork liegt auf github.com/Largo/ruboto. Das README enthält den ganzen Schnellstart, Hinweise zu älteren Android-Versionen, Gems und Java-Callbacks sowie die IRB-Beispiele. Issues und Pull Requests sind willkommen.