← Articles

Écrire des apps Android en Ruby en 2026 : JRuby 10 et Ruboto

Ruboto permet d’écrire des apps Android en Ruby. Il fait tourner JRuby sur le téléphone, et vos activités sont des classes Ruby qui appellent directement l’API Android. Le projet s’était endormi : le dernier commit en amont date de mai 2023 et cible JRuby 9.4.

Je voulais le faire revivre, alors je l’ai forké : github.com/Largo/ruboto. Le fork fait tourner JRuby 10.1 (compatible Ruby 3.4) sur Android 6.0 jusqu’à Android 16 : téléphones, tablettes et montres Wear OS. Vous n’avez pas besoin d’Android Studio. Il vous faut un JDK, le SDK Android et Ruby.

La première vraie app que j’ai construite avec tourne sur un cadre photo numérique Frameo. J’y reviens plus bas.

Démarrage rapide

Il vous faut un JDK 17 ou plus récent, le SDK Android (définissez ANDROID_HOME), Gradle et Ruby.

git clone https://github.com/Largo/ruboto.git
cd ruboto && bundle install && cd ..

# Générer une app. Télécharge aussi les jars JRuby les plus récents dans libs/.
ruby -Iruboto/lib ruboto/bin/ruboto gen app --package org.example.todo --name Todo --path todo

# Utiliser la liste de tâches d'exemple comme 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

Lancez ruboto gen app avec ruby tout court, pas sous bundle exec. Bundler masque le gem jruby-jars, et aucun JRuby n’est alors copié dans l’app.

Toute l’app tient dans un seul fichier Ruby dans app/src/main/resources. Modifiez-le, reconstruisez, réinstallez. Voilà le cycle de développement.

À quoi ressemble le code

Une activité est une classe Ruby. ruboto/widget fournit un petit DSL de mise en page :

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

Vous pouvez aussi vous passer du DSL et appeler les classes Android comme le ferait Java : java_import 'android.widget.TextView', puis TextView.new(self). Toute l’API Android est là, il n’y a pas de couche d’abstraction à attendre.

Ce que j’ai dû corriger

JRuby 10 est écrit pour une JVM moderne. Le runtime d’Android, ART, n’en est pas une. Ruboto réécrit le jar JRuby pendant qu’il l’installe dans votre app, à l’aide de la bibliothèque ASM déjà incluse dans JRuby. Les points principaux :

Les gems avec des extensions C ne fonctionnent pas, pas plus que openssl. Sous-classer une classe Java en Ruby (class MyList < java.util.ArrayList) nécessite toujours la génération de classes à l’exécution et échoue. Pour HTTPS, java.net.HttpURLConnection fonctionne très bien depuis Ruby.

Chaque modification est testée avec des tests JVM qui ne nécessitent pas de SDK Android, ainsi qu’un smoke test sur appareil comprenant 65 vérifications pour les fonctionnalités Ruby, la stdlib, les bundled gems, les sockets, net/http et l’intégration Java. Il passe sur des émulateurs Android 13 et Android 6.

Le cadre photo Frameo

Les cadres Frameo sont des cadres photo WiFi bon marché. Le mien a un écran de 1920×1200, un Rockchip RK312x 32 bits avec quatre cœurs Cortex-A7, 1 Go de RAM et Android 6.0.1. Il exécute l’app Frameo et rien d’autre. Mais il a un interrupteur caché : Paramètres › À propos › Programme bêta, puis accès ADB. Ensuite, adb install fonctionne.

Je voulais un cadre qui n’ait pas besoin du cloud Frameo. Alors j’en ai écrit un, en Ruby, environ 1300 lignes :

Le serveur web représente environ 160 lignes de Ruby au-dessus de java.net.ServerSocket. Il sert une connexion à la fois et écrit les envois directement sur le disque, car 1 Go de RAM est partagé avec l’app Frameo. Les photos sont décodées avec le BitmapFactory d’Android sur un executor en arrière-plan, puis transmises au thread UI avec un message 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

Le firmware Frameo ne permet pas aux apps de se lancer elles-mêmes au démarrage. La parade consiste à enregistrer l’activité comme app d’écran d’accueil avec un intent filter android.intent.category.HOME. Android demande alors quelle app d’accueil utiliser, et le cadre lance mon app après un redémarrage. « Fermer le cadre photo » envoie un intent d’accueil à l’app Frameo, qui reprend son propre diaporama.

Une fois JRuby lancé, les fondus de photos en 1920×1200 et la réception des envois sont fluides, même sur cette petite puce. Le démarrage demande encore un peu de patience : sur ce matériel, JRuby a besoin d’environ 20 secondes derrière l’écran de démarrage, et en dessous d’Android 13, le substitut de java.lang.invoke passe par la réflexion. Pour un cadre photo, cela importe peu, puisqu’il démarre une fois puis tourne pendant des semaines. Le temps de démarrage est mon prochain chantier : attendez-vous donc à des améliorations bientôt.

C’est grâce à cette app que le fork prend en charge Android 6. Avant, le minimum était Android 13.

Wear OS et IRB

Les mêmes apps tournent sur Wear OS. J’ai construit avec un lecteur EPUB pour ma montre, et le fork contient deux exemples IRB : un pour téléphone, qui peut installer des gems en pur Ruby depuis l’invite avec gem install, et un dimensionné pour une montre, avec dictée, car taper du Ruby sur une montre est un calvaire.

Deux choses à savoir pour Wear OS. Le geste de retour n’appelle jamais l’ancien onBackPressed. Définissez android:enableOnBackInvokedCallback="true" et enregistrez un proc avec on_back_invoked_dispatcher. Et les champs Java gardent leur nom exact : display_metrics.widthPixels fonctionne, width_pixels non. JRuby n’ajoute des alias snake_case que pour les méthodes.

Faut-il l’utiliser ?

Pour un outil personnel, un kiosque, un appareil qui vous appartient ou une app que vous voulez écrire en un après-midi : oui. Vous écrivez du Ruby, vous obtenez toute l’API Android, et vous vous passez de Kotlin, de Compose et d’Android Studio. JRuby sur Android est interprété, le premier démarrage sur du matériel ancien prend encore un moment (j’y travaille), et un APK de debug pèse environ 20 Mo. Pour une app avec un million d’utilisateurs, je choisirais toujours Kotlin.

Le fork se trouve sur github.com/Largo/ruboto. Le README contient le démarrage rapide complet, les notes sur les anciennes versions d’Android, les gems et les callbacks Java, ainsi que les exemples IRB. Issues et pull requests sont les bienvenues.