← Blog

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:

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:

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.