本文是一份超详细的 RemoteCompose 使用指南,将带你从零开始,逐步掌握 RemoteCompose 的开发。我们将通过完整的示例项目,深入理解文档创建、播放器使用、交互处理等核心功能。
目录
I. 环境配置与依赖
1.1 项目要求
- Android Studio:Arctic Fox (2020.3.1) 或更高版本
- Kotlin:1.5.10 或更高版本
- 最低 SDK:API 21 (Android 5.0)
- 目标 SDK:API 33 或更高
- Gradle:7.0 或更高版本
1.2 添加依赖
在项目的 build.gradle.kts (Module 级别) 中添加以下依赖:
// build.gradle.kts (Module: app)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
compileSdk = 34
defaultConfig {
applicationId = "com.example.remotecompose"
minSdk = 21
targetSdk = 34
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.3"
}
}
dependencies {
// Compose BOM (统一版本管理)
val composeBom = platform("androidx.compose:compose-bom:2024.01.00")
implementation(composeBom)
// Compose 基础库
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.8.2")
// RemoteCompose 核心库
implementation("androidx.compose.remote:core:1.0.0-alpha01")
// RemoteCompose 发送端 (用于创建文档)
implementation("androidx.compose.remote:sender:1.0.0-alpha01")
// RemoteCompose 播放器 (Compose 后端)
implementation("androidx.compose.remote:player-compose:1.0.0-alpha01")
// RemoteCompose 播放器 (View 后端,可选)
// implementation("androidx.compose.remote:player-view:1.0.0-alpha01")
// Layout 模块 (可选,用于组件树)
// implementation("androidx.compose.remote:layout:1.0.0-alpha01")
// 网络库 (用于从服务器获取文档)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// 协程
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
}
⚠️ 版本说明
RemoteCompose 目前处于 Alpha 阶段,版本号可能会频繁变化。请访问 AndroidX Release Notes 获取最新版本信息。
1.3 启用 Compose Compiler
确保在 build.gradle.kts (Project 级别) 中配置了 Compose Compiler:
// build.gradle.kts (Project level)
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
}
}
II. 核心概念理解
2.1 RemoteCompose 工作流程
RemoteCompose 完整工作流程
1. 文档创建 (Sender 端)
┌─────────────────────────────────────────────────────┐
│ @Composable 函数 │
│ ↓ │
│ RemoteComposeSender │
│ ↓ │
│ 捕获 Canvas 操作 │
│ ↓ │
│ 生成二进制文档 (WireBuffer) │
│ ↓ │
│ 序列化并传输 │
└─────────────────────────────────────────────────────┘
2. 文档传输
┌─────────────────────────────────────────────────────┐
│ 二进制文档 │
│ (可通过 HTTP, WebSocket, 本地文件等传输) │
└─────────────────────────────────────────────────────┘
3. 文档播放 (Player 端)
┌─────────────────────────────────────────────────────┐
│ 接收二进制文档 │
│ ↓ │
│ RemoteComposePlayer │
│ ↓ │
│ 解析 WireBuffer │
│ ↓ │
│ 在 Canvas 上执行绘制操作 │
│ ↓ │
│ 渲染到屏幕 │
└─────────────────────────────────────────────────────┘
2.2 核心 API 概览
// 1. RemoteComposeSender - 文档创建器
class RemoteComposeSender {
fun createDocument(
width: Float,
height: Float,
content: @Composable () -> Unit
): RemoteComposeDocument
fun encode(document: RemoteComposeDocument): ByteArray
}
// 2. RemoteComposePlayer - 文档播放器
class RemoteComposePlayer {
fun decode(bytes: ByteArray): RemoteComposeDocument
@Composable
fun Play(
document: RemoteComposeDocument,
modifier: Modifier = Modifier,
onSemanticAction: (SemanticAction) -> Unit = {}
)
}
III. 创建第一个文档
3.1 最简单的示例
让我们从最简单的例子开始:创建一个包含文本的文档。
import androidx.compose.remote.sender.RemoteComposeSender
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
class DocumentCreator {
private val sender = RemoteComposeSender()
fun createSimpleDocument(): ByteArray {
// 1. 创建文档
val document = sender.createDocument(
width = 360f, // 文档宽度 (dp)
height = 640f // 文档高度 (dp)
) {
// 2. 定义 UI 内容
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Hello, RemoteCompose!",
style = MaterialTheme.typography.headlineLarge
)
}
}
// 3. 编码为二进制
return sender.encode(document)
}
}
3.2 添加按钮和交互
fun createInteractiveDocument(): ByteArray {
val document = sender.createDocument(
width = 360f,
height = 640f
) {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { count++ },
modifier = Modifier.semantics {
// 添加语义动作,用于客户端处理
semanticAction(
action = SemanticAction.SendEvent(
id = "button_click",
eventName = "increment",
payload = mapOf("current_count" to count)
)
)
}
) {
Text("Increment")
}
}
}
return sender.encode(document)
}
3.3 使用 Modifier 自定义样式
fun createStyledDocument(): ByteArray {
val document = sender.createDocument(
width = 360f,
height = 640f
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6200EE),
Color(0xFF03DAC5)
)
)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(Alignment.Center),
elevation = CardDefaults.cardElevation(8.dp)
) {
Column(
modifier = Modifier.padding(24.dp)
) {
Text(
text = "Styled Card",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "This is a beautifully styled card with gradient background.",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
return sender.encode(document)
}
IV. 播放器使用
4.1 基础播放
import androidx.compose.remote.player.RemoteComposePlayer
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
class DocumentViewModel : ViewModel() {
private val player = RemoteComposePlayer()
private val _document = MutableStateFlow<RemoteComposeDocument?>(null)
val document: StateFlow<RemoteComposeDocument?> = _document
fun loadDocument(bytes: ByteArray) {
try {
val doc = player.decode(bytes)
_document.value = doc
} catch (e: Exception) {
// 处理解码错误
Log.e("DocumentViewModel", "Failed to decode document", e)
}
}
}
@Composable
fun RemoteComposeScreen(
viewModel: DocumentViewModel = viewModel()
) {
val document by viewModel.document.collectAsState()
val player = remember { RemoteComposePlayer() }
Scaffold { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
document?.let { doc ->
// 播放文档
player.Play(
document = doc,
modifier = Modifier.fillMaxSize()
)
} ?: CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
4.2 从网络加载文档
interface RemoteComposeApi {
@GET("api/document/{id}")
suspend fun getDocument(@Path("id") id: String): ResponseBody
}
class DocumentRepository(
private val api: RemoteComposeApi
) {
suspend fun fetchDocument(id: String): Result<ByteArray> {
return try {
val response = api.getDocument(id)
if (response.isSuccessful) {
Result.success(response.bytes()!!)
} else {
Result.failure(Exception("Failed to fetch document"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
class DocumentViewModel(
private val repository: DocumentRepository
) : ViewModel() {
private val player = RemoteComposePlayer()
private val _uiState = MutableStateFlow<DocumentUiState>(
DocumentUiState.Loading()
)
val uiState: StateFlow<DocumentUiState> = _uiState
fun loadDocument(id: String) {
viewModelScope.launch {
_uiState.value = DocumentUiState.Loading()
when (val result = repository.fetchDocument(id)) {
is Result.Success -> {
try {
val doc = player.decode(result.getOrNull()!!)
_uiState.value = DocumentUiState.Success(doc)
} catch (e: Exception) {
_uiState.value = DocumentUiState.Error(e.message ?: "Unknown error")
}
}
is Result.Failure -> {
_uiState.value = DocumentUiState.Error(
result.exceptionOrNull()?.message ?: "Unknown error"
)
}
}
}
}
}
sealed class DocumentUiState {
data class Loading(val message: String = "Loading...") : DocumentUiState()
data class Success(val document: RemoteComposeDocument) : DocumentUiState()
data class Error(val message: String) : DocumentUiState()
}
V. 处理交互事件
5.1 语义动作处理
@Composable
fun RemoteComposeScreen(
viewModel: DocumentViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val player = remember { RemoteComposePlayer() }
Scaffold { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
when (val state = uiState) {
is DocumentUiState.Loading -> {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(text = state.message)
}
}
is DocumentUiState.Success -> {
player.Play(
document = state.document,
modifier = Modifier.fillMaxSize(),
onSemanticAction = { action ->
// 处理语义动作
handleSemanticAction(action, viewModel)
}
)
}
is DocumentUiState.Error -> {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Error: ${state.message}",
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadDocument("default") }) {
Text("Retry")
}
}
}
}
}
}
}
fun handleSemanticAction(
action: SemanticAction,
viewModel: DocumentViewModel
) {
when (action) {
is SemanticAction.Navigate -> {
// 处理导航
Log.d("RemoteCompose", "Navigate to: ${action.route}")
// 使用 Navigation Compose 导航
}
is SemanticAction.OpenUrl -> {
// 打开 URL
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url))
context.startActivity(intent)
}
is SemanticAction.SendEvent -> {
// 发送事件到服务器
viewModel.sendEvent(action.eventName, action.payload)
}
is SemanticAction.LocalAction -> {
// 执行本地动作
when (action.actionType) {
LocalActionType.SHARE -> {
// 分享
}
LocalActionType.COPY_TO_CLIPBOARD -> {
// 复制到剪贴板
}
// ... 其他动作
}
}
is SemanticAction.Composite -> {
// 执行复合动作
action.actions.forEach {
handleSemanticAction(it, viewModel)
}
}
}
}
5.2 事件回传到服务器
interface RemoteComposeApi {
@POST("api/event")
suspend fun sendEvent(
@Body event: EventRequest
): EventResponse
}
data class EventRequest(
val documentId: String,
val eventName: String,
val payload: Map<String, Any>
)
data class EventResponse(
val success: Boolean,
val newDocumentId: String?, // 可选:返回新文档
val message: String?
)
class DocumentViewModel(
private val repository: DocumentRepository
) : ViewModel() {
private var currentDocumentId: String? = null
fun sendEvent(eventName: String, payload: Map<String, Any>) {
viewModelScope.launch {
val documentId = currentDocumentId ?: return@launch
val result = repository.sendEvent(
EventRequest(documentId, eventName, payload)
)
if (result.is Result.Success) {
val response = result.getOrNull()!!
if (response.success) {
// 如果服务器返回了新文档,加载它
response.newDocumentId?.let { newId ->
loadDocument(newId)
}
}
}
}
}
}
VI. 完整示例项目
6.1 项目结构
项目结构
app/
├── src/main/java/com/example/remotecompose/
│ ├── MainActivity.kt
│ ├── ui/
│ │ ├── RemoteComposeScreen.kt
│ │ └── DocumentViewModel.kt
│ ├── data/
│ │ ├── RemoteComposeApi.kt
│ │ ├── DocumentRepository.kt
│ │ └── models/
│ │ ├── EventRequest.kt
│ │ └── EventResponse.kt
│ └── di/
│ └── AppModule.kt
6.2 MainActivity
package com.example.remotecompose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.remotecompose.ui.RemoteComposeScreen
import com.example.remotecompose.ui.theme.RemoteComposeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RemoteComposeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RemoteComposeScreen()
}
}
}
}
}
6.3 依赖注入 (Hilt)
package com.example.remotecompose.di
import com.example.remotecompose.data.DocumentRepository
import com.example.remotecompose.data.RemoteComposeApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://your-api.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApi(retrofit: Retrofit): RemoteComposeApi {
return retrofit.create(RemoteComposeApi::class.java)
}
@Provides
@Singleton
fun provideRepository(api: RemoteComposeApi): DocumentRepository {
return DocumentRepository(api)
}
}
VII. 最佳实践
7.1 文档缓存
class DocumentCache(
private val context: Context
) {
private val cacheDir = File(context.cacheDir, "remote_compose")
private val memoryCache = LruCache<String, ByteArray>(50)
init {
cacheDir.mkdirs()
}
suspend fun get(id: String): ByteArray? {
// 1. 检查内存缓存
memoryCache.get(id)?.let { return it }
// 2. 检查磁盘缓存
val file = File(cacheDir, "$id.bin")
if (file.exists()) {
val bytes = withContext(Dispatchers.IO) {
file.readBytes()
}
memoryCache.put(id, bytes)
return bytes
}
return null
}
suspend fun put(id: String, bytes: ByteArray) {
// 1. 存入内存缓存
memoryCache.put(id, bytes)
// 2. 存入磁盘缓存
withContext(Dispatchers.IO) {
val file = File(cacheDir, "$id.bin")
file.writeBytes(bytes)
}
}
fun clear() {
memoryCache.evictAll()
cacheDir.listFiles()?.forEach { it.delete() }
}
}
7.2 错误处理
sealed class DocumentError : Exception() {
data class NetworkError(val message: String) : DocumentError()
data class DecodeError(val message: String) : DocumentError()
data class VersionMismatch(val expected: Int, val actual: Int) : DocumentError()
data class InvalidFormat(val message: String) : DocumentError()
}
fun DocumentError.getUserMessage(): String {
return when (this) {
is NetworkError -> "网络连接失败,请检查网络设置"
is DecodeError -> "文档解析失败,请重试"
is VersionMismatch -> "文档版本不兼容,请更新应用"
is InvalidFormat -> "文档格式错误"
}
}
7.3 性能优化
✅ 性能优化建议
- 文档大小:尽量控制文档大小,避免包含过多图片
- 懒加载:对于长列表,使用分页加载
- 缓存策略:合理使用内存和磁盘缓存
- 增量更新:只传输变化的操作,而非整个文档
- 预加载:在用户可能访问前预加载文档
VIII. 常见问题
8.1 文档无法解码
❌ 问题:文档解码失败
可能原因:
- 文档版本不兼容
- 文档格式损坏
- 编码过程中出错
解决方案:
- 检查文档版本号
- 验证文档完整性(使用校验和)
- 确保使用相同版本的 RemoteCompose 库
8.2 交互事件不响应
⚠️ 问题:点击无响应
检查清单:
- 确认文档中定义了语义动作
- 检查
onSemanticAction回调是否正确设置 - 验证点击区域是否在文档范围内
- 检查是否有其他 View 遮挡
8.3 文档渲染不完整
⚠️ 问题:UI 显示不完整
可能原因:
- 文档尺寸与屏幕不匹配
- 某些操作未正确捕获
- 资源(图片、字体)缺失
解决方案:
- 使用响应式尺寸(
fillMaxSize) - 检查资源是否完整嵌入文档
- 使用布局约束而非固定尺寸
IX. 总结
通过本文的详细讲解,你应该已经掌握了 RemoteCompose 的完整使用方法。从环境配置到实际项目集成,从基础播放到交互处理,RemoteCompose 为服务器驱动 UI 提供了一个强大而灵活的解决方案。
记住以下关键点:
- 使用
RemoteComposeSender创建文档 - 使用
RemoteComposePlayer播放文档 - 通过
SemanticAction处理交互 - 实现合理的缓存和错误处理
- 遵循最佳实践优化性能
现在,开始构建你的第一个 RemoteCompose 应用吧!🚀