Curso de React Native

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.

React Native nos permite crear aplicaciones reales nativas para iOS y Android basado en React JS. Utilizando ReactJS, en lugar de obtener una aplicación web híbrida, obtenemos una aplicación real nativa.
Una aplicación híbrida es aquella que ejecuta código HTML, CSS y Javascript dentro de un componente de la aplicación móvil destinado a cargar páginas web (una especie de iframe). Una aplicación nativa es la que está programada utilizando el lenguaje nativo de la plataforma.

El código que programemos usando React Native será traducido al lenguaje nativo de la plataforma, mientras que cuando desarrollamos una aplicación híbrida, ejecutamos código HTML, CSS y JavaScript dentro de esta especie de iframe.

Para programar una aplicación nativa para Android, necesitaríamos conocer el lenguaje Kotlin, mientras que si la aplicación es para IOS, necesitaríamos saber Swift. Sin embargo, con React Native, podremos realizar una aplicación nativa para ambas plataformas conociendo una sola tecnología.

Las aplicaciones nativas, respecto de las aplicaciones híbridas, tienen:

  • Mejor rendimiento
  • Menor consumo de memoria.
  • Mayor velocidad.

Crear una aplicación en React Native con Expo

Documentación de la instalación.

1. Instalamos Expo en el sistema operativo:

npm install -g expo-cli

Es posible que durante el proceso de instalación tengamos el siguiente error:

npm ERR! code EEXIST
npm ERR! path C:\Users\mañana\AppData\Roaming\npm\expo.cmd
npm ERR! EEXIST: file already exists
npm ERR! File exists: C:\Users\mañana\AppData\Roaming\npm\expo.cmd
npm ERR! Remove the existing file and try again, or run npm
npm ERR! with --force to overwrite files recklessly.

Para evitarlo, antes de ejecutar el comando, deberemos eliminar todos los ficheros que comiencen por expo ubicados en la ruta C:\Users\mañana\AppData\Roaming\npm\

2. Creamos nuestro proyecto Expo. Si estamos usando windows, es recomendable que ejecutemos este comando desde la cmd, no desde powershell ni git bash. Crearemos la carpeta del proyecto y dentro ejecutaremos:

expo init .

Si al ejecutar el comando anterior obtuviesemos este error:

expo is not recognized as an internal or external command

Debemos añadir npm a nuestro conjunto de variables de entorno. En windows, sería: botón de inicio → Editar variables de entorno del sistema → Variables de entorno … → Path → %USERPROFILE%\AppData\Roaming\npm

Ejecutar nuestra aplicación de Expo

1. Ejecutamos el proyecto que hemos creado

npm start

2. Instalamos la aplicación Expo en el teléfono móvil: Expo

5. Para desplegar escanearemos el código qr generado. Para evitar problemas…

  • En el teléfono móvil Android, iremos a ajustes → Aplicaciones → Expo → Mostrar sobre otras apps.
  • Es probable que tengas actualizar el método start del package.json para añadir la opción de tunnel:
"scripts": {
    "start": "expo start --tunnel",
    ...
},
 

Actualizar Expo

Algunos problemas utilizando expo pueden solucionarse simplemente actualizándolo:

expo upgrade

Instalación de módulos en un proyecto de Expo

No debemos instalar los módulos utilizando npm, ya que este gestor de dependencias, por defecto, instala siempre la última versión del módulo y puede ocurrir que esta no sea compatible con la versión de Expo que estamos usando. En lugar del comando npm, utilizaremos el comando expo.

Ejecutando la aplicación en el emulador

1. Debemos tener instalado Android Studio.

2. Si estamos usando windows, tendremos que añadir las siguientes variables de entorno a nivel de usuario:

ANDROID_HOME → C:\Users(name)\AppData\Local\Android\Sdk
Añadir al Path → C:\Users(name)\AppData\Local\Android\Sdk\platform-tools

2. Creamos y arrancamos un nuevo Device Manager:

Curso de React Native 1

Componentes básicos de React Native

View

