Algo silenciosamente significativo llegó a Jetpack a finales de 2025. androidx.compose.remote — Remote Compose — es la respuesta oficial de Google a la UI server-driven en Android. Alcanzó la versión alpha07 el 25 de marzo de 2026, y vale la pena entender qué es y, más importante, qué problema resuelve en realidad.
El problema que nunca se resolvió del todo
La UI Server-Driven (SDUI) ha sido un patrón conocido en Android durante años. Airbnb, Twitter, Lyft y otros construyeron sus propios stacks de SDUI. El enfoque común: definir un esquema JSON para componentes, enviar un renderer que mapea claves JSON a UI real, y actualizar la interfaz cambiando el payload del servidor.
Funciona. Pero tiene desventajas reales:
- JSON es verboso y sin tipos — construyes tu propio contrato y esperas que ambos lados coincidan
- Los renderers divergen — el renderer del cliente y la definición del servidor se desincronizaron con el tiempo
- Sin seguridad en compilación — un typo en el nombre de un componente simplemente no renderiza nada
Mientras tanto, la solución nativa de Android era RemoteViews — un formato de UI serializable y cross-process que existe desde Android 1.5. Los widgets de Glance, los tiles de Wear OS, los widgets de la pantalla de bloqueo — todos usan RemoteViews hoy. Es confiable pero muy limitado: sin layouts personalizados, sin composables arbitrarios, sin Compose en absoluto.
Remote Compose es el sucesor de ambos.
Qué es Remote Compose en realidad
Remote Compose es una capa de serialización sobre la semántica de Jetpack Compose. En lugar de renderizar en un Canvas, escribes código similar a Compose usando la API remote-creation, y esta produce un documento binario compacto — un árbol de UI serializado. Ese documento se transmite (por red, IPC, o como quieras) a un cliente Android que lo renderiza usando el runtime remote-player.
La idea clave: el cliente no necesita un compilador de Compose ni un runtime de Kotlin para el código de UI. El player simplemente deserializa el documento y lo pinta. Sin actualización de APK.
Servidor / Proceso JVM
└── API remote-creation → escribe código similar a Compose
↓ produce
[ documento binario ]
↓ transmite
Cliente Android
└── remote-player → deserializa + renderiza en View/host Compose
Cómo difiere del SDUI basado en JSON
| SDUI con JSON | Remote Compose | |
|---|---|---|
| Formato | Texto (verboso) | Binario (compacto) |
| Seguridad de tipos | Ninguna (runtime) | Aplicada en tiempo de creación |
| Renderer | Personalizado, diverge | Provisto por el sistema, versionado |
| Poder de layout | Limitado por tu esquema | Modelo de layout completo de Compose |
| Oficial (Google) | No | Sí (Jetpack) |
La conexión con Glance
Si has usado Glance (la librería Jetpack para widgets y tiles de Wear OS), estás usando RemoteViews bajo el capó — la misma API de 2009. Remote Compose es el futuro backend de renderizado de Glance. La migración ya está en curso.
Este contexto importa: Remote Compose no es una curiosidad experimental. Es la infraestructura que Google está construyendo para la próxima década de widgets e UIs cross-process.
El encuadre “Volver al Futuro” que usó Arman Chatikyan es acertado: mismo problema que RemoteViews en 2009, solución moderna.
Estructura de módulos
La librería está dividida en creación (lado servidor/JVM) y player (cliente Android):
// Servidor / JVM — produce documentos
implementation("androidx.compose.remote:remote-creation:1.0.0-alpha07")
implementation("androidx.compose.remote:remote-creation-compose:1.0.0-alpha07")
// Cliente Android — renderiza documentos
implementation("androidx.compose.remote:remote-player-core:1.0.0-alpha07")
implementation("androidx.compose.remote:remote-player-view:1.0.0-alpha07")
// Tooling
debugImplementation("androidx.compose.remote:remote-tooling-preview:1.0.0-alpha07")
La separación es intencional: el lado de creación puede ejecutarse en un backend JVM (un servicio Spring, una Cloud Function, etc.) sin ninguna dependencia de Android. El lado del player es exclusivo de Android.
La barrera del RemoteApplier
Una decisión de diseño que destaca: desde alpha04, el RemoteApplier está habilitado por defecto. Es una barrera en tiempo de compilación que evita que composables regulares de Jetpack Compose se usen accidentalmente dentro de código de Remote Compose.
En la práctica, las funciones de Remote Compose se parecen a las funciones Compose regulares, pero operan en un scope de composición diferente. El compilador aplica la barrera. Si intentas llamar a un @Composable regular desde dentro de una función de Remote Compose, obtienes un error en compilación.
Es la decisión correcta — previene una clase de bugs donde UI no serializable se filtra a un documento que el player no puede renderizar.
Qué hay en alpha07 (25 de marzo de 2026)
La última versión agregó:
- Escalado de fuente no lineal — respeta la configuración de accesibilidad para texto grande
- LayoutDirection — soporte RTL/LTR para layouts internacionales
RemoteSpacer,RemoteImageVector,painterRemoteVector— APIs recién públicas- Funciones de modificador semántico — soporte de accesibilidad
RemoteArrangement.spacedBy()— espaciado basado en gapsRemoteDrawScope,RemoteCanvas,RemotePainter,RemoteBrushextendidos- Operaciones aritméticas en
RemoteFloaty conversiónasRemoteDp()
Cambios que rompen compatibilidad en alpha07:
RemoteArrangement.CenterHorizontallyeliminado — usarRemoteArrangement.Center- El parámetro de alineación de
RemoteBoxcambió a un únicoRemoteAlignment
Estado actual: todavía alpha, pero moviéndose rápido
La librería publicó 7 releases alpha en aproximadamente 3.5 meses (diciembre 2025 → marzo 2026). El ritmo es agresivo: cada release agrega nuevas APIs públicas y rompe las existentes.
Hitos significativos hasta ahora:
- alpha04:
minSdkbajó de 26 a 23 — señal de que el equipo busca adopción amplia - alpha04: Soporte de
FlowLayoutagregado - alpha06: Target Java 11 (puede requerir desugaring)
- alpha07: Escalado de fuente + RTL — requisitos básicos para apps internacionalizadas
¿Deberías usarlo ahora?
Para apps en producción: todavía no. La API cambia cada dos semanas.
Para desarrolladores de widgets con Glance: empieza a prestar atención. Hacia aquí va Glance, y la familiaridad temprana valdrá la pena cuando se formalice el camino de migración.
Para equipos de SDUI: vale la pena hacer un prototipo serio. Si tu equipo ha estado manteniendo un stack de SDUI personalizado, el atractivo de una alternativa oficial, con tipos, en formato binario, es obvio.
Para casos de uso de widgets y tiles: esta es la aplicación práctica más inmediata, ya que Glance ya está en este camino.
De aquí en adelante
En el recorrido de configuración que sigue, construimos un ejemplo práctico: crear un documento Remote Compose simple en el lado de creación, serializarlo, y renderizarlo dentro de una app Android usando remote-player-view.
Recursos
- Remote Compose — notas de lanzamiento oficiales
- Arman Chatikyan — Remote Compose: Volver al Futuro
- Luca Fioravanti — De RemoteViews a RemoteCompose
Primeros pasos con Remote Compose: un ejemplo práctico
Construye un documento Remote Compose en el lado de creación, serialízalo y renderízalo en una app Android con remote-player-view.
Leer por separado: Primeros pasos con Remote Compose: un ejemplo prácticoAhora que explicamos qué es Remote Compose y por qué existe, construimos algo real — un documento de UI simple, lo serializamos, y lo renderizamos en una app Android.
Advertencia alpha:
androidx.compose.remoteestá actualmente en1.0.0-alpha07. La superficie de la API sigue cambiando. El código de este post refleja el estado de alpha07.
Configuración del proyecto
Dependencias
Agrega los módulos de Remote Compose. Para una app que crea y reproduce documentos localmente (útil para pruebas o widgets), necesitas ambos lados:
// build.gradle.kts (módulo app)
dependencies {
// Lado de creación — genera el documento
implementation("androidx.compose.remote:remote-creation:1.0.0-alpha07")
implementation("androidx.compose.remote:remote-creation-compose:1.0.0-alpha07")
// Lado del player — renderiza el documento
implementation("androidx.compose.remote:remote-player-core:1.0.0-alpha07")
implementation("androidx.compose.remote:remote-player-view:1.0.0-alpha07")
// Preview durante desarrollo
debugImplementation("androidx.compose.remote:remote-tooling-preview:1.0.0-alpha07")
}
minSdk: Remote Compose requiere minSdk 23 (desde alpha04). Si tu proyecto apunta a un nivel menor, habilita core library desugaring — la librería usa APIs de Java 11 desde alpha06.
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools.desugar_jdk_libs:2.1.4")
}
El lado de creación: construyendo un documento
La API de creación espeja los composables de Compose pero opera en un scope de composición diferente — el RemoteApplier. Las funciones aquí no dibujan en un Canvas; construyen un árbol de UI serializable.
Piénsalo como escribir una plantilla que se congela y se envía al dispositivo.
import androidx.compose.remote.creation.RemoteCompose
import androidx.compose.remote.creation.compose.RemoteColumn
import androidx.compose.remote.creation.compose.RemoteText
import androidx.compose.remote.creation.compose.RemoteButton
import androidx.compose.remote.creation.compose.RemoteSpacer
import androidx.compose.remote.creation.compose.remoteModifier
import androidx.compose.remote.creation.compose.fillMaxWidth
import androidx.compose.remote.creation.compose.padding
fun buildWelcomeDocument(): ByteArray {
return RemoteCompose.capture {
RemoteColumn(
modifier = remoteModifier()
.fillMaxWidth()
.padding(24f)
) {
RemoteText(
text = "¡Hola desde el servidor!",
style = RemoteTextStyle.titleLarge()
)
RemoteSpacer(height = 12f)
RemoteText(
text = "Esta UI fue definida fuera de la app y renderizada localmente por el runtime del player.",
style = RemoteTextStyle.bodyMedium()
)
RemoteSpacer(height = 24f)
RemoteButton(
onClick = RemoteAction.log("boton_presionado"),
modifier = remoteModifier().fillMaxWidth()
) {
RemoteText(text = "Presióname")
}
}
}
}
RemoteCompose.capture { ... } ejecuta el bloque de creación y devuelve un ByteArray — el documento serializado. Puedes escribirlo en disco, enviarlo por HTTP, o pasarlo por IPC.
Restricción clave: No puedes llamar funciones
@Composableregulares dentro de un bloqueRemoteCompose.capture { }. ElRemoteApplierlo aplica en tiempo de compilación.
Simulando una respuesta del servidor
En una app real en producción, el documento vendría de un servicio backend. Para este ejemplo, lo simulamos localmente con una coroutine:
// En un ViewModel o repositorio
class DocumentRepository {
suspend fun fetchWelcomeDocument(): ByteArray {
// Simular latencia de red
delay(300)
// En producción: val response = httpClient.get("/ui/welcome")
// return response.body()
return buildWelcomeDocument()
}
}
El documento es solo bytes. Puedes cachearlo, guardarlo en una base de datos, o versionarlo como quieras en tu backend.
El lado del player: renderizando en un fragment
remote-player-view provee RemoteComposePlayerView — una View (no un Composable) que acepta un documento y lo renderiza. Puedes embeberlo en cualquier layout XML o en una UI Compose mediante AndroidView.
Opción A: usarlo en un layout XML
<!-- res/layout/fragment_remote.xml -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.remote.player.view.RemoteComposePlayerView
android:id="@+id/remote_player"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
class RemoteFragment : Fragment(R.layout.fragment_remote) {
private val viewModel: RemoteViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val player = view.findViewById<RemoteComposePlayerView>(R.id.remote_player)
val loading = view.findViewById<ProgressBar>(R.id.loading)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.documentState.collect { state ->
when (state) {
is DocumentState.Loading -> {
loading.isVisible = true
player.isVisible = false
}
is DocumentState.Success -> {
loading.isVisible = false
player.isVisible = true
player.setDocument(state.bytes)
}
is DocumentState.Error -> {
loading.isVisible = false
// Mostrar UI de error
}
}
}
}
}
}
}
Opción B: embeber en Compose con AndroidView
Si tu app es completamente Compose:
@Composable
fun RemoteScreen(documentBytes: ByteArray?) {
if (documentBytes == null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
return
}
AndroidView(
factory = { context ->
RemoteComposePlayerView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
},
update = { playerView ->
playerView.setDocument(documentBytes)
},
modifier = Modifier.fillMaxWidth()
)
}
Manejando acciones remotas
Los botones y elementos interactivos en el documento pueden emitir RemoteActions — eventos nombrados que la app anfitriona maneja. El documento declara qué emitir; el player los expone:
// Al construir el documento:
RemoteButton(
onClick = RemoteAction.named("navegar_a_detalle", mapOf("id" to "42"))
) { ... }
// En la app anfitriona, establece un listener de acciones en el player view:
player.setActionListener { action ->
when (action.name) {
"navegar_a_detalle" -> {
val id = action.params["id"]
findNavController().navigate(
RemoteFragmentDirections.toDetalle(id)
)
}
"boton_presionado" -> {
analytics.track("remote_boton_presionado")
}
}
}
Esto mantiene la lógica de negocio en la app donde pertenece, mientras el documento controla el layout y el contenido.
El ViewModel
sealed interface DocumentState {
data object Loading : DocumentState
data class Success(val bytes: ByteArray) : DocumentState
data class Error(val message: String) : DocumentState
}
class RemoteViewModel(
private val repository: DocumentRepository
) : ViewModel() {
private val _documentState = MutableStateFlow<DocumentState>(DocumentState.Loading)
val documentState: StateFlow<DocumentState> = _documentState.asStateFlow()
init {
loadDocument()
}
private fun loadDocument() {
viewModelScope.launch {
_documentState.value = DocumentState.Loading
runCatching { repository.fetchWelcomeDocument() }
.onSuccess { _documentState.value = DocumentState.Success(it) }
.onFailure { _documentState.value = DocumentState.Error(it.message ?: "Error desconocido") }
}
}
fun reintentar() = loadDocument()
}
Qué habilita esto
Con esta configuración, la UI que se renderiza en RemoteComposePlayerView puede actualizarse cambiando lo que devuelve el servidor — sin ninguna actualización de la app. El usuario ve un layout diferente, texto diferente, botón diferente, en su próximo inicio.
Casos de uso prácticos:
- Tests A/B de variantes de UI sin desplegar nuevos builds
- Feature flags que muestran u ocultan secciones de UI
- Widgets y tiles (así funcionará Glance una vez migrado)
- Flujos de onboarding dinámicos que el equipo de producto puede iterar de forma independiente
Lo que todavía no puede hacer
Al estar en alpha07, hay limitaciones reales:
- Sin composables arbitrarios — solo puedes usar lo que expone
remote-creation. Sin composables personalizados de tu codebase. - Sin scope de ViewModel — el documento es sin estado. Cualquier lógica con estado vive en la app anfitriona y se comunica vía
RemoteAction. - Sin navegación — el player renderiza un único documento; la navegación entre pantallas es responsabilidad de la app anfitriona.
- La API cambia frecuentemente — cada release alpha ha cambiado algo. No construyas infraestructura de producción sobre esto hasta llegar a beta.
Qué vigilar
La trayectoria es clara. A medida que la superficie de la API se estabilice y avance la migración de Glance, Remote Compose se convertirá en la forma estándar de construir widgets, componentes de pantalla de bloqueo, tiles de Wear OS, y UIs cross-process.
Presta atención a:
remote-creation-jvm— el módulo JVM del lado servidor que habilitará generación real de documentos desde el backend- El roadmap de migración de Glance a
remote-player - La transición alpha → beta, que señalará la estabilidad de la API