feat: 集成服务端驱动 UI 演示

新增 Remote Compose 播放端与服务端生成逻辑,支持按页面拉取并渲染二进制 UI 文档。
This commit is contained in:
2026-08-14 10:43:44 +08:00
parent c37b8ff0a4
commit 2bb3255704
11 changed files with 447 additions and 29 deletions

97
AGENTS.md Normal file
View File

@@ -0,0 +1,97 @@
# AGENTS.md
本仓库是多模块 Monorepo演示 **服务端驱动 UIServer-Driven UI**:后端用 AndroidX Remote Compose 把 UI 编译成二进制文档Android 端下载后由 `RemoteComposePlayer` 原生渲染。
## 仓库结构
```
VibeCoding_ComposeRemote/
├── ComposeRemoteAndroid/ # 【可改】Android 客户端Kotlin + Jetpack Compose
├── ComposeRemoteServer/ # 【可改】Spring Boot 4 后端(生成 Remote Compose 二进制文档)
├── remotecompose/ # 【只读】GitHub 开源Web 编辑器 + JVM 转换器 + .rc 二进制产物
└── remotecompose-android/ # 【只读】GitHub 开源:纯播放端示例 App
```
> ⚠️ `remotecompose/` 与 `remotecompose-android/` 是上游 GitHub 开源项目,**禁止修改**。
> 仅可在 `ComposeRemoteAndroid/` 与 `ComposeRemoteServer/` 中编写代码。
## 架构与数据流
```
CREATION SIDE (JVM) PLAYBACK SIDE (Android)
┌──────────────────────┐ binary ┌──────────────────────────┐
│ ComposeRemoteServer │ .rc bytes │ ComposeRemoteAndroid │
│ RemoteComposeWriter │ ── HTTP ───▶ │ OkHttp GET → RemoteDoc │
│ → byte[] (octet) │ (GET /card) │ RemoteComposePlayer 渲染 │
└──────────────────────┘ └──────────────────────────┘
```
- 后端只负责「创建」文档;前端只负责「播放」文档,**两端都不解析 JSON 布局、不共享创建端源码**。
- 通信协议Android 向 `GET /api/remote-ui/card` 发请求,后端返回 `application/octet-stream` 的二进制 `byte[]`,前端用 `RemoteDocument(bytes)` 解码。
## 1. ComposeRemoteAndroid可改
- 类型Android ApplicationKotlin + Jetpack Compose声明式 UI
- 构建GradleAGP `9.2.1`Kotlin `2.4.0`Compose BOM `2026.02.01`)。
- SDK`minSdk 29` / `targetSdk 36` / `compileSdk 36.1`Java 语言级别 11。
- 关键依赖:
- `androidx.compose.remote:remote-player-core / -compose / -view:1.0.0-alpha17`(播放端)
- `com.squareup.okhttp3:okhttp:4.12.0`(下载二进制文档)
- 网络:`AndroidManifest.xml` 已声明 `INTERNET` 权限,且 `res/xml/network.xml` 允许明文流量(`cleartextTrafficPermitted=true`)。
- 关键文件:
- `app/src/main/java/com/ttstd/composeremote/MainActivity.kt` — 入口,`enableEdgeToEdge()`,硬编码服务端地址(`192.168.1.5:8000`)。
- `app/src/main/java/com/ttstd/composeremote/ui/RemoteUIScreen.kt` — 拉取二进制 → `RemoteDocument``RemoteComposePlayer` 渲染;含 loading / error 状态。
- `ui/theme/` — Material 3 主题(默认未启用 `ComposeRemoteAndroidTheme`)。
- 构建/运行(在 `ComposeRemoteAndroid/` 内):
- `./gradlew :app:assembleDebug`
- `./gradlew :app:installDebug`(需连接模拟器/设备)
- 约定:
- 服务端地址改为常量或 `BuildConfig`/`local.properties` 注入,勿在 `MainActivity` 中硬编码内网 IP。
- 播放端代码不要引入 `remote-creation-*`(创建端)依赖。
## 2. ComposeRemoteServer可改
- 类型Spring Boot 4.1.0 Web MVC 应用Java 17Maven 构建。
- 关键依赖:
- `spring-boot-starter-webmvc``jackson-databind``lombok``spring-boot-starter-test`
- `androidx.compose.remote:remote-creation-jvm:1.0.0-alpha17`(服务端生成二进制文档)
- 仓库配置:`maven.google.com`Remote Compose artifacts 来自 Google Maven
- 关键文件:
- `src/main/java/com/ttstd/composeremote/server/controller/RemoteUIController.java`
- `GET /api/remote-ui/card``produces = APPLICATION_OCTET_STREAM`,返回二进制 `byte[]`
- 内部用 `RemoteComposeWriter`Profile: `DOCUMENT_API_LEVEL` / `PROFILE`1080×1920density 1.0)写 `header``startRoot`/`column`/`textComponent``encodeToByteArray()`
- 构建/运行(在 `ComposeRemoteServer/` 内):
- `./mvnw spring-boot:run`(默认端口 8080 → 需与客户端地址一致,例如 8000
- `./mvnw clean package`
- 约定:
- 新增 UI 时复用 `buildRemoteDocument()` 的 writer/Profile 模式;保持 apiLevel/profiles 与 Android 播放端兼容。
- 注意 **Remote Compose 版本一致性**:服务端 `remote-creation-jvm``1.0.0-alpha17`Android `remote-player` 同为 `alpha17`;而只读的 `remotecompose/server` 用的是 `alpha05`,不要混用。
## 3. remotecompose只读禁止修改
- 上游:[armcha/remotecompose](https://github.com/armcha/remotecompose)。
- 内容浏览器拖拽编辑器Vanilla JS、Kotlin/Wasm 预览、`server/`JVM 转换器,`remote-creation` `alpha05`)、若干 `.json` 布局配置 + 生成的 `.rc` 二进制文档、`index.html` 实时预览。
- 用途:参考其 `.rc` 文档格式与 JSON→二进制转换逻辑**切勿在此目录提交改动**。
## 4. remotecompose-android只读禁止修改
- 上游:[armcha/remotecompose-android](https://github.com/armcha/remotecompose-android)。
- 内容:纯播放端示例 App从 GitHub 下载 `.rc` 文件用 `RemoteComposePlayer` 渲染4 个屏幕 + Compose Navigation
- 用途:播放端架构/组件封装(如 `RemoteDocumentView``RemoteConfigFetcher`)的参考实现;**切勿修改**。
## 版本与依赖关键约束
| 模块 | Remote Compose artifact | 版本 |
|---|---|---|
| ComposeRemoteServer | `remote-creation-jvm` | `1.0.0-alpha17` |
| ComposeRemoteAndroid | `remote-player-*` | `1.0.0-alpha17` |
| remotecompose/server只读 | `remote-creation*` | `1.0.0-alpha05` |
- 创建端与播放端必须使用**同一 alpha 版本**`alpha17`)以保证二进制兼容;升级时两端同步升级。
- 所有 Remote Compose 依赖均来自 Google Maven`maven.google.com`)。
## 通用协作约定
- 提交前在各自子项目目录内构建通过Android: `:app:assembleDebug`Server: `./mvnw compile`)。
- 不要触碰 `remotecompose/``remotecompose-android/`
- 新增「服务端驱动 UI」能力时后端在 `RemoteUIController` 生成文档,前端在 `RemoteUIScreen` 消费,保持二进制协议一致。

