RemoteCompose 完整使用指南:从入门到实战

📅 2024-07-10 · ⏱ 45 min · 📚 实战教程

本文是一份超详细的 RemoteCompose 使用指南,将带你从零开始,逐步掌握 RemoteCompose 的开发。我们将通过完整的示例项目,深入理解文档创建、播放器使用、交互处理等核心功能。

目录

  1. 环境配置与依赖
  2. 核心概念理解
  3. 创建第一个文档
  4. 播放器使用
  5. 处理交互事件
  6. 完整示例项目
  7. 最佳实践
  8. 常见问题

I. 环境配置与依赖

1.1 项目要求

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 文档无法解码

❌ 问题:文档解码失败

可能原因:

解决方案:

8.2 交互事件不响应

⚠️ 问题:点击无响应

检查清单:

  1. 确认文档中定义了语义动作
  2. 检查 onSemanticAction 回调是否正确设置
  3. 验证点击区域是否在文档范围内
  4. 检查是否有其他 View 遮挡

8.3 文档渲染不完整

⚠️ 问题:UI 显示不完整

可能原因:

解决方案:

IX. 总结

通过本文的详细讲解,你应该已经掌握了 RemoteCompose 的完整使用方法。从环境配置到实际项目集成,从基础播放到交互处理,RemoteCompose 为服务器驱动 UI 提供了一个强大而灵活的解决方案。

记住以下关键点:

现在,开始构建你的第一个 RemoteCompose 应用吧!🚀