Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般
  • Ruby / Rails関連

Rails: Hotwire Nativeで作るネイティブモバイルアプリ: Android編(3)ネイティブ画面(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

従来Turbo NativeとStradaと呼ばれていたものは、現在はHotwire Nativeに統合されました。

参考: Hotwire Native: Hotwire Native is a web-first framework for building native mobile apps.

hotwired/hotwire-native-android - GitHub

Rails: Hotwire Nativeで作るネイティブモバイルアプリ: Android編(3)ネイティブ画面(翻訳)

前回の記事では、パス構成(path configuration)を設定するために必要な手順について説明しました。パス構成は、Hotwire Native iOSとHotwire Native Androidの両方で重要な概念です。これにより、特定のコンテンツをWeb画面、モーダル、またはネイティブ画面として表示できます。重要なのは、これをサーバーから更新することで、アプリストアでデプロイする場合よりも楽に変更をデプロイできることです。

今回は、Hotwire Androidアプリでネイティブ画面をレンダリングします。本記事の執筆時点では、Hotwire Native Android版は移行中の段階にあります。

第1に、Android Viewsを使う方法があります。これは、独自のロジックでマークアップするXMLファイルです。

第2に、SwiftUIに似たJetpack Composeを使う方法があります。

私がAndroidに携わったのは短い期間ですが、これまで出会ったのはほとんどがAndroid Viewsでした。Jetpack Composeがもっと普及するにはもう少し時間がかかりそうです。

私の予測の話はおいといて、本題に入りましょう。

訳注
Railsアプリも以下のiOS編(3)と同様に設定しておく必要があります。

Rails: Hotwire Nativeで作るネイティブモバイルアプリ: iOS編(3)ブリッジコンポーネント(翻訳)

🔗 Webフラグメントを設定する

ネイティブ画面を作成する前に、Hotwire Androidから継承するWebフラグメントを作成する必要があります。これをネイティブコンポーネントの基盤とします。

mainディレクトリでfeaturesという新しいパッケージを作成し、次の2つのフラグメントを作成します。

  • 1: WebFragmentを追加します。
// app/src/main/java/com/example/hotwireexample/WebFragment.kt
package com.example.hotwireexample

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

@HotwireDestinationDeepLink(uri = "hotwire://fragment/web")
class WebFragment : HotwireWebFragment() {}
  • 2: WebBottomSheetFragmentを追加します。
// app/src/main/java/com/example/hotwireexample/WebBottomSheetFragment.kt
package com.example.hotwireexample

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

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

次に、これらのフラグメントをHotwireApplicationに登録する必要があります。defaultFragmentDestinationを設定していることにご注目ください。

// app/src/main/java/com/example/hotwireexample/HotwireApplication.kt
package com.example.hotwireexample.main

import android.app.Application
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.turbo.config.PathConfiguration
import dev.hotwire.navigation.config.defaultFragmentDestination
import dev.hotwire.navigation.config.registerFragmentDestinations
import com.example.hotwireexample.*

class HotwireApplication: Application() {
  override fun onCreate() {
    super.onCreate()
    configureApp()
  }

  private fun configureApp() {
    Hotwire.loadPathConfiguration(
      context = this,
      location = PathConfiguration.Location(
        assetFilePath = "json/path-configuration.json"
      )
    )

    Hotwire.defaultFragmentDestination = WebFragment::class

    Hotwire.registerFragmentDestinations(
      WebFragment::class,
      WebBottomSheetFragment::class,
    )
  }
}

この時点でアプリを実行して、すべて順調に動いていることを確認します。

🔗 新規フラグメントをAndroid Viewsで作成する場合

ネイティブ画面をレンダリングするには、以下の3つの作業が必要です。

  1. パス構成を調整する
  2. ネイティブフラグメントを作成する
  3. フラグメントのdestinationを登録する

最も面倒なのはステップ2です。🤣

最初にpath-configuration.jsonに以下の新しいルールを追加しましょう。

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

次に、他のフラグメントと一緒に存在する新しいフラグメントを作成しましょう。

このフラグメントをHelloWorldFragmentと呼ぶことにします。

// app/src/main/java/com/example/hotwireexample/HelloWorldFragment.kt
package com.example.hotwireexample

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink
import dev.hotwire.navigation.fragments.HotwireFragment
import com.example.hotwireexample.main.*

@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)
  }
}

