← Blog

Writing Android apps in Ruby in 2026: JRuby 10 and Ruboto

Ruboto lets you write Android apps in Ruby. It runs JRuby on the phone, and your activities are Ruby classes that call the Android API directly. The project went quiet: the last upstream commit is from May 2023 and targets JRuby 9.4.

I wanted it back, so I forked it: github.com/Largo/ruboto. The fork runs JRuby 10.1 (Ruby 3.4 compatible) on Android 6.0 up to Android 16, phones, tablets and Wear OS watches. You do not need Android Studio. You need a JDK, the Android SDK and Ruby.

The first real app I built with it runs on a Frameo digital photo frame. More on that below.

Quick start

You need JDK 17 or newer, the Android SDK (set ANDROID_HOME), Gradle and Ruby.

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

# Generate an app. This also downloads the newest JRuby jars into libs/.
ruby -Iruboto/lib ruboto/bin/ruboto gen app --package org.example.todo --name Todo --path todo

# Use the sample todo list as the 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

Run ruboto gen app with plain ruby, not under bundle exec. Bundler hides the jruby-jars gem, and then no JRuby gets copied into the app.

The whole app is one Ruby file in app/src/main/resources. Edit it, rebuild, reinstall. That is the development cycle.

What the code looks like

An activity is a Ruby class. ruboto/widget gives you a small layout DSL:

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

You can also skip the DSL and call the Android classes the way Java would. java_import 'android.widget.TextView', then TextView.new(self). Every Android API is there, there is no wrapper layer to wait for.

What I had to fix

JRuby 10 is written for a modern JVM. Android’s runtime, ART, is not one. Ruboto rewrites the JRuby jar while it installs it into your app, using the ASM library that already ships inside JRuby. The main points:

Gems with C extensions do not work, and neither does openssl. Subclassing a Java class in Ruby (class MyList < java.util.ArrayList) still needs runtime class generation and fails. For HTTPS, java.net.HttpURLConnection works fine from Ruby.

Each change is tested with JVM tests that need no Android SDK, and a device smoke test with 65 checks for Ruby features, the stdlib, the bundled gems, sockets, net/http and Java integration. It passes on Android 13 and Android 6 emulators.

The Frameo photo frame

Frameo frames are cheap WiFi picture frames. Mine has a 1920×1200 screen, a 32-bit Rockchip RK312x with four Cortex-A7 cores, 1 GB of RAM and Android 6.0.1. It runs the Frameo app and nothing else. But it has a hidden switch: Settings › About › Beta program, then ADB access. After that, adb install works.

I wanted a frame that does not need the Frameo cloud. So I wrote one, in Ruby, about 1300 lines:

The web server is about 160 lines of Ruby on top of java.net.ServerSocket. It serves one connection at a time and streams uploads straight to disk, because 1 GB of RAM is shared with the Frameo app. Photos are decoded with Android’s BitmapFactory on a background executor, then handed to the UI thread with a Handler message:

@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

The Frameo firmware does not let apps start themselves at boot. The workaround is to register the activity as a home screen app with an android.intent.category.HOME intent filter. Android then asks which home app to use, and the frame starts my app after a reboot. “Close photo frame” sends a home intent to the Frameo app, which resumes its own slideshow.

It is not fast. JRuby takes about 20 seconds to start on this hardware, behind a splash screen. Below Android 13 the java.lang.invoke stand-in uses reflection, so instance variables and Java calls are slower. For a photo frame that does not matter: it starts once and then runs for weeks. Once JRuby is up, cross-fading 1920×1200 photos and serving uploads is smooth.

This app is why the fork supports Android 6. Before it, the minimum was Android 13.

Wear OS and IRB

The same apps run on Wear OS. I built an EPUB reader for my watch with it, and the fork has two IRB samples: one for phones that can install pure-Ruby gems at the prompt with gem install, and one sized for a watch, with dictation because typing Ruby on a watch is miserable.

Two things to know for Wear OS. Swiping back never calls the old onBackPressed. Set android:enableOnBackInvokedCallback="true" and register a proc with on_back_invoked_dispatcher. And Java fields keep their exact names: display_metrics.widthPixels works, width_pixels does not. JRuby only adds snake_case aliases for methods.

Should you use it?

For a personal tool, a kiosk, a device you own or an app you want to write in an afternoon: yes. You write Ruby, you get the whole Android API, and you skip Kotlin, Compose and Android Studio. JRuby on Android is interpreted, starting it takes a while (about 20 seconds on a watch or an old photo frame), and a debug APK is about 20 MB. For an app with a million users I would still pick Kotlin.

The fork is at github.com/Largo/ruboto. The README has the full quick start, the notes on older Android versions, gems and Java callbacks, and the IRB samples. Issues and pull requests are welcome.