FitTrack Pro — @expo/ui Migration

De librerías comunitarias a UI nativa real: SwiftUI · Jetpack Compose · Expo SDK 56

@expo/ui SDK 56

Árbol de decisión — ¿qué capa usar?

1
Empieza siempre por Universal — importa desde @expo/ui. Una sola implementación funciona en iOS, Android y Web sin splits de plataforma.
2
Si la capa universal no cubre el componente/modificador que necesitas → baja a @expo/ui/swift-ui (iOS, archivo .ios.tsx) o @expo/ui/jetpack-compose (Android, archivo .android.tsx).
3
Migrando una librería comunitaria existente (ej. @gorhom/bottom-sheet) → usa los drop-in replacements de @expo/ui/community/<nombre>. Mismo API, distinto import.
9:41 ●●● ▲ 100%
FitTrack Pro
Lunes, 16 jun 2026
Hoy
Semana
Mes
Año
👟
8.342
Pasos
🔥
487
kcal
📍
5,8km
Dist.
ACTIVIDADES
🏃
Running matutino
35 min · 5,2 km
Hecho
🏋️
Fuerza + core
45 min · Gym
Pendiente
🧘
Yoga noche
20 min · Casa
Pendiente
El dashboard usa componentes universales de @expo/ui: List + ListItem para las actividades, y el SegmentedControl drop-in para el filtro Hoy/Semana/Mes/Año — un solo árbol para iOS y Android.
ℹ️
Cada árbol @expo/ui debe estar envuelto en <Host>. Para tamaño intrínseco usa matchContents; para pantalla completa usa style={{ flex: 1 }}.
⚠️
List / ListItem no son aptos para listas grandes — cada item se procesa en el JS thread. Para datasets grandes, usa LazyColumn de Jetpack Compose (Android) o List de SwiftUI dentro del árbol platform-specific.
🌐
Universal — Dashboard + Filtro (iOS & Android)
Un solo archivo, sin splits de plataforma
Componentes usados
Host, Column, Row, List, ListItem, Text, Switch, Slider
Drop-ins usados
SegmentedControl@react-native-segmented-control/segmented-control
screens/Dashboard.tsx
UNIVERSAL
import { Host, Column, Row, List, ListItem, Text, Switch } from '@expo/ui'; import SegmentedControl from '@expo/ui/community/segmented-control'; import { useState } from 'react'; import { Activity } from '../types'; const Dashboard = ({ activities }: { activities: Activity[] }) => { const [period, setPeriod] = useState(0); return ( {/* Host es OBLIGATORIO como raíz de todo árbol @expo/ui */} <Host style={{ flex: 1 }}> <Column> {/* Filtro de periodo — drop-in replacement, mismo API que el paquete original */} <SegmentedControl values={['Hoy', 'Semana', 'Mes', 'Año']} selectedIndex={period} onChange={(e) => setPeriod(e.nativeEvent.selectedSegmentIndex)} /> {/* Métricas */} <Row> <MetricCard icon="👟" value="8.342" label="Pasos" /> <MetricCard icon="🔥" value="487" label="kcal" /> <MetricCard icon="📍" value="5,8km" label="Dist." /> </Row> {/* Lista de actividades — OK para < ~50 items */} <List> {activities.map((act) => ( <ListItem key={act.id} title={act.name} subtitle={`${act.duration} min · ${act.location}`} trailing={() => <Switch value={act.completed} onValueChange={(v) => toggleActivity(act.id, v)} /> } /> ))} </List> </Column> </Host> ); };
🔄
Drop-in Replacements — Migración FitTrack Pro
Mismo API, nuevo import — cero cambios en props
Librería actual (eliminar) Nuevo import @expo/ui Cambios requeridos
@gorhom/bottom-sheet @expo/ui/community/bottom-sheet Solo el import. Mismo API de props.
@react-native-community/datetimepicker @expo/ui/community/datetime-picker Solo el import. Verificar prop mode.
@react-native-segmented-control/segmented-control @expo/ui/community/segmented-control Solo el import. Mismo API.
components/NewActivitySheet.tsx
DROP-IN REPLACEMENTS
// ANTES — librerías comunitarias: // import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; // import DateTimePicker from '@react-native-community/datetimepicker'; // AHORA — @expo/ui drop-in replacements (mismo API): import BottomSheet, { BottomSheetView } from '@expo/ui/community/bottom-sheet'; import DateTimePicker from '@expo/ui/community/datetime-picker'; import { Host, Column, Row, Text, Button, Slider } from '@expo/ui'; import { useRef, useState } from 'react'; export const NewActivitySheet = ({ onSave }) => { const sheetRef = useRef(null); const [date, setDate] = useState(new Date()); const [duration, setDuration] = useState(30); return ( {/* BottomSheet — API idéntico a @gorhom/bottom-sheet */} <BottomSheet ref={sheetRef} snapPoints={['50%', '80%']}> <BottomSheetView> {/* Árbol universal @expo/ui dentro del sheet */} <Host matchContents> <Column> <Text>Nueva actividad</Text> {/* Slider de duración (universal) */} <Slider value={duration} minimumValue={5} maximumValue={180} step={5} onValueChange={setDuration} /> <Text>{duration} minutos</Text> </Column> </Host> {/* DateTimePicker — API idéntico a @react-native-community/datetimepicker */} <DateTimePicker value={date} mode="date" onChange={(_e, d) => d && setDate(d)} /> </BottomSheetView> </BottomSheet> ); };
🍎
iOS Platform-Specific — Input con worklet (SwiftUI)
Solo cuando la capa universal no es suficiente · archivo .ios.tsx
ℹ️
En FitTrack Pro, iOS necesita formatear el nombre de la actividad en tiempo real (mayúsculas automáticas, sin espera de re-render). La capa universal de TextInput usa useNativeState, pero para la lógica custom de transformación necesitamos bajarlo al árbol SwiftUI.
components/ActivityNameInput.ios.tsx
SwiftUI · iOS only
import { Host, TextInput, useNativeState } from '@expo/ui'; // react-native-worklets requerido para sincronía en el UI thread import { useCallback } from 'react'; export const ActivityNameInput = ({ onName }) => { // ObservableState: se actualiza en el UI thread sin React re-render const nameState = useNativeState(''); const handleChange = useCallback((raw: string) => { 'worklet'; // corre en el UI thread — sin flicker // Capitaliza primera letra de cada palabra (title case) const formatted = raw .split(' ') .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(' '); nameState.value = formatted; // escribe directo — sin re-render onName(formatted); }, [nameState]); return ( <Host matchContents> <TextInput value={nameState} {/* ObservableState, no string */} onChangeText={handleChange} placeholder="Nombre de la actividad" /> </Host> ); }; // Android fallback → components/ActivityNameInput.android.tsx // usa TextInput estándar de React Native (sin worklet necesario)
🤖
Android Platform-Specific — LazyColumn (Jetpack Compose)
Historial largo de actividades · archivo .android.tsx
⚠️
List universal no es apto para listas largas. El historial de FitTrack Pro puede tener cientos de actividades — en Android usamos LazyColumn de Jetpack Compose con Host style={{ flex: 1 }} (tamaño explícito obligatorio para LazyColumn).
components/ActivityHistory.android.tsx
Jetpack Compose · Android only
import { Host, LazyColumn, Text, Row } from '@expo/ui/jetpack-compose'; import { paddingAll, fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers'; import { Activity } from '../types'; export const ActivityHistory = ({ activities }: { activities: Activity[] }) => ( {/* flex: 1 OBLIGATORIO cuando LazyColumn es hijo directo de Host */} <Host style={{ flex: 1 }}> <LazyColumn verticalArrangement={{ spacedBy: 8 }} modifiers={[paddingAll(16)]} > {activities.map((act) => ( <Row key={act.id} modifiers={[fillMaxWidth(), paddingAll(12)]} horizontalArrangement={{ spacedBy: 12 }} > <Text style={{ typography: 'titleMedium' }}>{act.name}</Text> <Text style={{ typography: 'bodySmall' }}> {act.duration} min · {act.date} </Text> </Row> ))} </LazyColumn> </Host> );
📋
Resumen de la migración FitTrack Pro
Qué capa se usa en cada pantalla
Pantalla / Componente Capa @expo/ui Archivo
Dashboard — métricas y lista actividades Universal Dashboard.tsx
Filtro Hoy / Semana / Mes / Año Drop-in (SegmentedControl) Dashboard.tsx
Panel nueva actividad (BottomSheet) Drop-in (BottomSheet) NewActivitySheet.tsx
Selector de fecha en el panel Drop-in (DateTimePicker) NewActivitySheet.tsx
Input nombre actividad — title case sin flicker SwiftUI (useNativeState) ActivityNameInput.ios.tsx
Historial largo de actividades (Android) Jetpack Compose (LazyColumn) ActivityHistory.android.tsx