View File

@@ -13,7 +13,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.ttstd.composeremote" applicationId "com.ttstd.composeremote"
minSdk 24 minSdk 29
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
@@ -52,4 +52,12 @@ dependencies {
androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.junit
debugImplementation libs.androidx.compose.ui.test.manifest debugImplementation libs.androidx.compose.ui.test.manifest
debugImplementation libs.androidx.compose.ui.tooling debugImplementation libs.androidx.compose.ui.tooling
// Remote Compose Playerview 版,参考 remotecompose-android 的集成方式)
implementation("androidx.compose.remote:remote-core:1.0.0-alpha17")
implementation("androidx.compose.remote:remote-player-core:1.0.0-alpha17")
implementation("androidx.compose.remote:remote-player-view:1.0.0-alpha17")
// OkHttp 用于请求二进制 payload
implementation("com.squareup.okhttp3:okhttp:4.12.0")
} }

View File

@@ -2,12 +2,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.ComposeRemoteAndroid"> android:theme="@style/Theme.ComposeRemoteAndroid">

View File

@@ -4,13 +4,23 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp
import com.ttstd.composeremote.ui.RemoteUIScreen
import com.ttstd.composeremote.ui.theme.ComposeRemoteAndroidTheme import com.ttstd.composeremote.ui.theme.ComposeRemoteAndroidTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -19,29 +29,31 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
ComposeRemoteAndroidTheme { ComposeRemoteAndroidTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> // 页面 id 列表,每个 id 对应后端一个独立接口 /api/remote-ui/{page}
Greeting( var page by remember { mutableStateOf("home") }
name = "Android", Column(modifier = Modifier.fillMaxSize()) {
modifier = Modifier.padding(innerPadding) Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = page == "home",
onClick = { page = "home" },
label = { Text("首页") }
)
FilterChip(
selected = page == "detail",
onClick = { page = "detail" },
label = { Text("详情") }
) )
} }
Box(modifier = Modifier.fillMaxSize().weight(1f)) {
RemoteUIScreen(page = page)
} }
} }
} }
} }
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
ComposeRemoteAndroidTheme {
Greeting("Android")
} }
} }