Equivalente a los div de HTML. Sirve para agrupar componentes.

import { View } from 'react-native';
<View><View>

Text

import { Text } from 'react-native';
<Text>El texto<Text>

TextInput

<TextInput onChangeText={text => setStateValue(text)}/>

Image

import { Image } from 'react-native';
import imagen from 'ruta-imagen/imagen.png';
<Image source={imagen} />

Picker (desplegable)

<Picker style={styles.container} 
  selectedValue={microCicloActivo}
  onValueChange={valor => setValor(valor)}>
    <Picker.Item key=0 value=0 label="titulo 0"/>
    <Picker.Item key=1 value=1 label="titulo 1"/>
    <Picker.Item key=2 value=2 label="titulo 2"/>
</Picker>

Button

<Button title="Cerrar" onPress={ btnPulsado }/>

El componete Button no se puede estilizar (sólo en Android). En su lugar, usaremos TouchableOpacity.

Listado de elementos:

Este componente permite renderizar sólo los elementos de la lista que se están viendo en pantalla, siendo más óptimas que usar combinaciones de etiquetas <View> y <Text> dentro de un map cuando tengamos un listado de muchos elementos.

El atributo keyExtractor es utilizado para identificar de manera inequivoca a cada registro, igual que hacíamos con el atributo key cuando usábamos un map en ReactJS.

<FlatList
  data={pacients}
  renderItem={({ item }) => <Text>{item}</Text>}
  keyExtractor={item => item.id}
/>

styled-components en react-native

Es posible utilizar Styled Components en React native sin ningún tipo de limitación. Sin embargo, para maquetar los componentes en React Native, hemos de tener en cuenta lo siguiente:

  • El valor por defecto el estilo display es flex. Los posibles valores de este estilo son: flex y none.
  • El valor por defecto del estilo flex-direction es column, al contrario de lo que ocurre en una aplicación web, es es row.
  • La unidad de medida que usaremos será px. Ni %, ni em, ni rem.
import React from 'react';
import { View } from 'react-native';
import styled from 'styled-components/native';

export const VerticalLayout = styled.View`
  flex-direction: column;
`;

Al usar styled-components en React native no puedes mezclarlos con estilos nativos en línea. El siguiente ejemplo sería incorrecto:

<MyStyledComponent style={{marginTop:'6px'}} />

Media queries

import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');

const isMobile = width <= 700;

export const ColLeft = styled.View`
    flex-direction:${isMobile ? 'column' : 'row'};
`;

Cargar tipografías

import React from 'react';
import {useFonts} from 'expo-font';
import AppLoading from 'expo-app-loading';
import QuicksandRegular from './assets/fonts/Quicksand-Light.ttf';
import {View, Text} from 'react-native';

export default () => {
    const [fontsLoaded] = useFonts({'QuicksandRegular': 'QuicksandRegular'});

    return !fontsLoaded ? <AppLoading/> : 
        <View
            style={{justifyContent: 'center', alignItems: 'center', flex: 1}}
        >
            <Text
                style={{fontFamily: 'QuicksandRegular', fontSize: 30}}
            >
                Pablo Monteserín
            </Text>
        </View>
}

Navigation

Stack Navigation

La stack navigation es similar a la que realizamos en nuestro navegador web mediante los botones de avanzar y retroceder página.

Imagina que tu aplicación móvil tiene tres páginas: A, B y C. Empiezas tu navegación en la página A. Pulsas un botón que te lleva a la página B. El stack queda así: B, A y C. Además, aparece el botón en pantalla de ir a la página anterior, en este caso, la página A. Si lo pulso, el stack quedaría así: A, B y C. Si pulso en ir a la página C, el stack quedaría así: C, A, B. Nuevamente, si pulso en ir a la página anterior, iríamos a la página A y la página C quedaría en la última posición. El stack quedaría así: A, B y C.

El stack es el contenedor donde se precargan las pantallas.

Instalación de las dependencias del core de react navigator:

