Cómo crear una ExploreCard específica para iOS y Android

Clase 18 de 23Curso de Fundamentos de React Native

Resumen

Potencia tu vista de explore con un componente reusable y un servicio de sugerencias. Aquí verás cómo crear una ExploreCard accesible, aplicar estilos específicos para iOS y Android con Platform y construir un SuggestService que usa un catálogo por categoría con prioridades.

¿Cómo crear una ExploreCard con props y accesibilidad?

Para darle forma al tag de explore se construye un componente con props claras: emoji, título, subtítulo y acción. La acción se ejecuta al presionar, y se expone a través de las props para mantener la lógica desacoplada. También se añade accesibilidad con role y label del botón.

¿Qué props y type definen la interfaz?

  • type con las props: emoji, title, subtitle, onPress.
  • Acción disparada en onPress desde las props.
  • Exportación del componente para reutilizarlo en el tag de explore.
// ExploreCard.tsx
import React from 'react';
import { View, Text, Pressable, Platform, StyleSheet } from 'react-native';

type ExploreCardProps = {
  emoji: string;
  title: string;
  subtitle: string;
  onPress: () => void;
};

export function ExploreCard({ emoji, title, subtitle, onPress }: ExploreCardProps) {
  return (
    <Pressable
      onPress={onPress}
      accessibilityRole="button"
      accessibilityLabel={title}
      style={[
        styles.base,
        Platform.OS === 'ios' ? styles.ios : styles.android,
        styles.common,
      ]}
    >
      <View style={styles.emojiBox}>
        <Text style={styles.emoji}>{emoji}</Text>
      </View>
      <View style={styles.textBox}>
        <Text style={styles.title}>{title}</Text>
        <Text style={styles.subtitle}>{subtitle}</Text>
      </View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  base: { flexDirection: 'row', alignItems: 'center', padding: 16, borderRadius: 12 },
  ios: { shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8, shadowOffset: { width: 0, height: 4 } },
  android: { elevation: 3 },
  common: { backgroundColor: '#fff' },
  emojiBox: { marginRight: 12 },
  emoji: { fontSize: 24 },
  textBox: { flex: 1 },
  title: { fontSize: 16, fontWeight: '600' },
  subtitle: { fontSize: 13, color: '#666' },
});

¿Cómo estructurar la vista con emoji, título y subtítulo?

  • Contenedor para el emoji con tamaño visible.
  • Bloque para textos: título claro y subtítulo descriptivo.
  • Botón presionable que envuelve todo para capturar la acción.

¿Cómo aplicar estilos específicos por sistema operativo con Platform?

La clave está en combinar una base común con estilos condicionales por plataforma usando Platform. Así, la card se ve distinta en iOS y Android, sin duplicar estilos. Además, puedes sumar estilos comunes (p. ej., colores) al final del arreglo.

style:[
  styles.base,
  Platform.OS === 'ios' ? styles.ios : styles.android,
  styles.common,
]
  • Uso de Platform.OS para decidir entre iOS y Android.
  • Base consistente para layout.
  • Ajustes visuales nativos: sombras en iOS y elevation en Android.

¿Cómo construir un SuggestService con catálogo, modelo y fetch?

Se crea un servicio en TS para manejar sugerencias por categoría. Incluye un modelo que comparte campos con las cards y añade prioridad. Un catálogo agrupa hábitos por categorías como energía y enfoque, con id, emoji y prioridad. La selección puede simular una llamada a una API mediante fetch con POST, headers, body y try/catch; si no hay endpoint, se devuelve la sugerencia del catálogo según la categoría y prioridad.

¿Qué modelo y catálogo necesitas?

  • Modelo con id, emoji, title, subtitle, priority.
  • Catálogo indexado por categoría: energia, enfoque.
  • Llaves de categoría tipadas para seguridad en TS.
// SuggestService.ts
export type HabitSuggestion = {
  id: string;
  emoji: string;
  title: string;
  subtitle: string;
  priority: number;
};

type CategoryKey = 'energia' | 'enfoque';

const catalog: Record<CategoryKey, HabitSuggestion[]> = {
  energia: [
    { id: 'energia_despertar', emoji: '☀️', title: 'Levantarte temprano', subtitle: 'Activa tu mañana', priority: 2 },
    { id: 'energia_hidratacion', emoji: '💧', title: 'Hidratación', subtitle: 'Más energía sostenida', priority: 1 },
  ],
  enfoque: [
    { id: 'enfoque_bloques', emoji: '🎯', title: 'Trabajo en bloques', subtitle: 'Sin distracciones', priority: 1 },
    { id: 'enfoque_respiro', emoji: '🌬️', title: 'Pausa consciente', subtitle: 'Recupera foco', priority: 2 },
  ],
};

export async function getByCategory(key: CategoryKey): Promise<HabitSuggestion[]> {
  await new Promise(r => setTimeout(r, 300));
  return catalog[key];
}

¿Cómo simular la llamada a la API y manejar errores?

  • Endpoint opcional para IA simulada.
  • Envío de category, profileName y habits en el body.
  • Manejo de latencia con await y control de errores con try/catch.
type SuggestParams = {
  category: CategoryKey;
  profileName: string;
  habits: string[];
};

const ENDPOINT = '';

export async function suggestService(params: SuggestParams): Promise<HabitSuggestion> {
  const { category } = params;
  try {
    if (ENDPOINT) {
      const res = await fetch(ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
      });
      if (!res.ok) throw new Error('Network error');
      const data = await res.json();
      return data.suggestion as HabitSuggestion;
    }

    // Fallback: usar catálogo local.
    await new Promise(r => setTimeout(r, 400));
    const list = catalog[category];
    return list.sort((a, b) => a.priority - b.priority)[0];
  } catch (error) {
    // Repropagar para identificar el fallo aguas arriba.
    throw error;
  }
}

¿Tienes otra categoría o hábito que quisieras priorizar? Comenta tu caso y lo integramos en el catálogo.