Skip to content
April 09, 2026 · 4 min read Ruby on Rails

Rails 8 to Native Android in 200 Lines of Kotlin — Part 2: Auto-Updates Without the Play Store

In Part 1, we built the WebView shell with bottom tabs, session persistence, and server-side detection. Now we add something the Play Store gives you for free — but we'll do it ourselves, with more control.

The Problem

When you distribute an APK outside the Play Store, users have no way to know when updates are available. They'll use version 1.0 forever unless you tell them. We need:

  1. Automatic version checking on app launch
  2. Update prompt with what's new
  3. Force update capability for breaking changes
  4. Direct download from your own server

The Rails API Endpoint

First, create an endpoint that returns the current app version:

ruby
# config/routes.rb
namespace :api do
  namespace :v1 do
    get "app_version", to: "app_version#show"
  end
end
ruby
# app/controllers/api/v1/app_version_controller.rb
module Api
  module V1
    class AppVersionController < ApplicationController
      skip_before_action :authenticate_user!

      def show
        render json: {
          version_code: 3,
          version: "1.2.0",
          download_url: "https://yourapp.com/mobile",
          message: "New: Video calling and staff meetings!",
          force_update: false
        }
      end
    end
  end
end

version_code is the integer that matters — it's what the app compares against. version is the display string for the prompt. force_update is the nuclear option — when true, the user cannot dismiss the dialog.

In practice, update this controller (or back it with a database record / SiteSetting) when you ship a new APK.

The Android Update Checker

kotlin
class UpdateChecker(private val activity: AppCompatActivity) {

    fun check() {
        // Run on background thread
        Thread {
            try {
                val url = URL("${BuildConfig.BASE_URL}/api/v1/app_version")
                val connection = url.openConnection() as HttpURLConnection
                connection.requestMethod = "GET"
                connection.connectTimeout = 5000

                if (connection.responseCode == 200) {
                    val response = connection.inputStream.bufferedReader().readText()
                    val json = JSONObject(response)

                    val serverVersionCode = json.getInt("version_code")
                    val currentVersionCode = getLocalVersionCode()

                    if (serverVersionCode > currentVersionCode) {
                        val version = json.getString("version")
                        val downloadUrl = json.getString("download_url")
                        val message = json.optString("message", "A new version is available.")
                        val forceUpdate = json.optBoolean("force_update", false)

                        // Show dialog on main thread
                        activity.runOnUiThread {
                            showUpdateDialog(version, message, downloadUrl, forceUpdate)
                        }
                    }
                }
            } catch (e: Exception) {
                // Silent fail — don't block the app if API is unreachable
            }
        }.start()
    }
}

Design decisions:
- Background thread — doesn't block the UI while checking
- Silent failure — if the API is down or the user is offline, the app works normally
- 5-second timeout — doesn't hang on slow connections

The Update Dialog

When an update is available, show an AlertDialog:

  • Normal update: Title, message, "Update" and "Later" buttons. User can dismiss.
  • Force update: Same dialog but setCancelable(false) and no "Later" button. User must update to continue.

The "Update" button opens the download URL in the system browser — your Rails app serves the download page where the APK is hosted.

Triggering the Check

Call it once on app launch in MainActivity.onCreate:

kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // ... setup code ...

    if (savedInstanceState == null) {
        UpdateChecker(this).check()
    }
}

The savedInstanceState == null check ensures it only runs on fresh launch, not on configuration changes (screen rotation).

Hosting the APK

Your Rails app needs a download page and the APK file:

ruby
# config/routes.rb
get "mobile", to: "pages#download"

The view shows your app icon, version info, installation instructions, and a download button pointing to the APK file in public/.

Post-deploy script — if you use Hatchbox or Capistrano, the current directory changes on every deploy. Copy the APK from shared to current:

bash
# In your deploy hook or post-deploy script
cp ~/app/shared/public/app.apk ~/app/current/public/app.apk

Without this, the download link breaks after every deploy.

Version Management Workflow

  1. Make changes to the Android app
  2. Increment versionCode in build.gradle
  3. Build the APK: ./gradlew assembleDebug
  4. Upload APK to your server's shared/public/
  5. Update the Rails API endpoint with the new version_code
  6. Deploy Rails
  7. Users get the update prompt on next app launch

For force updates (breaking changes, security patches):
- Set force_update: true in the API response
- Users see a non-dismissible dialog on launch
- App is unusable until they update


What You Have Now

Your app:
- Checks for updates on every launch (silently)
- Shows a friendly prompt when updates are available
- Can force users to update for critical releases
- Downloads directly from your server — no Play Store
- Handles offline and API failures gracefully


Next: Part 3 — Video Permissions, Scroll Fixes, and Production Polish

We'll handle the hardest part of WebView apps — bridging native camera/microphone permissions for embedded video calls, fixing the infamous scroll conflict, and polishing for production distribution.

Shipping your Rails app to Android? Reach out — I'll help you skip the pitfalls.

Filed under Ruby on Rails

Discussion

Comments

Leave a comment

No comments yet. Be the first to share your thoughts!

Enjoyed this post?

Subscribe to get notified about new articles.