John Cardozo
John Cardozo
React Native es un framework open-source creado por Meta enfocado al desarrollo de aplicaciones para Android, Android TV, iOS, macOS, tvOS, Web, Windows y UWP.
Pros
Cons
Eficiencia de costos
Optimización de recursos: desarrolladores con las mismas habilidades
Código base unificado: mismo código para todas las plataformas
Desempeño ligeramente menor que aplicaciones nativas
Problemas en actualizaciones de librerías externas
Brecha con nuevas características de los sistemas operativos
Showcase
Uso de plataformas móviles
Nativo
Híbrido
iOS
Android
Swift
Java
Kotlin
Javascript
Código base
Múltiples repos
Equipo de desarrollo / Testing
Cambios de reglas de negocio
React Native
iOS
Android
Desempeño
Uso de utilidades del dispositivo
Librerías de terceros
Expo
VSCode
HTML
CSS
Javascript
React
NodeJS
Git
Android Studio
XCode
iOS
open -a Simulator.app
Se puede ejecutar la app Simulator.app desde la Terminal o desde un launcher
Android
Se descarga Android Studio y se crea un device
inicia el emulador
Crea un nuevo dispositivo
~/Library/Android/sdk/emulator/emulator -list-avds
~/Library/Android/sdk/emulator/emulator -avd Pixel_6_Pro_API_33
lista de emuladores
inicia el emulador
Creación del proyecto
npx create-expo-app --template
sudo npm install -g create-expo-app
Opcional: Actualización de create-expo-app
- cd my-coffee
- npm run android
- npm run ios
- npm run web
Siguientes pasos
Computador Mac
Ejecución web: Si muestra error, hay que instalar paquetes adicionales
npx expo install react-native-web@~0.18.9 react-dom@18.1.0 @expo/webpack-config@^0.17.2
Es posible que solo funcione con Node 16
ERR_OSSL_EVP_UNSUPPORTED
Simulador en ejecución
Cualquier Sistema Operativo
Emulador en ejecución
npm start
blank
App.js
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
importa componentes
componente principal
Estilos del componente
Documentación de componentes
App.js
import Main from "./src/components/Main";
export default function App() {
return <Main />;
}
components / Main.jsx
import React from "react";
import { Text, View } from "react-native";
const Main = () => {
return (
<View>
<Text>Our coffee</Text>
</View>
);
};
export default Main;
components / Main.jsx
import React from "react";
import { SafeAreaView, StyleSheet, StatusBar, Text } from "react-native";
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<Text>Our Coffee</Text>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
},
});
export default Main;
SafeAreaView establece el área donde la aplicación se puede visualizar
data / coffees.js
export const coffees = [
{
id: "1", product: "Mocca", brand: "Juan Valdez",
country: "Colombia", body: "full", acidity: "milt",
notes: "caramel", stars: 4,
},
{
id: "2", product: "Vesuvio", brand: "Kolb",
country: "Kenia", body: "smooth", acidity: "low",
notes: "chocolate", stars: 3,
},
{
id: "3", product: "RR", brand: "Kolb",
country: "Camerun", body: "full", acidity: "milt",
notes: "hazelnut", stars: 4,
},
];
components / CoffeeList.jsx
import React from "react";
import { View, Text } from "react-native";
import { coffees } from "../data/coffees";
const CoffeeList = () => {
return (
<View>
{coffees.map((coffee) => (
<View key={coffee.id}>
<Text>{coffee.product}</Text>
</View>
))}
</View>
);
};
export default CoffeeList;
Map sobre la lista para generar los nuevos componentes
El key es requerido
Main.jsx
import CoffeeList from "./CoffeeList";
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<CoffeeList />
</SafeAreaView>
);
};
import React from "react";
import { View, Text } from "react-native";
import { coffees } from "../data/coffees";
const CoffeeList = () => {
return (
<View>
{coffees.map((coffee) => (
<View key={coffee.id}>
<Text>{coffee.id}</Text>
<Text>{coffee.product}</Text>
<Text>{coffee.brand}</Text>
<Text>{coffee.country}</Text>
<Text>{coffee.body}</Text>
<Text>{coffee.acidity}</Text>
<Text>{coffee.notes}</Text>
<Text>{coffee.stars}</Text>
</View>
))}
</View>
);
};
Sin scroll
components / CoffeeList.jsx
import React from "react";
import { View, FlatList, Text } from "react-native";
import { coffees } from "../data/coffees";
const CoffeeList = () => {
return (
<FlatList
data={coffees}
ItemSeparatorComponent={<Text> </Text>}
renderItem={({ item }) => (
<View>
<Text>{item.id}</Text>
<Text>{item.product}</Text>
<Text>{item.brand}</Text>
<Text>{item.country}</Text>
<Text>{item.body}</Text>
<Text>{item.acidity}</Text>
<Text>{item.notes}</Text>
<Text>{item.stars}</Text>
</View>
)}
keyExtractor={(item) => item.id}
/>
);
};
Con scroll
map
FlatList
import React from "react";
import { View, FlatList, Text } from "react-native";
import { coffees } from "../data/coffees";
import CoffeeItem from "./CoffeeItem";
const CoffeeList = () => {
return (
<FlatList
data={coffees}
ItemSeparatorComponent={<Text> </Text>}
renderItem={({ item }) => <CoffeeItem item={item} />}
keyExtractor={(item) => item.id}
/>
);
};
export default CoffeeList;
components / CoffeeList.jsx
import React from "react";
import { View, Text } from "react-native";
const CoffeeItem = ({ item }) => {
return (
<View>
<Text>{item.id}</Text>
<Text>{item.product}</Text>
<Text>{item.brand}</Text>
<Text>{item.country}</Text>
<Text>{item.body}</Text>
<Text>{item.acidity}</Text>
<Text>{item.notes}</Text>
<Text>{item.stars}</Text>
</View>
);
};
export default CoffeeItem;
components / CoffeeItem.jsx
import React from "react";
import { View, Text } from "react-native";
const CoffeeItem = ({ coffee }) => {
return (
<View style={{ padding: 20 }}>
<Text>{coffee.id}</Text>
<Text style={{ fontWeight: 500 }}>{coffee.product}</Text>
<Text>{coffee.brand}</Text>
<Text>{coffee.country}</Text>
<Text>{coffee.body}</Text>
<Text>{coffee.acidity}</Text>
<Text>{coffee.notes}</Text>
<Text>{coffee.stars}</Text>
</View>
);
};
export default CoffeeItem;
components / CoffeeItem.jsx
Estilos mezclados con JSX
No es la mejor práctica
import React from "react";
import { View, Text, StyleSheet } from "react-native";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<Text>{coffee.id}</Text>
<Text style={styles.product}>{coffee.product}</Text>
<Text>{coffee.brand}</Text>
<Text>{coffee.country}</Text>
<Text>{coffee.body}</Text>
<Text>{coffee.acidity}</Text>
<Text>{coffee.notes}</Text>
<Text>{coffee.stars}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
product: {
fontWeight: "500",
color: "brown",
},
});
export default CoffeeItem;
components / CoffeeItem.jsx
Definición de estilos
Uso de estilos
import { StyleSheet, Text } from "react-native";
const styles = StyleSheet.create({
text: {
fontSize: 15,
color: "gray",
},
bold: {
fontWeight: "bold",
},
blue: {
color: "blue",
},
big: {
fontSize: 20,
},
small: {
fontSize: 12,
},
});
export default function StyledText({ blue, bold, children, big, small }) {
const textStyles = [
styles.text,
blue && styles.blue,
bold && styles.bold,
big && styles.big,
small && styles.small,
];
return <Text style={textStyles}>{children}</Text>;
}
components / StyledText.jsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import StyledText from "./StyledText";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<StyledText big bold>{coffee.id}</StyledText>
<StyledText bold>{coffee.product}</StyledText>
<StyledText blue>{coffee.brand}</StyledText>
<StyledText small>{coffee.country}</StyledText>
<StyledText small>{coffee.body}</StyledText>
<StyledText small>{coffee.acidity}</StyledText>
<StyledText small>{coffee.notes}</StyledText>
<StyledText small>{coffee.stars}</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
}
});
export default CoffeeItem;
components / CoffeeItem.jsx
Uso del componente
Definición del componente
const theme = {
colors: {
textPrimary: "#555555",
textSecondary: "#9d9d9d",
background: "#d7d7d8",
cardBackground: "#fff",
},
fontSizes: {
heading: 20,
subheading: 17,
normal: 16,
},
fonts: {
main: "System",
},
fontWeights: {
normal: "400",
bold: "700",
},
};
export default theme;
theme.js
import { StyleSheet, Text } from "react-native";
import theme from "../theme.js";
const styles = StyleSheet.create({
text: {
color: theme.colors.textPrimary,
fontSize: theme.fontSizes.normal,
fontFamily: theme.fonts.main,
fontWeight: theme.fontWeights.normal,
},
colorPrimary: {
color: theme.colors.textPrimary,
},
colorSecondary: {
color: theme.colors.textSecondary,
},
bold: {
fontWeight: theme.fontWeights.bold,
},
heading: {
fontSize: theme.fontSizes.heading,
},
subheading: {
fontSize: theme.fontSizes.subheading,
},
});
export default function StyledText({
children,
color,
fontSize,
fontWeight,
...props
}) {
const textStyles = [
styles.text,
color === "primary" && styles.colorPrimary,
color === "secondary" && styles.colorSecondary,
fontSize === "subheading" && styles.subheading,
fontSize === "heading" && styles.heading,
fontWeight === "bold" && styles.bold,
];
return (
<Text style={textStyles} {...props}>
{children}
</Text>
);
}
components / StyledText.jsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import StyledText from "./StyledText";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<StyledText fontWeight="bold" fontSize="heading">
{coffee.product}
</StyledText>
<StyledText fontSize="subheading" fontWeight="bold">
{coffee.brand}
</StyledText>
<StyledText>{coffee.country}</StyledText>
<StyledText>{coffee.body}</StyledText>
<StyledText>{coffee.acidity}</StyledText>
<StyledText>{coffee.notes}</StyledText>
<StyledText>{coffee.stars}</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
product: {
fontWeight: "500",
color: "brown",
},
});
export default CoffeeItem;
components / CoffeeItem.jsx
import { StyleSheet, View } from "react-native";
import StyledText from "./StyledText";
const CoffeeItemHeader = ({ coffee }) => {
return (
<View style={styles.container}>
<StyledText fontWeight="bold" fontSize="heading">
{coffee.product}
</StyledText>
<StyledText>{coffee.stars}</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
export default CoffeeItemHeader;
components / CoffeeItemHeader.jsx
import React from "react";
import { View, StyleSheet } from "react-native";
import StyledText from "./StyledText";
import CoffeeItemHeader from "./CoffeeItemHeader";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<CoffeeItemHeader coffee={coffee} />
<StyledText fontSize="subheading" fontWeight="bold">
{coffee.brand}, {coffee.country}
</StyledText>
<StyledText>
{coffee.body}, {coffee.acidity}, {coffee.notes}
</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
});
export default CoffeeItem;
components / CoffeeItem.jsx
Reglas Flexbox
import React from "react";
import { FlatList, StyleSheet } from "react-native";
import { coffees } from "../data/coffees";
import CoffeeItem from "./CoffeeItem";
import theme from "../theme.js";
const CoffeeList = () => {
return (
<FlatList
style={styles.container}
data={coffees}
renderItem={({ item }) => <CoffeeItem coffee={item} />}
keyExtractor={(item) => item.id}
/>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
},
});
export default CoffeeList;
components / CoffeeList.jsx
import React from "react";
import { View, StyleSheet } from "react-native";
import StyledText from "./StyledText";
import CoffeeItemHeader from "./CoffeeItemHeader";
import theme from "../theme.js";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<CoffeeItemHeader coffee={coffee} />
<StyledText fontSize="subheading" fontWeight="bold">
{coffee.brand}, {coffee.country}
</StyledText>
<StyledText>
{coffee.body}, {coffee.acidity}, {coffee.notes}
</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: theme.colors.cardBackground,
marginTop: 6,
marginBottom: 6,
marginLeft: 15,
marginRight: 15,
borderRadius: 15,
},
});
export default CoffeeItem;
components / CoffeeItem.jsx
Estilos CSS
export const coffees = [
{
id: "1",
product: "Mocca",
brand: "Juan Valdez",
country: "Colombia",
body: "full",
acidity: "milt",
notes: "caramel",
stars: 4,
img: "https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80",
},
{
id: "2",
product: "Vesuvio",
brand: "Kolb",
country: "Kenia",
body: "smooth",
acidity: "low",
notes: "chocolate",
stars: 3,
img: "https://images.unsplash.com/photo-1595434091143-b375ced5fe5c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=735&q=80",
},
...
];
data / coffees.js
import React from "react";
import { Image, View, StyleSheet } from "react-native";
import StyledText from "./StyledText";
import CoffeeItemHeader from "./CoffeeItemHeader";
import theme from "../theme.js";
const CoffeeItem = ({ coffee }) => {
return (
<View style={styles.container}>
<Image style={styles.image} source={{ uri: coffee.img }} />
<CoffeeItemHeader coffee={coffee} />
<StyledText fontSize="subheading" fontWeight="bold">
{coffee.brand}, {coffee.country}
</StyledText>
<StyledText>
{coffee.body}, {coffee.acidity}, {coffee.notes}
</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: theme.colors.cardBackground,
marginTop: 6,
marginBottom: 6,
marginLeft: 15,
marginRight: 15,
borderRadius: 15,
},
image: {
height: 50,
borderRadius: 4,
marginBottom: 5,
},
});
export default CoffeeItem;
components / CoffeeItem.jsx
Estilo y source de imágenes
const { StyleSheet, View, StatusBar, Text } = require("react-native");
import theme from "../theme.js";
const AppBar = () => {
return (
<View style={styles.container}>
<Text style={styles.text}>Our Coffee</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.appBarBackground,
paddingTop: StatusBar.currentHeight + 10,
paddingBottom: 10,
paddingLeft: 15,
},
text: {
color: theme.colors.cardBackground,
fontSize: theme.fontSizes.heading,
fontWeight: theme.fontWeights.bold,
},
});
export default AppBar;
components / AppBar.jsx
import React from "react";
import { SafeAreaView, StyleSheet, StatusBar } from "react-native";
import CoffeeList from "./CoffeeList";
import AppBar from "./AppBar";
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<AppBar />
<CoffeeList />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
//marginTop: StatusBar.currentHeight || 0,
},
});
export default Main;
components / Main.jsx
Ya no es necesario: se elimina
const theme = {
colors: {
textPrimary: "#555555",
textSecondary: "#9d9d9d",
background: "#d7d7d8",
cardBackground: "#fff",
appBarBackground: "#B79383",
},
};
theme.js
import { Text, View } from "react-native";
const CoffeeStars = ({ stars }) => {
const array = Array(stars).fill(0);
return (
<View style={{ flexDirection: "row" }}>
{array.map((star, index) => (
<Text key={index}>⭐️</Text>
))}
</View>
);
};
export default CoffeeStars;
components / CoffeeStars.jsx
import { StyleSheet, View } from "react-native";
import StyledText from "./StyledText";
import CoffeeStars from "./CoffeeStars";
const CoffeeItemHeader = ({ coffee }) => {
return (
<View style={styles.container}>
<StyledText fontWeight="bold" fontSize="heading">
{coffee.product}
</StyledText>
<CoffeeStars stars={coffee.stars} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
export default CoffeeItemHeader;
components / CoffeeHeader.jsx
Inicialización de arreglo
npm install react-router-native
Instalación del paquete NodeJS
import Main from "./src/components/Main";
import { NativeRouter } from "react-router-native";
import { StatusBar } from "expo-status-bar";
export default function App() {
return (
<>
<StatusBar style="light" />
<NativeRouter>
<Main />
</NativeRouter>
</>
);
}
App.js
Se envuelve la aplicación con NativeRouter
StatusBar: light | dark
import React from "react";
import { Text, SafeAreaView, StyleSheet } from "react-native";
import { Route, Routes } from "react-router-native";
import CoffeeList from "./CoffeeList";
import AppBar from "./AppBar";
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<AppBar />
<Routes>
<Route path="/" element={<CoffeeList />} />
<Route path="/signin" element={<Text>SignIn</Text>} />
</Routes>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default Main;
components / Main.jsx
Routes + Route
const { StyleSheet, View, StatusBar, Text } = require("react-native");
import { Link } from "react-router-native";
import theme from "../theme.js";
import StyledText from "./StyledText.jsx";
const AppBarTab = ({ children, to }) => {
return (
<Link to={to}>
<StyledText fontWeight="bold" style={styles.text}>
{children}
</StyledText>
</Link>
);
};
const AppBar = () => {
return (
<View style={styles.container}>
<AppBarTab to="/">The Coffee Network</AppBarTab>
<AppBarTab to="/signin">Sign In</AppBarTab>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: theme.colors.appBarBackground,
paddingTop: StatusBar.currentHeight + 10,
paddingBottom: 10,
paddingLeft: 15,
paddingRight: 15,
},
text: {
color: theme.colors.cardBackground,
fontSize: theme.fontSizes.heading,
fontWeight: theme.fontWeights.bold,
},
});
export default AppBar;
components / AppBar.jsx
Link
import { StyleSheet, View, StatusBar, Text, ScrollView } from "react-native";
import { Link, useLocation } from "react-router-native";
import theme from "../theme.js";
import StyledText from "./StyledText.jsx";
const AppBarTab = ({ children, to }) => {
const { pathname } = useLocation();
const active = pathname === to;
const textStyles = [styles.text, active && styles.active];
return (
<Link to={to}>
<StyledText fontWeight="bold" style={textStyles}>
{children}
</StyledText>
</Link>
);
};
const AppBar = () => {
return (
<View style={styles.container}>
<AppBarTab to="/">The Coffee Network</AppBarTab>
<AppBarTab to="/signin">Sign In</AppBarTab>
</View>
);
};
const styles = StyleSheet.create({
container: { ... },
text: {
color: theme.colors.cardBackground,
fontSize: theme.fontSizes.heading,
fontWeight: theme.fontWeights.bold,
},
active: {
borderBottomColor: theme.colors.background,
borderBottomWidth: 1,
},
});
components / AppBar.jsx
Obtiene el path actual
Obtiene el path actual
Genera los estilos del texto
import React from "react";
import { Image, View, StyleSheet, Platform } from "react-native";
import StyledText from "./StyledText";
import CoffeeItemHeader from "./CoffeeItemHeader";
import theme from "../theme.js";
const CoffeeItem = ({ coffee }) => {
const platform = Platform.OS === "android" ? "🤖" : "🍏";
return (
<View style={styles.container}>
<Image style={styles.image} source={{ uri: coffee.img }} />
<CoffeeItemHeader coffee={coffee} />
<StyledText fontSize="subheading" fontWeight="bold">
{coffee.brand}, {coffee.country}
</StyledText>
<StyledText>
{platform}: {coffee.body}, {coffee.acidity}, {coffee.notes}
</StyledText>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: Platform.select({
android: theme.colors.cardBackground,
ios: "#FFCC6D",
default: "purple",
}),
marginTop: 6,
marginBottom: 6,
marginLeft: 15,
marginRight: 15,
borderRadius: 15,
},
image: {
height: 50,
borderRadius: 4,
marginBottom: 5,
},
});
components / CoffeeItem.jsx
Genera un valor dependiendo de la plataforma
Modo más simple: Platform.select
Genera un valor dependiendo de la plataforma
import { Platform }
from "react-native";
const theme = {
colors: { ... },
fontSizes: { ... },
fonts: {
main: Platform.select({
ios: "Times New Roman",
android: "Roboto",
default: "System",
}),
},
fontWeights: { ... },
};
theme.js
Selección de estilos por plataforma a nivel del theme
const AppBarTab = ({ children, to }) => { ... };
const IOSAppBar = () => { ... };
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: "black",
paddingTop: StatusBar.currentHeight + 10,
paddingBottom: 10,
paddingLeft: 15,
paddingRight: 15,
},
text: { ... },
active: { ... },
});
export default IOSAppBar;
components / IOSAppBar.jsx
Carga dinámica del componente dependiendo de la plataforma
import React from "react";
import { Text, SafeAreaView, StyleSheet, Platform } from "react-native";
import { Route, Routes } from "react-router-native";
import CoffeeList from "./CoffeeList";
const AppBar = Platform.select({
ios: () => require("./IOSAppBar").default,
default: () => require("./AppBar").default,
})();
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<AppBar />
<Routes>
<Route path="/" element={<CoffeeList />} />
<Route path="/signin" element={<Text>ABC</Text>} />
</Routes>
</SafeAreaView>
);
};
const styles = StyleSheet.create({ ... });
components / Main.jsx
La manera más fácil de cargar el componente
Renombrar IOSAppBar.jsx
AppBar.ios.jsx
Importar el componente SIN la extensión jsx
import { useEffect, useState } from "react";
import { FlatList, StyleSheet } from "react-native";
//import { coffees } from "../data/coffees";
import CoffeeItem from "./CoffeeItem";
import theme from "../theme.js";
const CoffeeList = () => {
const [coffees, setCoffees] = useState(null);
useEffect(() => {
const fetchCoffees = async () => {
const url = "https://64f8eadd824680fd21803236.mockapi.io/coffees";
try {
const response = await fetch(url);
const json = await response.json();
setCoffees(json);
} catch (error) {
console.log(error);
}
};
fetchCoffees();
}, []);
return (
<FlatList
style={styles.container}
data={coffees}
renderItem={({ item }) => <CoffeeItem coffee={item} />}
keyExtractor={(item) => item.id}
/>
);
};
components / CoffeeList.jsx
No se obtienen los datos del archivo local
Se obtienen los datos de un endpoint remoto
www.mockapi.com
useState
import { FlatList, StyleSheet } from "react-native";
import CoffeeItem from "./CoffeeItem";
import theme from "../theme.js";
import useCoffees from "../hooks/useCoffees";
const CoffeeList = () => {
const { coffees } = useCoffees();
return (
<FlatList
style={styles.container}
data={coffees}
renderItem={({ item }) => <CoffeeItem coffee={item} />}
keyExtractor={(item) => item.id}
/>
);
};
const styles = StyleSheet.create({ ... });
export default CoffeeList;
components / CoffeeList.jsx
import { useEffect, useState } from "react";
const useCoffees = () => {
const [coffees, setCoffees] = useState(null);
useEffect(() => {
const fetchCoffees = async () => {
const url = "https://64f8eadd824680fd21803236.mockapi.io/coffees";
try {
const response = await fetch(url);
const json = await response.json();
setCoffees(json);
} catch (error) {
console.log(error);
}
};
fetchCoffees();
}, []);
return { coffees };
};
export default useCoffees;
hooks / useCoffees.js
import { FlatList, StyleSheet, ActivityIndicator } from "react-native";
import { Dimensions } from "react-native";
import CoffeeItem from "./CoffeeItem";
import theme from "../theme.js";
import useCoffees from "../hooks/useCoffees";
const CoffeeList = () => {
const { coffees } = useCoffees();
return (
<>
{!coffees ? (
<ActivityIndicator
size="large"
color="#ff0000"
style={styles.loading}
/>
) : (
<FlatList
style={styles.container}
data={coffees}
renderItem={({ item }) => <CoffeeItem coffee={item} />}
keyExtractor={(item) => item.id}
/>
)}
</>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
},
loading: {
flex: 1,
justifyContent: "center",
alignItems: "center",
height: Dimensions.get("window").height,
},
});
components / CoffeeList.jsx
ActivityIndicator
estilo del indicador
Expresión ternaria
npm install react-native-dotenv
Instalación del paquete NodeJS
{
"plugins": [
[
"module:react-native-dotenv",
{
"envName": "APP_ENV",
"moduleName": "@env",
"path": ".env"
}
]
]
}
Variables de Ambiente con Expo
REACT_APP_URL_API=https://64f8eadd824680fd21803236.mockapi.io/coffees
.env
import { useEffect, useState } from "react";
import { REACT_APP_URL_API } from "@env";
const useCoffees = () => {
const [coffees, setCoffees] = useState(null);
useEffect(() => {
const fetchCoffees = async () => {
try {
const response = await fetch(REACT_APP_URL_API);
const json = await response.json();
setCoffees(json);
} catch (error) {
console.log(error);
}
};
fetchCoffees();
}, []);
return { coffees };
};
export default useCoffees;
hooks / useCoffees.js
.babelrc
EXPO_PUBLIC_URL_API=https://64f8eadd824680fd21803236.mockapi.io/coffees
const response = await fetch(process.env.EXPO_PUBLIC_URL_API);
import AddCoffee from "./AddCoffee";
const Main = () => {
return (
<SafeAreaView style={styles.container}>
<AppBar />
<Routes>
<Route path="/" element={<CoffeeList />} />
<Route path="/signin" element={<AddCoffee />} />
</Routes>
</SafeAreaView>
);
};
const styles = StyleSheet.create({ ... });
export default Main;
components / Main.jsx
npm install formik
Instalación del paquete NodeJS
Nueva ruta que contendrá el formulario
import { View, Button, StyleSheet, TextInput } from "react-native";
import { Formik } from "formik";
const initialValues = {
product: "",
brand: "",
country: "",
body: "",
acidity: "",
notes: "",
stars: "",
img: "",
};
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}>
{({ handleChange, handleSubmit, values }) => {
return (
<View style={styles.form}>
<TextInput placeholder="product" value={values.product} onChangeText={handleChange("product")} />
<TextInput placeholder="brand" value={values.brand} onChangeText={handleChange("brand")} />
<TextInput placeholder="country" value={values.country} onChangeText={handleChange("country")} />
<TextInput placeholder="body" value={values.body} onChangeText={handleChange("body")} />
<TextInput placeholder="acidity" value={values.acidity} onChangeText={handleChange("acidity")} />
<TextInput placeholder="notes" value={values.notes} onChangeText={handleChange("notes")} />
<TextInput placeholder="stars" value={values.stars} onChangeText={handleChange("stars")} />
<TextInput placeholder="image" value={values.img} onChangeText={handleChange("img")} />
<Button onPress={handleSubmit} title="Add coffee" />
</View>
);
}}
</Formik>
);
};
const styles = StyleSheet.create({
form: {
margin: 12,
},
});
export default AddCoffee;
components / AddCoffee.jsx
onSubmit
Función interna de Formik
eventos
onSubmit
onPress
onChangeText
Valores iniciales
import { View, Button, StyleSheet, TextInput } from "react-native";
import { Formik } from "formik";
const initialValues = { ... };
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}>
{({ handleChange, handleSubmit, values }) => {
return (
<View style={styles.form}>
...
<TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.button.text}>Add Coffee</Text>
</TouchableOpacity>
</View>
);
}}
</Formik>
);
};
const styles = StyleSheet.create({
form: {
margin: 12,
},
button: {
backgroundColor: theme.colors.appBarBackground,
color: "#fff",
padding: 8,
borderRadius: 5,
marginTop: 10,
text: {
color: "#fff",
textAlign: "center",
fontSize: 15,
},
},
});
export default AddCoffee;
components / AddCoffee.jsx
TouchableOpacity necesita Text
Se pueden utilizar estilos anidados
import React from "react";
import { StyleSheet, TextInput } from "react-native";
const StyledTextInput = ({ style = {}, ...props }) => {
const inputStyle = {
...styles.textInput,
...style,
};
return <TextInput style={inputStyle} {...props} />;
};
const styles = StyleSheet.create({
textInput: {
borderRadius: 5,
borderWidth: 1,
borderColor: "#999",
paddingHorizontal: 20,
paddingVertical: 10,
marginBottom: 10,
},
});
export default StyledTextInput;
components / StyledTextInput.jsx
import { View, Button, StyleSheet, TextInput } from "react-native";
import { Formik } from "formik";
import StyledTextInput from "./StyledTextInput";
const initialValues = { ... };
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)} >
{({ handleChange, handleSubmit, values }) => {
return (
<View style={styles.form}>
<StyledTextInput
placeholder="product"
value={values.product}
onChangeText={handleChange("product")}
/>
<StyledTextInput
placeholder="brand"
value={values.brand}
onChangeText={handleChange("brand")}
/>
...
<Button onPress={handleSubmit} title="Add coffee" />
</View>
);
}}
</Formik>
);
};
const styles = StyleSheet.create({
form: {
margin: 12,
},
});
components / AddCoffee.jsx
Se reemplaza el TextInput por StyledTextInput
import { Formik, useField } from "formik";
const initialValues = { ... };
const FormikInputValue = ({ name, ...props }) => {
const [field, meta, helpers] = useField(name);
return (
<StyledTextInput
value={field.value}
onChangeText={(value) => helpers.setValue(value)}
{...props}
/>
);
};
const Login = () => {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)} >
{({ handleSubmit }) => {
return (
<View style={styles.form}>
<FormikInputValue placeholder="product" name="product" />
<FormikInputValue placeholder="brand" name="brand" />
<FormikInputValue placeholder="country" name="country" />
<FormikInputValue placeholder="body" name="body" />
<FormikInputValue placeholder="acidity" name="acidity" />
<FormikInputValue placeholder="notes" name="notes" />
<FormikInputValue placeholder="stars" name="stars" />
<FormikInputValue placeholder="image" name="img" />
<Button onPress={handleSubmit} title="Add coffee" />
</View>
);
}}
</Formik>
);
};
components / AddCoffee.jsx
Creación de componente de manejo de datos de los campos del formulario
Uso del componente
Sólo se require el name y el placeholder
Solo se requiere handleSubmit
Se cambia StyledTextInput por FormikInputValue
secureTextEntry
atributo
passwords
import StyledText from "./StyledText";
const FormikInputValue = ({ name, ...props }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<StyledTextInput
value={field.value}
onChangeText={(value) => helpers.setValue(value)}
{...props} />
{meta.error && <StyledText style={styles.error}>{meta.error}</StyledText>}
</>
);
};
const validate = (values) => {
const errors = {};
if (!values.product) {
errors.product = "Product is required";
}
if (!values.country) {
errors.country = "Country is required";
}
if (!/^\+?(0|[1-9]\d*)$/.test(values.stars)) {
errors.stars = "Numeric value is required";
}
return errors;
};
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={(values) => console.log(values)} >
{ ... }
</Formik>
);
};
components / AddCoffee.jsx
Muestra el mensaje de validación
Uso de la función de validación
Validación del campo - Mostrar mensaje
Función de validación
const initialValues = { ... };
const FormikInputValue = ({ name, ...props }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<StyledTextInput
error={meta.error}
value={field.value}
onChangeText={(value) => helpers.setValue(value)}
{...props}
/>
{meta.error && <StyledText style={styles.error}>{meta.error}</StyledText>}
</>
);
};
const validate = (values) => { ... };
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={(values) => console.log(values)} >
{ ... }
</Formik>
);
};
const styles = StyleSheet.create({
form: {
margin: 12,
},
error: {
color: "red",
marginBottom: 15,
marginTop: -5,
},
});
components / AddCoffee.jsx
Parámetro de error
const StyledTextInput = ({ style = {}, error, ...props }) => {
const inputStyle =
[
styles.textInput,
style,
error && styles.error
];
return <TextInput style={inputStyle} {...props} />;
};
const styles = StyleSheet.create({
textInput: {
borderRadius: 5,
borderWidth: 1,
borderColor: "#999",
paddingHorizontal: 20,
paddingVertical: 10,
marginBottom: 10,
},
error: {
borderColor: "red",
},
});
components / StyledtextInput.jsx
Validación del campos - Bordes de error en el Input
import { coffeeValidationSchema } from "../schemas/coffeeValidation";
const initialValues = { ... };
const FormikInputValue = ({ name, ...props }) => {
const [field, meta, helpers] = useField(name);
return (
<>
<StyledTextInput
error={meta.error}
value={field.value}
onChangeText={(value) => helpers.setValue(value)}
{...props}
/>
{meta.error && <StyledText style={styles.error}>{meta.error}</StyledText>}
</>
);
};
const AddCoffee = () => {
return (
<Formik
initialValues={initialValues}
validationSchema={coffeeValidationSchema}
onSubmit={(values) => console.log(values)} >
{ ... }
</Formik>
);
};
components / AddCoffee.jsx
npm install yup
Instalación del paquete NodeJS
import * as yup from "yup";
export const coffeeValidationSchema =
yup
.object()
.shape({
product:
yup
.string()
.required("Please type a product"),
brand:
yup
.string()
.required(),
country:
yup
.string()
.oneOf(
[
"Colombia",
"Kenia",
"Camerun",
"Brazil"]),
stars:
yup
.number(),
});
schemas / coffeeValidation.js
Establece el schema de validación
min, max, required, number, string, oneOf, matches, email
import { useState } from "react";
import Slider from "@react-native-community/slider";
const AddCoffee = () => {
const [sliderValue, setSliderValue] = useState(3);
const addCoffee = async (values) => {
const data = {
...values,
img: selectedImage,
stars: parseInt(sliderValue),
};
try { ... } catch (error) {
console.log(error);
}
};
return (
<Formik onSubmit={addCoffee}>
{({ handleSubmit }) => {
return (
<View style={styles.form}>
<Text>Stars: {sliderValue}</Text>
<Slider
style={{ width: "100%", height: 40 }}
minimumValue={0}
maximumValue={5}
onValueChange={(value) => setSliderValue(value)}
value={sliderValue} />
</View>
);
}}
</Formik>
);
};
components / AddCoffee.jsx
npx expo install @react-native-community/slider
Instalación del paquete NodeJS
Uso del Slider
Obtiene el valor seleccionado
Muestra el valor seleccionado
import { useState } from "react";
import { SelectList } from "react-native-dropdown-select-list";
const countries = [
{ key: "1", value: "Colombia" },
{ key: "2", value: "Kenia" },
{ key: "3", value: "Camerun" },
{ key: "4", value: "Brazil" },
];
const AddCoffee = () => {
const [selectedCountry, setSelectedCountry] = useState("");
const addCoffee = async (values) => {
const data = {
...values,
country: selectedCountry,
img: selectedImage,
stars: parseInt(sliderValue),
};
...
};
return (
<Formik>
{({ handleSubmit }) => {
return (
<View style={styles.form}>
<SelectList
placeholder="Country"
setSelected={(val) => setSelectedCountry(val)}
data={countries}
save="value"
boxStyles={{ marginBottom: 15 }}
/>
</View>
);
}}
</Formik>
);
};
components / AddCoffee.jsx
npm install react-native-dropdown-select-list
Instalación del paquete NodeJS
Uso del Dropdown Select List
Obtiene el valor seleccionado
import { coffeeValidationSchema } from "../schemas/coffeeValidation";
import { REACT_APP_URL_API } from "@env";
import { useNavigate } from "react-router-native";
const initialValues = { ... };
const FormikInputValue = ({ name, ...props }) => { ... };
const AddCoffee = () => {
const navigate = useNavigate();
const addCoffee = async (values) => {
const data = { ...values, stars: parseInt(values.stars) };
try {
await fetch(REACT_APP_URL_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
navigate("/");
} catch (error) {
console.log(error);
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={coffeeValidationSchema}
onSubmit={addCoffee}>
...
</Formik>
);
};
components / AddCoffee.jsx
Uso de variable de entorno
Función de envío de datos
Navegación al home
Conversión de datos
import { Text, View, Button, StyleSheet, TouchableOpacity } from "react-native";
import * as ImagePicker from "expo-image-picker";
import { useState } from "react";
const AddCoffee = () => {
const navigate = useNavigate();
const [selectedImage, setSelectedImage] = useState(null);
const openImagePicker = async () => {
let permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.granted === false) {
alert("Permission to camera/images is required");
return;
}
const pickerResult = await ImagePicker.launchImageLibraryAsync();
if (!pickerResult.canceled) {
setSelectedImage(pickerResult.assets[0].uri);
}
};
const addCoffee = async (values) => {
const data = {
...values,
img: selectedImage,
stars: parseInt(values.stars),
};
...
};
return (
<Formik onSubmit={addCoffee} >
{({ handleSubmit }) => {
return (
<View style={styles.form}>
...
<Button onPress={openImagePicker} title="Select image..." />
<TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.button.text}>Add Coffee</Text>
</TouchableOpacity>
</View>
);
}}
</Formik>
);
};
components / AddCoffee.jsx
npx expo install expo-image-picker
Instalación del paquete NodeJS
Función para mostrar el ImagePicker
useState para almacenar la ruta de la imagen
{
"expo": {
"name": "our-coffee",
"slug": "our-coffee",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/fnc-splash.png",
"resizeMode": "contain",
"backgroundColor": "#B00025"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
App.json
Copiar las imágenes de Splash y el ícono en el folder assets y referenciarlas
1284 x 2778
Splash
Ícono
1024 x 1024
npx expo run:android
Instrucción para generar la aplicación
npx expo run:ios
Creación de cuenta gratis
Java
XCode
java -version
Verificar la instalación de Java
Crear proyecto
npm install --global eas-cli
eas init --id identificador
Se crea un nuevo proyecto en el sitio web y se toman los siguientes comandos
Documentación de Building
johncardozo@gmail.com