次に、hello_worldというレイアウトを以下の内容で作成する必要があります。これは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>

これで、画面中央にTextViewが表示され、そこに"hello world"が表示されます。この"@string/hello_world"に対応する文字列リソースをアプリで作成しておく必要があるでしょう。

訳注

これを行うには、res/values/strings.xmlに以下を追加する必要があります。

<resources>
    <string name="app_name">Hotwire Example</string>
+   <string name="hello_world">Hello World</string>
</resources>

これで、アプリを実行して/nativeに移動すると、真ん中に「Hello World」がレンダリングされるはずです。

Android Viewsは非常に人気がありますが、Jetpack Composeも素晴らしい技術なので、次はJetpack Composeを使う方法を考えてみましょう。

🔗 新規フラグメントをJetpack Composeで作成する場合

🔗 Jetpack Composeコンパイラをインストールする

最初に必要なのはJetpack Composeライブラリです。Android Studioのセットアップ時にJetpack Composeを選んでいなかったので、インストールが必要です。

最初に、Compose Compiler Gradle Pluginを設定します。

libs.versions.tomlファイルで、以下のようにKotlinのバージョンを変更し、Compose Compilerをプラグインに追加します。

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

次に、プロジェクト直下のbuild.gradle.ktsファイルに以下を追加します。


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

次に、app/ディレクトリの下にあるbuild.gradle.ktsファイルに以下を追加します。

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

app/ディレクトリの下にあるbuild.gradle.ktsファイルで行う最後の手順として、以下のbuildFeaturesを追加します。

android {
   ...

    buildFeatures {
        compose = true
    }
}

ふう。すべてが同期・ビルド可能であることを確認してからJetpack Compose ライブラリの追加に進みましょう。

🔗 Jetpack Composeをインストールする

アプリ直下のbuild.gradle.ktsファイルの依存関係に以下を追加します。

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")
}

Android Studioで"Sync Now"が表示されたら必ずクリックしてください。

![image.png](https://image.docbase.io/uploads/4e367e63-1f02-48c0-9ecc-6cf126b2d70c.png =WxH)

🔗 Jetpack Compose Componentを初めてビルドする

Jetpack Composeがインストールされたので(これは面倒な作業です)、Jetpack Composeでネイティブ画面を構築する準備が整いました。

HelloWorldFragmentの内容を以下で置き換えてJetpack Composeが使われるようにしましょう。

// app/src/main/java/com/example/hotwireexample/HelloWorldFragment.k
package com.example.hotwireexample

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")
    }
  }
}

これで、アプリをビルドして実行し、/nativeのネイティブ画面に移動すると、すべてAndroid Viewsと同じように動くことがわかります。

次回

Androidでネイティブ画面を構築し、そこへ移動するさまざまな方法を見てきました。Jetpack ComposeとAndroid Viewsのどちらを使うかはお好み次第です。

ネイティブ画面は、顧客と仕事をしていると最も求められる機能の1つです。ただし、ネイティブ機能にアクセスするには、よりよい方法がある場合もあります。

Javascriptブリッジでデバイスと通信できることをご存知ですか?次回の記事ではこれについて解説します。

それまで、Happy hacking!

関連記事

Rails: Hotwire Nativeで作るネイティブモバイルアプリ: Android編(1)セットアップ(翻訳)

Rails: Hotwire Nativeで作るネイティブモバイルアプリ: Android編(2)パス構成(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。