Scrivere app Android in Ruby nel 2026: JRuby 10 e Ruboto
Ruboto permette di scrivere app Android in Ruby. Esegue JRuby sul telefono, e le vostre activity sono classi Ruby che chiamano direttamente le API Android. Il progetto si era fermato: l’ultimo commit upstream risale a maggio 2023 e punta a JRuby 9.4.
Volevo riaverlo, quindi ne ho fatto un fork: github.com/Largo/ruboto. Il fork esegue JRuby 10.1 (compatibile con Ruby 3.4) da Android 6.0 fino ad Android 16, su telefoni, tablet e smartwatch Wear OS. Non serve Android Studio. Servono un JDK, l’Android SDK e Ruby.
La prima vera app che ci ho costruito gira su una cornice digitale Frameo. Ne parlo più avanti.
Avvio rapido
Servono JDK 17 o successivo, l’Android SDK (impostate ANDROID_HOME), Gradle e Ruby.
git clone https://github.com/Largo/ruboto.git
cd ruboto && bundle install && cd ..
# Generare un'app. Scarica anche i jar JRuby più recenti in libs/.
ruby -Iruboto/lib ruboto/bin/ruboto gen app --package org.example.todo --name Todo --path todo
# Usare la lista todo di esempio come app
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
Eseguite ruboto gen app con ruby semplice, non con bundle exec. Bundler nasconde la gem jruby-jars, e allora nessun JRuby viene copiato nell’app.
L’intera app è un unico file Ruby in app/src/main/resources. Modificatelo, ricompilate, reinstallate. Questo è il ciclo di sviluppo.
Com’è il codice
Un’activity è una classe Ruby. ruboto/widget offre un piccolo DSL per il layout:
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
Potete anche fare a meno del DSL e chiamare le classi Android come farebbe Java: java_import 'android.widget.TextView', poi TextView.new(self). Ogni API Android è disponibile, non c’è nessuno strato di wrapper da aspettare.
Cosa ho dovuto sistemare
JRuby 10 è scritto per una JVM moderna. Il runtime di Android, ART, non lo è. Ruboto riscrive il jar di JRuby mentre lo installa nella vostra app, usando la libreria ASM già inclusa in JRuby. I punti principali:
- Nessuna generazione di classi a runtime. ART non può caricare bytecode generato a runtime. Normalmente JRuby lo fa per gli invoker dei metodi, gli scope dinamici e le implementazioni di interfacce Java. Il fork pregenera gli invoker in fase di build, usa una classe di scope fissa e implementa le interfacce Java con
java.lang.reflect.Proxy. Blocchi, proc,Runnable.impl { }einclude SomeJavaInterfacefunzionano tutti come callback. - Classi JDK mancanti.
java.lang.ModuleeSwitchPointnon esistono su Android.ClassValueesiste solo da Android 14, e JRuby 10.1 ne ha bisogno all’avvio. Le chiamate a metodi JDK più recenti del dispositivo, comeInputStream.readAllBytesoBigInteger.TWO, vengono reindirizzate a backport. - Da Android 6 a 12. JRuby 10 usa
java.lang.invokeovunque, eVarHandleè API pubblica solo da Android 13. Per un--min-sdkpiù basso, Ruboto sposta queste chiamate su un piccolo sostituto basato su reflection esun.misc.Unsafe. Il core library desugaring aggiungejava.nio.fileejava.timesotto Android 8. - Fiber. JRuby esegue le Fiber e
Enumerator#nextsu virtual thread. Android non ne ha, quindi girano su platform thread. - La libreria standard. Le parti Java di
stringio,strscan,jsonepsychsono distribuite come jar dentro JRuby, e Android non può caricare jar a runtime. Le loro classi vengono fuse nell’app. Da Ruby 3.4,bigdecimal,csv,logger,base64,rexmle altre sono bundled gem, che normalmente si trovano tramite RubyGems. Ruboto le copia nel load path. - RubyGems è disattivato. Non può leggere le specifiche delle gem dentro un APK. Le gem in puro Ruby funzionano comunque: spacchettate la gem e mettete la sua cartella
libinapp/src/main/resources. L’esempio Todo lo fa con la gem humanize.
Le gem con estensioni C non funzionano, e nemmeno openssl. Derivare una classe Java in Ruby (class MyList < java.util.ArrayList) richiede ancora la generazione di classi a runtime e fallisce. Per HTTPS, java.net.HttpURLConnection funziona bene da Ruby.
Ogni modifica è testata con test JVM che non richiedono l’Android SDK, e con uno smoke test sul dispositivo con 65 verifiche su funzionalità Ruby, stdlib, bundled gem, socket, net/http e integrazione Java. Passa sugli emulatori Android 13 e Android 6.
La cornice Frameo
Le cornici Frameo sono economiche cornici digitali WiFi. La mia ha uno schermo 1920×1200, un Rockchip RK312x a 32 bit con quattro core Cortex-A7, 1 GB di RAM e Android 6.0.1. Esegue l’app Frameo e nient’altro. Ma ha un interruttore nascosto: Impostazioni › Info › Programma beta, poi accesso ADB. Da lì adb install funziona.
Volevo una cornice che non avesse bisogno del cloud Frameo. Così l’ho scritta io, in Ruby, circa 1300 righe:
- Le foto sono mostrate a schermo intero con dissolvenza incrociata ogni 15 secondi fino a 30 minuti, in ordine casuale o sequenziale.
- Un piccolo web server sulla porta 8080 permette a telefoni e computer sulla stessa WiFi di caricare, mostrare ed eliminare foto. È protetto da password. Il browser ridimensiona le foto alla dimensione dello schermo prima del caricamento, e la cornice le ricontrolla.
- Le foto occupano al massimo un quarto della memoria e lasciano sempre 1 GB libero.
- Un piccolo overlay mostra l’ora in due città e il meteo del giorno di MeteoSvizzera.
- Toccando lo schermo compaiono l’indirizzo web, la password e i pulsanti per la foto successiva e per chiudere l’app.
Il web server è di circa 160 righe di Ruby sopra java.net.ServerSocket. Serve una connessione alla volta e scrive i caricamenti direttamente su disco, perché 1 GB di RAM è condiviso con l’app Frameo. Le foto vengono decodificate con BitmapFactory di Android su un executor in background, poi passate al thread UI con un messaggio Handler:
@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
Il firmware Frameo non permette alle app di avviarsi da sole al boot. La soluzione è registrare l’activity come app della schermata home con un intent filter android.intent.category.HOME. Android chiede allora quale app home usare, e dopo un riavvio la cornice avvia la mia app. “Chiudi cornice” invia un intent home all’app Frameo, che riprende la propria presentazione.
Una volta avviato JRuby, la dissolvenza di foto 1920×1200 e la gestione dei caricamenti sono fluide, anche su questo piccolo chip. L’avvio per ora richiede un po’ di pazienza: su questo hardware JRuby impiega circa 20 secondi dietro lo splash screen, e sotto Android 13 il sostituto di java.lang.invoke passa per la reflection. Per una cornice conta poco, visto che si avvia una volta e poi gira per settimane. Il tempo di avvio è la prossima cosa su cui sto lavorando, quindi aspettatevi miglioramenti a breve.
Questa app è il motivo per cui il fork supporta Android 6. Prima il minimo era Android 13.
Wear OS e IRB
Le stesse app girano su Wear OS. Ci ho costruito un lettore EPUB per il mio orologio, e il fork ha due esempi IRB: uno per telefoni, che può installare gem in puro Ruby dal prompt con gem install, e uno dimensionato per l’orologio, con dettatura, perché digitare Ruby su un orologio è una sofferenza.
Due cose da sapere per Wear OS. Lo swipe indietro non chiama mai il vecchio onBackPressed: impostate android:enableOnBackInvokedCallback="true" e registrate una proc con on_back_invoked_dispatcher. Inoltre i campi Java mantengono il loro nome esatto: display_metrics.widthPixels funziona, width_pixels no. JRuby aggiunge alias snake_case solo per i metodi.
Conviene usarlo?
Per uno strumento personale, un chiosco, un dispositivo di vostra proprietà o un’app da scrivere in un pomeriggio: sì. Scrivete Ruby, avete tutte le API Android e saltate Kotlin, Compose e Android Studio. JRuby su Android è interpretato, il primo avvio su hardware datato richiede ancora un momento (ci sto lavorando) e un APK di debug pesa circa 20 MB. Per un’app con un milione di utenti sceglierei ancora Kotlin.
Il fork è su github.com/Largo/ruboto. Il README contiene l’avvio rapido completo, le note sulle versioni Android più vecchie, sulle gem e sulle callback Java, e gli esempi IRB. Issue e pull request sono benvenute.