Creando una aplicación React Native del mundo real desde cero con Expo y Metricalp (2024)
Sabemos que no te gusta leer documentaciones ni plantillas predefinidas. Por eso, preparamos un ejemplo del mundo real para ti. En esta publicación, crearemos una aplicación React Native desde cero con Expo y Metricalp.
Introducción
Vamos a desarrollar una aplicación de ToDo en React Native. El usuario puede agregar, actualizar y eliminar tareas. Pero, mientras pensamos en esto como una aplicación del mundo real, tendremos algunos anuncios molestos en la pantalla 😬. El usuario puede actualizar a una cuenta PRO para deshacerse de los anuncios. Así que, en nuestra aplicación, tendremos una segunda pantalla /upgrade-pro. En esta pantalla tendremos un botón para actualizar a PRO. Usaremos Metricalp para rastrear los comportamientos del usuario y analizar nuestra aplicación. Confía en mí, va a ser divertido.
Para que parezca una aplicación del mundo real, utilizaremos algunas bibliotecas increíbles en nuestra aplicación, como Zustand store (para gestionar el estado) y MMKV store (para almacenar datos en el almacenamiento local de manera rápida). Aquí algunos capturas finales de nuestra app:
Vale, basta de hablar, ¡vamos a construirlo 🚀!
Configurar el Proyecto
Primero, necesitamos crear un nuevo proyecto de React Native con Expo:
npx create-expo-app@latest
En este ejemplo, utilizaremos principalmente el simulador de iOS. Por lo tanto, debes tener Xcode instalado en tu máquina. Si no tienes Xcode, puedes instalarlo desde la App Store. Además, instalaremos watchman para trabajar correctamente en el simulador:
brew update
brew install watchman
Luego instalaremos expo dev client para trabajar con nuestra app en el simulador:
npx expo install expo-dev-client
Asegúrate de tener instalada la última versión del simulador. Puedes comprobarlo en XCode -> Preferences -> Components.
Ejecutemos nuestra aplicación por primera vez, dentro de la carpeta de nuestra nueva aplicación, ejecuta:
npx expo run:ios
Instalar Bibliotecas
Ahora, instalaremos algunas bibliotecas para facilitarnos la vida. Primero, instalaremos la biblioteca react-native-mmkv store de mrousavy. Funciona basada en la nueva arquitectura de React Native, por lo que puedes leer/escribir en el almacenamiento local de forma rápida y sincronizada:
npx expo install react-native-mmkv
En este punto también necesitamos ejecutar expo prebuild
npx expo prebuild
Para rastrear a nuestros usuarios y sus tareas, usaremos UUIDs creadas aleatoriamente. Instalaremos la biblioteca crypto de expo para generar estos UUIDs:
npx expo install expo-crypto
Mantendremos las tareas en el Zustand store (una biblioteca de gestión de estado), así que necesitamos instalarla:
yarn add zustand
Finalmente, instalaremos Metricalp para rastrear los comportamientos de usuario en nuestra app (mientras escribimos este artículo la última versión es 1.0.12 pero siempre instala la más reciente):
yarn add @metricalp/react-native
Estructura de la App
Aquí está la estructura final de la app. Explicaremos todos los archivos utilizados a continuación:
Empecemos con lo básico. Primero crea una carpeta llamada 'utils' en la raíz del proyecto.
Luego crea un archivo llamado zustand-store.ts en la carpeta utils. Este archivo se utilizará para gestionar nuestro estado en la app:
import { unstable_batchedUpdates } from 'react-native'; // or 'react-native'
import { create } from 'zustand';
import * as Crypto from 'expo-crypto';
import { storage } from './mmkv-store';
interface Todo {
id: string;
text: string;
}
interface ZustandStore {
todos: Todo[];
}
export const useZustandStore = create<ZustandStore>((set) => ({
todos: [],
}));
export const setTodos = (todos: Todo[]) => {
unstable_batchedUpdates(() => {
useZustandStore.setState({ todos });
});
storage.set('USER_TODOS_KEY', JSON.stringify(todos));
};
export const updateTodo = (id: string, text: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, text };
}
return todo;
});
setTodos(updatedTodos);
};
export const addTodo = (text: string) => {
const todos = useZustandStore.getState().todos;
const newTodo = { id: Crypto.randomUUID(), text };
setTodos([...todos, newTodo]);
};
export const deleteTodo = (id: string) => {
const todos = useZustandStore.getState().todos;
const updatedTodos = todos.filter((todo) => todo.id !== id);
setTodos(updatedTodos);
};
export const resetTodos = () => {
setTodos([]);
};
Básicamente, definimos un Zustand store con tareas. Tenemos algunas funciones para agregar, actualizar, eliminar y restablecer tareas. Usamos mmkv-store para almacenar las tareas en el almacenamiento local. También actualizamos esta copia en el almacenamiento local en cada actualización del estado. Generamos UUIDs aleatorios para cada nueva tarea.
Ahora generemos un archivo llamado mmkv-store.ts en la carpeta utils:
import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV();
Ahora generemos un archivo llamado constants.ts en la carpeta utils. Mantendremos las constantes aquí:
export const METR_UUID_KEY = 'metr.uuid';
export const USER_TODOS_KEY = 'user.todos';
export const METRICALP_TID = 'mam48'; // Reemplaza con tu TID de Metricalp
export const SCREENS = {
INDEX: 'index',
UPGRADE_PRO: 'upgrade-pro',
};
Finalmente, generemos un archivo llamado metricalp-events.ts en la carpeta utils. Aquí guardaremos nuestros eventos personalizados para Metricalp:
export const MetricalpCustomEvents = {
UPGRADE_PRO_NAVIGATOR: 'upgrade_pro_navigator',
TODO_DELETE_CLICK: 'todo_delte_click',
TODO_EDIT_CLICK: 'todo_edit_click',
ADD_TODO_CLICK: 'add_todo_click',
UPDATE_TODO_CLICK: 'update_todo_click',
UPGRADE_PRO_CLICK: 'upgrade_pro_click',
};
Seguiremos los clics de edición y eliminación de tareas, el clic para agregar tareas, el clic para actualizar tareas y los clics para actualizar a PRO en nuestra app. También queremos rastrear cómo el usuario navega a la pantalla de actualización, por lo tanto, tenemos otro evento upgrade_pro_navigator que activaremos cada vez que un usuario navegue a la pantalla de actualización. Adjuntaremos una propiedad a este evento para guardar cómo navegó el usuario a la actualización. También registraremos cuántas tareas tenía el usuario cuando decidió actualizar. Porque esto podría ser una métrica importante (el equipo de marketing dijo esto). Además, queremos registrar la hora del día (mañana, tarde, noche o madrugada) para todos estos eventos. Queremos entender el comportamiento del usuario, tal vez tienden a usar la aplicación por la noche, pero tienen más inclinación a actualizarse por la mañana. Nuevamente, el equipo de marketing solicitó estos datos. En Metricalp, podemos mantener cualquier cosa, podemos tener escenarios ilimitados 🤝
Ahora, revisemos el archivo app/_layout.tsx, que es nuestro archivo de diseño principal, inicializaremos Metricalp y nuestra app aquí:
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack, usePathname } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useRef, useState } from 'react';
import 'react-native-reanimated';
import * as Crypto from 'expo-crypto';
import { useColorScheme } from '@/hooks/useColorScheme';
import { storage } from '@/utils/mmkv-store';
import {
METRICALP_TID,
METR_UUID_KEY,
USER_TODOS_KEY,
} from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { setTodos } from '@/utils/zustand-store';
import { AppState, NativeEventSubscription, Platform } from 'react-native';
// Evita que la pantalla de inicio se oculte automáticamente antes de que se complete la carga de los activos.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
const [initializationCompleted, setInitializationCompleted] = useState(false);
const pathname = usePathname();
const lastPathnameRef = useRef<string>('/');
lastPathnameRef.current = pathname;
useEffect(() => {
let subscription: NativeEventSubscription;
let userUUID = storage.getString(METR_UUID_KEY);
if (!userUUID) {
userUUID = Crypto.randomUUID();
storage.set(METR_UUID_KEY, userUUID);
}
let todos = [];
const userTODOsFromMMKVStore = storage.getString(USER_TODOS_KEY);
if (userTODOsFromMMKVStore) {
todos = JSON.parse(userTODOsFromMMKVStore);
}
setTodos(todos);
const osWithVersion =
(Platform.OS === 'ios' ? 'iOS' : 'Android') + ' ' + Platform.Version;
Metricalp.init({
platform: Platform.OS,
app: '[email protected]',
language: 'Spanish-ES',
os: osWithVersion,
uuid: userUUID,
tid: METRICALP_TID,
});
setInitializationCompleted(true);
subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
Metricalp.appLeaveEvent();
} else if (nextAppState === 'active') {
Metricalp.screenViewEvent(lastPathnameRef.current);
}
});
return () => {
subscription?.remove();
};
}, []);
useEffect(() => {
if (!initializationCompleted) {
return;
}
Metricalp.screenViewEvent(pathname);
}, [pathname, initializationCompleted]);
useEffect(() => {
if (loaded && initializationCompleted) {
SplashScreen.hideAsync();
}
}, [loaded, initializationCompleted]);
if (!loaded || !initializationCompleted) {
return null;
}
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}
Estamos inicializando nuestras tareas desde el almacenamiento local primero (mmkv store). Luego inicializamos Metricalp con información básica como plataforma, nombre de la app, idioma, sistema operativo, UUID y TID. También estamos rastreando los cambios de estado de la app para entender cuándo la app va al fondo o al frente. También estamos rastreando las vistas de pantalla en nuestra app, estamos usando el hook usePathname para obtener el nombre del camino actual de nuestra app. Ocultamos la pantalla de inicio cuando todas estas inicializaciones se completan.
Generamos un UUID de usuario por usuario y lo mantenemos en el almacenamiento local. Metricalp usa este UUID para identificar al usuario y contar vistas/eventos únicos. También puedes usar user_id, etc. si lo deseas.
Cuando la app va al fondo o está inactiva, generamos eventos appLeaveEvent desde la biblioteca de Metricalp. Esto nos ayuda a registrar la duración de la vista de pantalla, etc.
Hemos generado un hook personalizado useDayTime dentro de la carpeta hooks (hooks/useDayTime.ts) para obtener la hora del día del usuario:
export function useDayTime() {
const now = new Date();
const hours = now.getHours();
if (hours >= 6 && hours < 12) {
return 'mañana';
}
if (hours >= 12 && hours < 18) {
return 'tarde';
}
if (hours >= 18 && hours < 24) {
return 'noche';
}
return 'madrugada';
}
Ahora vamos a editar el archivo (tabs)/_layout.tsx para tener un menú de pestañas de actualización y activar el evento upgrade_pro_navigator cuando se haga clic en este menú de pestañas:
import { Tabs } from 'expo-router';
import React from 'react';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { SCREENS } from '@/utils/constants';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { useDayTime } from '@/hooks/useDayTime';
export default function TabLayout() {
const colorScheme = useColorScheme();
const dayTime = useDayTime();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
}}
screenListeners={{
tabPress: (e) => {
if (e.target?.startsWith(SCREENS.UPGRADE_PRO)) {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
day_time: dayTime,
placement: 'bottom-tab',
current_todos_count: useZustandStore.getState().todos.length,
});
}
},
}}
>
<Tabs.Screen
name={SCREENS.INDEX}
options={{
title: 'Inicio',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'home' : 'home-outline'}
color={color}
/>
),
}}
/>
<Tabs.Screen
name={SCREENS.UPGRADE_PRO}
options={{
title: 'Upgrade Pro',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon
name={focused ? 'arrow-up-circle' : 'arrow-up-circle-outline'}
color={color}
/>
),
}}
/>
</Tabs>
);
}
Vea que también estamos adjuntando las propiedades placement, current_todos_count y day_time a nuestro evento personalizado. ¡Qué fácil y genial, verdad?
Ahora edite el archivo (tabs)/index.tsx para tener la pantalla principal de nuestra aplicación:
import { SCREENS } from '@/utils/constants';
import {
addTodo,
deleteTodo,
updateTodo,
useZustandStore,
} from '@/utils/zustand-store';
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
import { Metricalp } from '@metricalp/react-native';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useDayTime } from '@/hooks/useDayTime';
export default function HomeScreen() {
const todos = useZustandStore((state) => state.todos);
const [todoInput, setTodoInput] = useState('');
const [editId, setEditId] = useState('');
const dayTime = useDayTime();
const router = useRouter();
const handleAddTask = () => {
if (editId) {
Metricalp.customEvent(MetricalpCustomEvents.UPDATE_TODO_CLICK, {
day_time: dayTime,
});
updateTodo(editId, todoInput);
} else {
Metricalp.customEvent(MetricalpCustomEvents.ADD_TODO_CLICK, {
day_time: dayTime,
});
addTodo(todoInput);
}
setEditId('');
setTodoInput('');
};
const handleEditTask = (id: string) => {
const taskToEdit = todos.find((todo) => todo.id === id);
Metricalp.customEvent(MetricalpCustomEvents.TODO_EDIT_CLICK, {
day_time: dayTime,
});
setTodoInput(taskToEdit?.text || '');
setEditId(id);
};
const handleDeleteTask = (id: string) => {
Metricalp.customEvent(MetricalpCustomEvents.TODO_DELETE_CLICK, {
day_time: dayTime,
});
deleteTodo(id);
};
const renderItem = ({ item }: any) => (
<View style={styles.task}>
<Text style={styles.itemList}>{item.text}</Text>
<View style={styles.taskButtons}>
<TouchableOpacity onPress={() => handleEditTask(item.id)}>
<Text style={styles.editButton}>Editar</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDeleteTask(item.id)}>
<Text style={styles.deleteButton}>Eliminar</Text>
</TouchableOpacity>
</View>
</View>
);
const renderAds = (adsPlace: 'top' | 'bottom') => {
return (
<>
<View style={styles.adsContainer}>
<Text style={styles.adsText}>Aquí hay algunos anuncios</Text>
</View>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_NAVIGATOR, {
placement: adsPlace,
day_time: dayTime,
current_todos_count: todos.length,
});
router.push(SCREENS.UPGRADE_PRO);
}}
>
<Text style={styles.upgradeButtonText}>
Mejora a Pro ahora para una experiencia sin anuncios
</Text>
</TouchableOpacity>
</>
);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Aplicación de Lista de Tareas</Text>
{renderAds('top')}
<TextInput
style={styles.input}
placeholder="Introduce una tarea"
value={todoInput}
onChangeText={(text) => setTodoInput(text)}
/>
<TouchableOpacity style={styles.addButton} onPress={handleAddTask}>
<Text style={styles.addButtonText}>
{editId ? 'Actualizar Tarea' : 'Agregar Tarea'}
</Text>
</TouchableOpacity>
<FlatList
data={todos}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
{renderAds('bottom')}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
heading: {
fontSize: 30,
fontWeight: 'bold',
marginBottom: 7,
color: 'green',
},
adsContainer: {
borderWidth: 2,
borderColor: 'purple',
marginTop: 10,
padding: 5,
borderRadius: 10,
},
adsText: {
fontSize: 14,
fontWeight: 'bold',
color: 'purple',
},
input: {
borderWidth: 3,
borderColor: '#ccc',
padding: 10,
marginBottom: 10,
marginTop: 20,
borderRadius: 10,
fontSize: 18,
},
addButton: {
backgroundColor: 'green',
padding: 10,
borderRadius: 5,
marginBottom: 10,
},
addButtonText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
fontSize: 18,
},
task: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
fontSize: 18,
},
itemList: {
fontSize: 19,
},
taskButtons: {
flexDirection: 'row',
},
editButton: {
marginRight: 10,
color: 'green',
fontWeight: 'bold',
fontSize: 18,
},
deleteButton: {
color: 'red',
fontWeight: 'bold',
fontSize: 18,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 10,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
Así que tenemos una aplicación de ToDo estándar. Podemos agregar/editar/eliminar tareas. Tenemos anuncios molestos y botones de mejora junto a los anuncios.
Estamos rastreando cada clic para agregar/editar/eliminar a través de Metricalp y estamos adjuntando la propiedad day_time a todos estos eventos.
Pero también estamos adjuntando las propiedades placement y current_todos_count al evento upgrade_pro_navigator. Porque son importantes para nosotros desde una perspectiva de marketing.
Finalmente, cree el archivo (tabs)/upgrade-pro.tsx para tener nuestra pantalla de actualización Pro:
import { useDayTime } from '@/hooks/useDayTime';
import { MetricalpCustomEvents } from '@/utils/metricalp-events';
import { useZustandStore } from '@/utils/zustand-store';
import { Metricalp } from '@metricalp/react-native';
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
export default function UpgradeProScreen() {
const dayTime = useDayTime();
return (
<View style={styles.container}>
<Text style={styles.title}>Aplicación de Lista de Tareas</Text>
<Text>
Nunca verás anuncios en la aplicación cuando actualices a PRO.
</Text>
<TouchableOpacity
style={styles.upgradeButton}
onPress={() => {
Metricalp.customEvent(MetricalpCustomEvents.UPGRADE_PRO_CLICK, {
day_time: dayTime,
current_todos_count: useZustandStore.getState().todos.length,
});
alert('Actualizado a Pro ahora');
}}
>
<Text style={styles.upgradeButtonText}>Mejora a Pro ahora</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 40,
marginTop: 40,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
upgradeButton: {
backgroundColor: 'purple',
padding: 10,
marginVertical: 20,
borderRadius: 5,
marginBottom: 10,
},
upgradeButtonText: {
color: 'white',
},
});
Aquí estamos generando un evento personalizado upgrade_pro_click cuando el usuario hace clic en el botón de mejora. También estamos adjuntando las propiedades day_time y current_todos_count a este evento. También estamos mostrando una alerta al usuario sobre que ha mejorado.
current_todos_count puede ser importante porque podemos medir que un usuario decide mejorar cuando tiene un cierto número de tareas. Tal vez podamos mostrar un descuento a los usuarios que tengan más de 10 tareas? 🤔 Mira cómo Metricalp ayuda a tomar acciones de marketing.
De acuerdo, estamos casi listos, pero necesitamos definir estas propiedades personalizadas en los eventos personalizados en el panel de Metricalp. Vayamos a la configuración del rastreador en el panel de Metricalp para hacerlo:
Aquí están todos los eventos:
Vamos a adjuntar propiedades personalizadas a estos eventos:
Bueno, hemos añadido todos nuestros eventos personalizados. Adjuntamos 'day_time' como alias de custom_prop1 a todos los eventos. También adjuntamos placement como custom_prop2 y current_todos_count como custom_prop3 al evento upgrade_pro_navigator. También adjuntamos current_todos_count al evento upgrade_pro_click como custom_prop2. Estamos listos 🚀
Revisemos nuestro panel:
Bueno, tenemos eventos de screen_view con información de ubicación, ruta, idioma del usuario, versión del sistema operativo, versión de la aplicación, pero además tenemos eventos personalizados para agregar/actualizar/eliminar tareas y acciones de actualización. Tenemos información del tiempo del día, información de la cantidad actual de tareas e información de la ubicación del navegador de actualización. Lo cual es genial. Estamos recopilando datos muy valiosos como en una prueba A/B real. Podemos decidir mostrar el botón de actualización a los usuarios, por ejemplo, que tienen más de 10 tareas y podemos decidir mostrar descuentos en horario nocturno. Podemos eliminar el botón de navegación de actualización inferior y mantener solo el superior. Quiero decir, los datos determinarán nuestra dirección de marketing como en un negocio real.
Bueno, todo es fácil con estas poderosas tecnologías como React Native, Expo y, por supuesto, Metricalp 💜. Gracias si leíste hasta aquí, no dudes en contactarnos si tienes alguna 🤝