expo install @react-navigation/native
expo install @react-native-masked-view/masked-view
expo install react-native-safe-area-context 
expo install react-native-gesture-handler

Instalación de las dependecias expecíficas de stack navigation:

expo install @react-navigation/stack

./App.js

import 'react-native-gesture-handler';
import React from 'react';
import Navigation from './src/navigation';

const App = () => <Navigation />

export default App;

./src/navigation/index.js

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import Main from './stacks/index'

export default () => <NavigationContainer><Main/></NavigationContainer>

./src/navigation/stacks/index.js

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import Home from '../../screens/home';
import Contact from '../../screens/contact';

const Stack = createStackNavigator();

export default () => (
    <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="Contact" component={Contact} />
    </Stack.Navigator>
);

./src/screens/Home.js

import React from 'react';
import { View, Text, Button } from "react-native";

export default ({ navigation, route }) =>
    <View>
        <Text>screen {route.name}</Text>
        <Button title="Change Screen" onPress={() =>
            navigation.navigate('Contact')
        }></Button>
    </View>
Descargar ejemplo de stack navigation

Con parámetros

Envío:

navigation.navigate('Contact', {
    param1: 86,
    param1: 'anything you want here',
})

Recogida:

console.log(routes.params)

Bottom Navigation

De momento vamos a crear un Botom navigation sin integrarlo con un Stack Navigation.

expo install @react-navigation/bottom-tabs

./src/navigation/index.js