View File

@@ -0,0 +1,11 @@
package com.ttstd.composeremote
/**
* 全局配置。
*
* 注意:服务端地址建议通过 BuildConfig / local.properties 注入,避免硬编码内网 IP。
* 这里集中管理,方便后续替换为可配置项。
*/
object RemoteConfig {
const val SERVER_BASE_URL = "http://192.168.1.5:8000"
}

View File

@@ -0,0 +1,145 @@
package com.ttstd.composeremote.ui
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.remote.player.view.RemoteComposePlayer
import androidx.compose.remote.player.core.RemoteDocument
import androidx.compose.ui.unit.dp
import com.ttstd.composeremote.RemoteConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.util.concurrent.TimeUnit
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
private sealed interface RemoteUiState {
data object Loading : RemoteUiState
data class Success(val bytes: ByteArray) : RemoteUiState {
override fun equals(other: Any?): Boolean =
other is Success && bytes.contentEquals(other.bytes)
override fun hashCode(): Int = bytes.contentHashCode()
}
data class Error(val message: String) : RemoteUiState
}
private suspend fun fetchRemoteUi(page: String, baseUrl: String): ByteArray =
withContext(Dispatchers.IO) {
val url = "$baseUrl/api/remote-ui/$page"
val request = Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("HTTP ${response.code}")
}
response.body?.bytes() ?: throw IOException("空响应")
}
}
/**
* 使用 [RemoteComposePlayer] 渲染 Remote Compose 文档。
*
* 在 alpha17 版本中,建议显式创建 [RemoteDocument] 并使用 [RemoteComposePlayer.setDocument]
* 的 RemoteDocument 重载版本。同时将 setDocument 放在 AndroidView 的 update 块中,
* 以确保在数据变化时能正确触发 View 的更新。
*/
@SuppressLint("RestrictedApiAndroidX")
@Composable
private fun RemoteDocumentView(
bytes: ByteArray,
modifier: Modifier = Modifier,
contentKey: Any = bytes.contentHashCode(),
) {
key(contentKey) {
AndroidView(
factory = { ctx ->
Log.d("RemoteUIScreen", "Creating RemoteComposePlayer")
RemoteComposePlayer(ctx)
},
update = { view ->
try {
// 显式解析 ByteArray 为 RemoteDocument 对象
val document = RemoteDocument(bytes)
view.setDocument(document)
Log.d("RemoteUIScreen", "RemoteDocument set successfully")
} catch (e: Exception) {
Log.e("RemoteUIScreen", "Failed to set RemoteDocument", e)
}
},
modifier = modifier
)
}
}
@Composable
fun RemoteUIScreen(page: String, baseUrl: String = RemoteConfig.SERVER_BASE_URL) {
var state by remember(page) { mutableStateOf<RemoteUiState>(RemoteUiState.Loading) }
var retryToken by remember { mutableIntStateOf(0) }
LaunchedEffect(page, retryToken) {
state = RemoteUiState.Loading
state = try {
val bytes = fetchRemoteUi(page, baseUrl)
RemoteUiState.Success(bytes)
} catch (e: Exception) {
Log.e("RemoteUIScreen", "Fetch failed", e)
RemoteUiState.Error(e.message ?: "加载失败")
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (val s = state) {
is RemoteUiState.Loading -> CircularProgressIndicator()
is RemoteUiState.Error -> ErrorView(s.message) { retryToken++ }
is RemoteUiState.Success -> {
RemoteDocumentView(
bytes = s.bytes,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
private fun ErrorView(message: String, onRetry: () -> Unit) {
Box(
modifier = Modifier.fillMaxSize().padding(24.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(text = "加载失败", style = MaterialTheme.typography.titleMedium)
Text(text = message, style = MaterialTheme.typography.bodyMedium)
ElevatedButton(onClick = onRetry) { Text("重试") }
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

View File

@@ -1,12 +1,12 @@
[versions] [versions]
agp = "9.2.1" agp = "9.2.1"
coreKtx = "1.18.0" coreKtx = "1.18.0"
junit = "5.0-SNAPSHOT" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.13.0" activityCompose = "1.13.0"
kotlin = "2.2.10" kotlin = "2.4.0"
composeBom = "2026.02.01" composeBom = "2026.02.01"
[libraries] [libraries]

View File

@@ -28,6 +28,7 @@
</scm> </scm>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
<kotlin.version>2.1.0</kotlin.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -40,6 +41,11 @@
<artifactId>spring-boot-starter-webmvc</artifactId> <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@@ -49,17 +55,78 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>annotationProcessor</scope> <scope>provided</scope>
</dependency>
<!-- Remote Compose服务端生成二进制文档 -->
<dependency>
<groupId>androidx.compose.remote</groupId>
<artifactId>remote-creation-jvm</artifactId>
<version>1.0.0-alpha17</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
<repositories>
<repository>
<id>google</id>
<url>https://maven.google.com</url>
</repository>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>17</jvmTarget>
<args>
<arg>-Xjsr305=strict</arg>
</args>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -0,0 +1,33 @@
package com.ttstd.composeremote.server.controller;
import com.ttstd.composeremote.server.RemoteDocumentBuilderKt;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 服务端驱动 UI 接口。
*
* <p>一个页面一个接口:每个页面通过独立的路径暴露,例如
* <ul>
* <li>GET /api/remote-ui/home → 首页</li>
* <li>GET /api/remote-ui/detail → 详情页</li>
* </ul>
* 返回 {@code application/octet-stream} 的 Remote Compose 二进制文档,
* 由 Android 端 {@code RemoteComposePlayer} 原生渲染。
*/
@RestController
@RequestMapping("/api/remote-ui")
public class RemoteUIController {
@GetMapping(
value = {"/{page}", ""},
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE
)
public byte[] getPage(@PathVariable(name = "page", required = false) String page) {
String target = (page == null || page.isBlank()) ? "home" : page;
return RemoteDocumentBuilderKt.buildRemoteDocument(target);
}
}

View File

@@ -0,0 +1,38 @@
package com.ttstd.composeremote.server
import androidx.compose.remote.core.CoreDocument
import androidx.compose.remote.creation.JvmRcPlatformServices
import androidx.compose.remote.creation.RemoteComposeWriter
import androidx.compose.remote.creation.dsl.RcColumnScope
import androidx.compose.remote.creation.dsl.RcProfile
import androidx.compose.remote.creation.dsl.RcSp
import androidx.compose.remote.creation.dsl.createRcBuffer
import androidx.compose.remote.creation.dsl.fillMaxSize
import androidx.compose.remote.creation.dsl.fillMaxWidth
import androidx.compose.remote.creation.dsl.padding
import androidx.compose.remote.creation.dsl.background
import androidx.compose.remote.creation.dsl.Modifier
import androidx.compose.remote.creation.profile.Profile
import androidx.compose.remote.creation.profile.RemoteComposeWriterFactory
fun buildRemoteDocument(page: String): ByteArray {
val writerFactory = RemoteComposeWriterFactory { _, _, _ ->
RemoteComposeWriter(1080, 1920, "remote-ui", JvmRcPlatformServices())
}
val profile = Profile(
CoreDocument.DOCUMENT_API_LEVEL,
CoreDocument.PROFILE,
JvmRcPlatformServices(),
writerFactory
)
val rcProfile = RcProfile(profile, false)
return createRcBuffer(rcProfile) {
Column(Modifier.fillMaxSize().background(0xFFFFFFFF.toInt()).padding(16f)) {
Text(
text = if (page == "home") "Home Page" else "Detail Page",
fontSize = RcSp(36f),
color = 0xFF0000FF.toInt() // Blue
)
}
}
}