← Blog

2026年、RubyでAndroidアプリを書く:JRuby 10とRuboto

Rubotoを使うと、AndroidアプリをRubyで書けます。端末の上でJRubyが動き、ActivityはAndroid APIを直接呼び出すRubyのクラスです。ただ、このプロジェクトは止まっていました。本家の最後のコミットは2023年5月で、対象はJRuby 9.4です。

もう一度使えるようにしたくて、フォークしました:github.com/Largo/ruboto。このフォークは JRuby 10.1(Ruby 3.4互換)で、Android 6.0からAndroid 16までのスマートフォン、タブレット、Wear OSのウォッチで動きます。Android Studioは不要です。必要なのはJDK、Android SDK、Rubyだけです。

これで作った最初の本格的なアプリは、Frameoのデジタルフォトフレームで動いています。詳しくは後ほど。

クイックスタート

JDK 17以降、Android SDK(ANDROID_HOMEを設定)、Gradle、Rubyが必要です。

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

# アプリを生成。最新のJRubyのjarもlibs/にダウンロードされます。
ruby -Iruboto/lib ruboto/bin/ruboto gen app --package org.example.todo --name Todo --path todo

# サンプルのTodoリストをアプリとして使う
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

ruboto gen appはbundle execではなく、普通のrubyで実行してください。Bundlerがjruby-jars gemを隠してしまい、JRubyがアプリにコピーされなくなります。

アプリ全体はapp/src/main/resourcesにあるRubyファイル1つです。編集して、ビルドして、インストールし直す。開発サイクルはそれだけです。

コードの見た目

ActivityはRubyのクラスです。ruboto/widgetには、レイアウト用の小さな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

DSLを使わず、Javaと同じようにAndroidのクラスを呼ぶこともできます。java_import 'android.widget.TextView'して、TextView.new(self)。Android APIはすべて使えるので、ラッパーができるのを待つ必要はありません。

直したところ

JRuby 10はモダンなJVM向けに書かれています。AndroidのランタイムであるARTはそうではありません。Rubotoは、アプリにJRubyをインストールするときに、JRubyに同梱されているASMライブラリでJRubyのjarを書き換えます。主なポイント:

C拡張のあるgemは動きません。opensslもありません。RubyでJavaクラスを継承する(class MyList < java.util.ArrayList)には今でも実行時のクラス生成が必要で、失敗します。HTTPSには、Rubyからjava.net.HttpURLConnectionを使えば問題ありません。

変更はすべてテストしています。Android SDKなしで動くJVMテストと、端末上のスモークテストです。スモークテストはRubyの機能、標準ライブラリ、bundled gem、ソケット、net/http、Java連携を65項目で確認し、Android 13とAndroid 6のエミュレータで通ります。

Frameoのフォトフレーム

Frameoは安いWi-Fiフォトフレームです。私のものは1920×1200の画面、Cortex-A7を4コア持つ32ビットのRockchip RK312x、RAM 1 GB、Android 6.0.1です。Frameoのアプリだけが動き、ほかには何もありません。でも隠しスイッチがあります。設定 › 情報 › ベータプログラム、それからADBアクセス。これでadb installが使えます。

Frameoのクラウドを使わないフォトフレームが欲しかったので、自分で書きました。Rubyで約1300行です。

Webサーバーはjava.net.ServerSocketの上に書いた約160行のRubyです。接続を1つずつ処理し、アップロードはそのままストレージに書き込みます。1 GBのRAMをFrameoのアプリと分け合うからです。写真はバックグラウンドのExecutorでAndroidのBitmapFactoryを使ってデコードし、HandlerのメッセージでUIスレッドに渡します。

@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

Frameoのファームウェアは、起動時にアプリが自分で立ち上がることを許しません。回避策は、android.intent.category.HOMEのintent filterで、Activityをホーム画面アプリとして登録することです。Androidがどのホームアプリを使うか聞いてくるので、選んでおけば再起動後に私のアプリが立ち上がります。「Close photo frame」ボタンはFrameoのアプリにホームのintentを送り、Frameoは自分のスライドショーを再開します。

速くはありません。このハードウェアではJRubyの起動にスプラッシュ画面の裏で約20秒かかります。Android 13未満ではjava.lang.invokeの代替がリフレクションを使うので、インスタンス変数やJavaの呼び出しも遅くなります。でもフォトフレームなら問題ありません。一度起動したら何週間も動き続けます。JRubyが立ち上がってしまえば、1920×1200の写真のクロスフェードもアップロードもスムーズです。

フォークがAndroid 6に対応したのは、このアプリのためです。それまでの最低バージョンはAndroid 13でした。

Wear OSとIRB

同じアプリがWear OSでも動きます。これでウォッチ用のEPUBリーダーを作りました。フォークにはIRBのサンプルが2つあります。スマートフォン用は、プロンプトでgem installしてピュアRubyのgemを入れられます。ウォッチ用は画面に合わせたもので、音声入力ボタンがあります。ウォッチでRubyを打つのはつらいからです。

Wear OSで知っておくべきことが2つあります。スワイプで戻っても、古いonBackPressedは呼ばれません。android:enableOnBackInvokedCallback="true"を設定し、on_back_invoked_dispatcherにProcを登録してください。それから、Javaのフィールドは名前がそのままです。display_metrics.widthPixelsは動きますが、width_pixelsは動きません。JRubyがsnake_caseの別名を作るのはメソッドだけです。

使うべきか

個人用のツール、キオスク端末、自分の持っているデバイス、午後だけで書きたいアプリなら、使えます。Rubyを書けば、Android APIがすべて使え、KotlinもComposeもAndroid Studioもいりません。AndroidのJRubyはインタプリタで動き、起動に時間がかかり(ウォッチや古いフォトフレームで約20秒)、デバッグ版APKは約20 MBです。100万人が使うアプリなら、私は今でもKotlinを選びます。

フォークはgithub.com/Largo/rubotoにあります。READMEには、クイックスタートの全文、古いAndroidバージョン、gem、Javaコールバックについての注意、IRBのサンプルがあります。IssueやPull Requestは歓迎します。