import React from 'react';
import Bottom from './bottom/index';
import { View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';

export default () =>  <NavigationContainer><Bottom /></NavigationContainer>

./src/navigation/bottom/index.js

import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import BottomScreen1 from '../../screens/BottomScreen1';
import BottomScreen2 from '../../screens/BottomScreen2';

const Tab = createBottomTabNavigator();

export default () => (
    <Tab.Navigator>
        <Tab.Screen name="BottomScreen1" component={BottomScreen1}
            options={{
                title: 'BottomScreen1',
                //tabBarIcon: () => <Image source={img} />
            }}
        />
        <Tab.Screen name="BottomScreen2" component={BottomScreen2} />

    </Tab.Navigator>
);

Integrando Stack Navigation en Bottom Navigation

./src/navigation/bottom/index.js

import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import BottomScreen2 from '../../screens/BottomScreen2';
import Stacks from '../stacks/index';

const Tab = createBottomTabNavigator();

export default () => (
    <Tab.Navigator>
        <Tab.Screen name="BottomScreen1" component={Stacks}
            options={{
                title: 'BottomScreen1',
                //tabBarIcon: () => <Image source={img} />
            }}
        />
        <Tab.Screen name="BottomScreen2" component={BottomScreen2} />
    </Tab.Navigator>
);

Configuración de las cabeceras

<BottomTabs.Navigator
    screenOptions={{
        headerStyle: {
        backgroundColor: COLORS.primary // Color de fondo de la cabecera
    },
    headerShown: false, // Ocultar la cabecera
    tabBarShowLabel: true,
    tabBarStyles: styles.tabBar, //Estilos de la cabecera (utilizando Stylesheet)
    headerTintColor: COLORS.secondary, // Estilos del texto de la cabecera
    headerRight: () => (<TouchableOpacity onPress={logout}><Text>Log out</Text></TouchableOpacity>) // Mostramos contentenido en la parte derecha de la barra superior. No se verá si headerShown es false
    }}
>

Módulos útiles

React native vector icons

Permite incrustar iconos en nuestra aplicación pertenecientes a múltiples colecciones de iconos.

Esta es la lista de iconos disponibles con su correspondiente codigo que lo carga:

https://oblador.github.io/react-native-vector-icons/

expo install react-native-vector-icons
import Ionicons from 'react-native-vector-icons/Ionicons';
...
<Ionicons name="rocket" size={30} color="#900" />

https://www.npmjs.com/package/react-native-vector-icons

Expo Image Picker

Permite seleccionar imágenes de la galería de fotos.

https://www.npmjs.com/package/expo-image-picker

React native modal

Cargar un cuadro de diálogo.

Cargaremos una librería externa para hacerlo.

React native maps

Integra un mapa de google maps en nuestra aplicación.

https://www.npmjs.com/package/react-native-maps

Date Time Picker

Permite seleccionar fechas.

https://www.npmjs.com/package/@react-native-community/datetimepicker

Paquetes de elementos de la user Interface

Cómo utilizar variables de entorno en una aplicación creada con Expo

Instalaremos el siguiente módulo:

expo install react-native-dotenv

Añadiremos el plugin react-native-dotenv al fichero babel.config.js:

module.exports = function(api) {
api.cache(true);
return {
  presets: [
   'babel-preset-expo'
  ],
  plugins: [
    ["module:react-native-dotenv", {
      "moduleName": "@env",
      "path": ".env",
      "blacklist": null,
      "whitelist": null,
      "safe": false,
      "allowUndefined": true
  }]
 ]
};
};

Como siempre, definiremos las variables de entorno en el fichero .env:

AMOR = bueno

Finalmente, para importar las variables de entorno usaremos:

import {AMOR} from '@env'

Subida de imágenes a Firebase

Utilizaremos el expo-image-picker:

npm i expo-image-picker
import { useState,useContext} from 'react';
import { AppContext } from '../application/provider';
import { Button } from 'react-native';
import * as ImagePicker from 'expo-image-picker';

import {uploadImage} from '../services/users';

const ImagePickerExample = () => {

    const [image, setImage] = useState(null);

    const uploadImageAsync = async(uri) => {
        const blob = await new Promise((resolve, reject) => {
 
            const xhr = new XMLHttpRequest();
            xhr.onload = function () {
                resolve(xhr.response);
            };
            xhr.onerror = function (e) {
                console.log(e);
                reject(new TypeError("Network request failed"));
            };
            xhr.responseType = "blob";
            xhr.open("GET", uri, true);
            xhr.send(null);
        });
        return blob;
    }

    const pickImage = async () => {
        let result = await ImagePicker.launchImageLibraryAsync({
            mediaTypes: ImagePicker.MediaTypeOptions.Images,
            allowsEditing: true,
            aspect: [4, 3],
            quality: 1,
        });
        if(!result.cancelled){
            setImage(result.uri);
            const blob = await uploadImageAsync(result.uri);
            uploadImage(blob, 'ruta/subida/foto');
        };
    };
    return <>
        <Button title="Pick an image from camera roll" onPress={()=>pickImage()} />

    {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}

    </>
}; 

export default ImagePickerExample;

El método de uploadImage que utilizaremos, lo tenemos definido en la lección de firebase.

Geolocalización

import*as Location from'expo-location';
...
useEffect(()=>{
        (async()=>{
            let{status}=await Location.requestForegroundPermissionsAsync();
            if(status!=='granted'){
                setErrorMsg('Permission to access location was denied');
                return;
            }
            let location=await Location.getCurrentPositionAsync({});
            setGeo(location);
        })();
},[]);

Persistiendo la información

https://react-native-async-storage.github.io/async-storage/docs/install/

Podemos usar el siguiente componente para gestionar el valor:

import AsyncStorage from '@react-native-async-storage/async-storage';

export const storeValue = async (value, KEY) => {
    if (!KEY || typeof KEY !== 'string') return
    try {
        switch (typeof value) {
            case 'object':
                if (!Array.isArray(value))
                    value = JSON.stringify(value);
                break;

            case 'boolean':
                value = value ? 'true' : '';

            default:
                break;
        }
        await AsyncStorage.setItem(KEY, value);
    } catch (e) {
        return console.log("ERROR_STORING_VALUE")
    }
}

export const getStoredValue = async (KEY, type = '') => {
    if (typeof KEY !== 'string') return console.log("ERROR_GETTING_STORED_VALUE")
    try {
        let value = await AsyncStorage.getItem(KEY);
        if (value !== null) {
            switch (type) {
                case 'object':
                    value = JSON.parse(value);
                    break;
                case 'boolean':
                    value = value === 'true';
                    break;

                default:
                    break;
            }
            return value;
        }
    } catch (e) {
        return console.log("ERROR_GETTING_STORED_VALUE")
    }
}

export { AsyncStorage }

Trabajo con mapas: calcular distancias, ver si un punto cae dentro de cierto radio…

https://www.npmjs.com/package/geolib

Compilar

Para Android:

El siguiente comando tendrá soporte hasta junio del 2022 y funcionará hasta el 2023. Por tanto, deberíamos dejar de usarlo

expo build:android

En su lugar, añadiremos el siguiente comando al bloque de scripts del package.json:

"build": "eas build --platform android --profile preview"

Y lo ejecutaremos para compilar.

Por otro lado, en el momento de redactar este texto existe a una incompatibilidad con la última versión de firebase. Podemos solventar el problema añadiendo un fichero con el nombre metro.config.js  en la raíz de nuestro proyecto. Este fichero debe tener el siguiente código:

const { getDefaultConfig } = require("@expo/metro-config");

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.assetExts.push("cjs");

module.exports = defaultConfig;

Para Web:

expo build:web

Ejercicio React Native

Listado

Hacer una aplicación que cargue un array de de elementos en un FlatList y tenga un cuadro de texto que le permita al usuario podrá añadir nuevos elementos.

Tener en cuenta que para detectar cuando un texto cambia dentro de un input usaremos onChangeText en lugar de onChange.

<TextInput onChangeText={text => setTxt(text)} />
Curso de React Native 2

Cuando el usuario pulse sobre un elemento de la lista se le preguntará al usuario si desea borrar el elemento.

Curso de React Native 3

Panadería

1. Hacer una screen Home que lea el siguiente fichero de datos y pinte una rejilla utilizando el componente FLatList con cada uno de los elementos.

Curso de React Native 4

./data/categories.js

export const CATEGORIES = [
    {
        id: 1,
        name: 'Categoría 1',
        color: '#896978'
    },
    {
        id: 2,
        name: 'Categoría 2',
        color: '#839791'
    },
    {
        id: 3,
        name: 'Categoría 3',
        color: '#aac0af'
    },
    {
        id: 4,
        name: 'Categoría 4',
        color: '#896978'
    },
]

2. Utilizando Stack Navigation, al pulsar en uno de los elementos de Home, debemos navegar al Screen Productos de la categoría en la que haremos una consulta a Firebase para recuperar los productos correspondientes a la categoría seleccionada.

Puedes alimentar tu aplicación con productos consultando la api de mercadolibre y haciendo una insercción de productos en Firebase a partir de los datos recuperados.

Curso de React Native 5

3. Al pulsar sobre un producto iremos a una tercera screen en la que recuperaremos los datos del mismo.

Curso de React Native 6

4. También tendremos la opción de añadir el producto a un carrito. Gestionaremos este carrito utilizando Context API para almacenar los datos.

Para navegar al carrito utilizaremos Bottom Tab Navigator.

El bottom tab navigator tendrá dos menús. Uno enlzará al stack navigator que acabamos de hacer y otro al screen Cart.

5. Debemos tener un stack con dos screens: login y registro. Si el usuario no esta logueado, cargaremos el creen de login, y si no el stack de navegación que ya teníamos:

src/navigation/index.js
import React, { useEffect, useState } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import TabsNavigator from './bottomtabs/TabsNavigator';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '../app/firebase';
import AuthScreen from '../screens/AuthScreen';

const MainNavigator = () => {
    const [isLogged, setIsLogged] = useState(false);

    useEffect(() => {
        onAuthStateChanged(auth, user => {
            if (user) {
                console.log('user', user);
                const uid = user.uid;
                setIsLogged(true);
            } else {
                console.log("No user logged");
                setIsLogged(false);
            }
        });
    }, []);

    return (<NavigationContainer>
        {isLogged ?
            <TabsNavigator /> :
            <AuthScreen />
        }
    </NavigationContainer>)

}

export default MainNavigator;