Up and Running with Hotwire Native Android Part 3 - Native Screens

In my previous article, I explained the steps needed to set up path configuration. Path Configuration is a vital concept in both Hotwire Native iOS and Hotwire Native Android. It allows you to display certain content as web screens, modals, or native screens. Crucially, you can update this from the server to facilitate deployment compared to the app store process.

Today, we will render a native screen in our Hotwire Android app. As of this writing, Android is in a state of transition. First, there are Android views, which are XML files that you markup with your own logic.

The second way is to use Jetpack Compose, a new library similar to SwiftUI.

Even though my time working with Android is brief, I have mostly encountered Android Views. Jetpack compose has some way to go before it is more widely adopted.

Regardless of my speculation, let’s dig in.

Configuring our Fragments

Before we create any native screens, we have to create web fragments that inherit from Hotwire Android. This is to lay the foundation for native components.

In our main directory, create a new package called features and create the following two fragments.

First, create WebFragment

import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink  
import dev.hotwire.navigation.fragments.HotwireWebFragment  
  
@HotwireDestinationDeepLink(uri = "hotwire://fragment/web")  
class WebFragment : HotwireWebFragment() {}

Next, create WebBottomSheetFragment

import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink  
import dev.hotwire.navigation.fragments.HotwireWebBottomSheetFragment

@HotwireDestinationDeepLink(uri = "hotwire://fragment/web/modal/sheet")  
class WebBottomSheetFragment : HotwireWebBottomSheetFragment() {}

Now we need to register these fragments in our HotwireApplication. Note that we set a default fragment destination

class HotwireApplication: Application() {  
    ...
  
    private fun configureApp() {  
        ...
    
        Hotwire.defaultFragmentDestination = WebFragment::class  
  
        Hotwire.registerFragmentDestinations(  
            WebFragment::class,  
            WebBottomSheetFragment::class,  
        )  
    }  
}

Run the app just to make sure everything is working.

Native Fragment with Android Views

There are 3 things we need to do to render a native screen.

  1. Adjust our Path Configuration
  2. Create the Native Fragment
  3. Register our Fragment destination

Step 2 is obviously the hardest 🤣

Let’s start by adjusting our path-configuration.json and adding a new rule.

{  
...
    {  
      "patterns": [  
        "/native"  
      ],  
      "properties": {  
        "context": "modal",  
        "uri": "hotwire://fragment/hello_world",  
        "pull_to_refresh_enabled": false  
      }  
    }  
  ]  
}

Now let’s create a new fragment to live alongside our other fragments.

Let’s call it HelloWorldFragment

@HotwireDestinationDeepLink(uri = "hotwire://fragment/hello_world")  
class HelloWorldFragment: HotwireFragment() {  
    override fun onCreateView(  
        inflater: LayoutInflater,  
        container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ): View? {  
        return inflater.inflate(R.layout.hello_world, container, false)  
    }  
}

Now, we need to create a new layout called hello_world, which will live in res/layout.

<?xml version="1.0" encoding="utf-8"?>  
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:app="http://schemas.android.com/apk/res-auto"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent">  
  
    <TextView  
        android:id="@+id/text_view_id"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="@string/hello_world"  
        app:layout_constraintBottom_toBottomOf="parent"  
        app:layout_constraintEnd_toEndOf="parent"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toTopOf="parent" />  
</androidx.constraintlayout.widget.ConstraintLayout>

This renders a TextView in the middle of the screen with the string hello world. In your app, you may have to create that string resource.

Now, when we run the app and navigate to /native, you should see “Hello World” rendered right in the middle.

Android views are quite popular, but Jetpack compose is also a wonderful technology, so let’s figure out how to do that next.

Native Fragment with Jetpack Compose

Installing Jetpack Compose Compiler

The first thing you’ll need is the Jetpack Compose library. This requires a few steps, as we did not select Jetpack Compose when setting up our app.

First, we have to configure the Compose Compiler Gradle Plugin.

In our libs.versions.toml, we need to update our Kotlin version and add the compose compiler.

[versions]  
...
-kotlin = "1.9.24"
+kotlin = "2.0.0"
...

[plugins]  
...
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

Now, in our project build.gradle.kts file, we add the following:

plugins {
  ...
  alias(libs.plugins.compose.compiler) apply false
}

Now, in our app build.gradle.kts, under plugins, we add.

plugins {
  ...
  alias(libs.plugins.compose.compiler) apply false
}

Now we have one final step in our app build.gradle.kts file which is to add some build features.

android {  
   ...
  
    buildFeatures {  
        compose = true  
    }  
}

Whoo. Make sure everything can sync and build before we move on to add the Jetpack Compose libraries.

Installing Jetpack Compose

In our app build.gradle.kts file under our dependencies, we add the following.

dependencies {  
    implementation(libs.hotwire.core)  
  
    val composeBom = platform("androidx.compose:compose-bom:2024.12.01")  
    implementation(composeBom)  
    androidTestImplementation(composeBom)  
  
  
    // Material Design 3  
    implementation("androidx.compose.material3:material3")  
      
    // Android Studio Preview support  
    implementation("androidx.compose.ui:ui-tooling-preview")  
    debugImplementation("androidx.compose.ui:ui-tooling")  
  
    // UI Tests  
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")  
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    ...
}

Make sure you click “Sync Now” when prompted in Android Studio.

Build our first Jetpack Compose Component

Now that we have Jetpack Compose installed(and what a process that is), we are now ready to build our native screens using Jetpack Compose.

Let’s update our HelloWorldFragment to use Jetpack Compose.

import android.os.Bundle  
import android.view.LayoutInflater  
import android.view.View  
import android.view.ViewGroup  
import androidx.compose.foundation.layout.Arrangement  
import androidx.compose.foundation.layout.Column  
import androidx.compose.material3.Text  
import androidx.compose.runtime.Composable  
import androidx.compose.ui.Alignment  
import androidx.compose.ui.platform.ComposeView  
import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink  
import dev.hotwire.navigation.fragments.HotwireFragment  
  
@HotwireDestinationDeepLink(uri = "hotwire://fragment/hello_world")  
class HelloWorldFragment: HotwireFragment() {  
    override fun onCreateView(  
        inflater: LayoutInflater,  
        container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ): View {  
        return ComposeView(requireContext()).apply {  
            setContent {  
                Hello()  
            }  
        }    }  
  
    @Composable  
    fun Hello() {  
        Column(  
            verticalArrangement = Arrangement.Center,  
            horizontalAlignment = Alignment.CenterHorizontally  
        ) {  
            Text("Hello World from Jetpack compose")  
        }  
    }  
}

Now when you build and run the app, then navigate to the native screen, you’ll see that everything works just as it did before.

Next Steps

We have seen the different ways of building a native screen in Android and navigating to it. Whether you use Jetpack Compose or Android views is a matter of preference.

Native screens are one of the features requested the most when I work with clients. However, there is sometimes a better way to access native functionality.

Did you know you can interact with the hardware using a Javascript bridge?

We’ll cover that in the next article.

Until then, happy hacking.

Learn to Build Android, iOS and Rails Apps using Hotwire

© 2025 William Kennedy, Inc. All rights reserved.