Curso de React Native

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

npx create-expo-app .

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 sistema.

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

Esto evitarárá el típico error:

ADB no se reconoce como un comando interno o externo.

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} />

Scroll

import { SafeAreaView, ScrollView} from 'react-native';

<SafeAreaView style={{ flex: 1 }}>
   <ScrollView>
      ... Contenido con el que quieres hacer scroll
   </ScrollView>
</SafeAreaView>

Button

<Pressable onPress={() => alert('pulsado')} style={{
        backgroundColor: 'gray',
        padding: 10,
        margin: 10,
        borderRadius: 10,
        shadowColor: 'black',
        // sombreado en Android
        shadowOpacity: 0.5,
        elevation: 3,
        // sombreado en iOS
        shadowOffset: { width: 0, height: 2 },
        shadowRadius: 10,
      }}>
        <Text style={{ color: 'white', textAlign: 'center' }}>Aquí no debería poner la etiqueta Button</Text>
</Pressable>

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.

import { FlatList} from 'react-native';

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

Para hacer que el contenido del FlatList tome el 100% del ancho, usaremos:

contentContainerStyle={{ flex: 1, justifyContent: 'center', alignItems: "stretch" }}

Para hacer un FlatList con columnas, usaremos:

numColumns={2}

Configuración de firebase en expo

En este enlace tienes una configuración adicional que debes codificar en el firebase.js para mantener los datos del usuario logueado cuando utilizas firebase con expo:

https://github.com/expo/fyi/blob/main/firebase-js-auth-setup.md

Debemos instalar el siguiente módulo:

npx expo install @react-native-async-storage/async-storage

Nuestra configuración quedaría así:

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth, setPersistence, browserLocalPersistence, initializeAuth, getReactNativePersistence } from 'firebase/auth';
import AsyncStorage from "@react-native-async-storage/async-storage";

const firebaseConfig = {
    // ...
};

const firebaseApp = initializeApp(firebaseConfig);
export const db = getFirestore();

export const auth = initializeAuth(firebaseApp, {
    persistence: getReactNativePersistence(AsyncStorage),
});

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 { 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

npx expo install expo-font
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

Ten en cuenta que es probable que esta navegación de problemas cuando la ejecutemos en en el navegador web.

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

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

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.

Si una navegación esta vinculada a una pantalla (como la stack navitation que estamos creando en este paso), la carpeta de navegación estará dentro de la carpeta de la pantalla. Si una navegación es global (como la bottom navigation que crearemos más adelante), estará en la carpeta navigation.

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

npx expo install @react-navigation/stack

./App.js

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

const App = () => <Navigation />

export default App;

./src/navigation/index.js

import { NavigationContainer } from '@react-navigation/native';
import StackNav from './stacks'

const Navigation = () => <NavigationContainer><StackNav/></NavigationContainer>

export default Navigation;

./src/screens/home/screens/index.js

import { createStackNavigator } from '@react-navigation/stack';
import Home from './Home';
import Page2 from './Page2';

const Stack = createStackNavigator();

const Navigation = () => (
    <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
        <Stack.Screen name="Home2" component={Home2} />
    </Stack.Navigator>
);

export default Navigation;

./src/screens/Home.js

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

const Home = ({ navigation, route }) => (
    <View>
        <Text>screen {route.name}</Text>
        <Button title="Change Screen" onPress={() =>
            navigation.navigate('Home2')
        }></Button>
    </View>
)

export default Home;
Descargar ejemplo de stack navigation

Paso de parámetros

Envío:

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

Recogida:

const { param1, param2 } = route.params;

Bottom Navigation

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

npx expo install @react-navigation/bottom-tabs

./src/navigation/index.js

import BottomTab from './bottom';
import { NavigationContainer } from '@react-navigation/native';

const Navigation = () =>  <NavigationContainer><BottomTab /></NavigationContainer>
export default Navigation;

./src/navigation/bottom/index.js

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Home from '../../screens/Home';
import Page2 from '../../screens/Page2';

const BottomTab = createBottomTabNavigator();

const BottomNav = () => (
    <BottomTab.Navigator>
        <BottomTab.Screen name="Nombre Screen 1" component={Home} />
        <BottomTab.Screen name="Nombre Screen 2" component={Page2} />
    </BottomTab.Navigator>
);

export default BottomNav;

Integrando Stack Navigation en Bottom Navigation

Simplemente, modificaremos el componente que se cargará en el Tab.Screen que codificamos en el punto anterior y cargaremos nuestro componente de Stacks.

./src/navigation/bottom/index.js

<BottomTab.Screen name="Nombre Screen 1" component={StackNav}    

Configuración de las cabeceras

Tenemos dos opciones:

  • Modificar el componente BottomTabs para obtener un comportamiento global de todas las screens.
  • Modificar el Tab.Screen que nos interese en cada caso.
const BottomNav = () => (
    <BottomTab.Navigator {...BottomConfig}>
        <BottomTab.Screen {...HomeConfig} />
        <BottomTab.Screen {...Page2Config} />
    </BottomTab.Navigator>
);

./src/navigation/bottom/bottom-config.js

const BottomConfig = {
    screenOptions: {
        headerStyle: {
            backgroundColor: '#0000ff' // Color de fondo de la cabecera
        },
        //headerShown: false, // Ocultar la cabecera
        headerTintColor: '#ff0000', // Estilos del texto de la cabecera
        headerRight: () => (<Pressable onPress={ () => {}}><Text>Log out</Text></Pressable>) // Mostramos contentenido en la parte derecha de la barra superior. No se verá si headerShown es false
    },
};

export default BottomConfig;

En las pantallas individuales, para establecer la configuración, utilizaremos options en lugar de screenoptions.

./src/screen/Home/screen-config.js

import Home from ".";

const HomeConfig = {
    name: "BottomScreen1",
    component: Home,
    options: {
        headerTitle: 'BottomScreen1',
        //headerShown: false, // Ocultar la cabecera
        //tabBarIcon: () => <Image source={img} />
    }
}

export default HomeConfig;

./src/screen/Page2/screen-config.js

import Page2 from ".";

const Page2Config = {
    name: "BottomScreen2",
    component: Page2
}

export default Page2Config;

Módulos útiles

React native vector icons

npx expo install react-native-vector-icons

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

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

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

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

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

expo-av

Permite cargar un video en el background.

npx expo install expo-av

https://docs.expo.dev/versions/latest/sdk/av/

React native svg

npx expo install react-native-svg

Documentación.

Expo Image Picker

npx expo install expo-image-picker

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

https://docs.expo.dev/versions/latest/sdk/imagepicker/

React native modal

npx expo install react-native-modal

Cargaremos una librería externa para hacerlo.

React native maps

npx expo install react-native-maps

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

https://docs.expo.dev/versions/latest/sdk/map-view/

Date Time Picker

npx expo install @react-native-community/datetimepicker

Permite seleccionar fechas.

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

Paquetes de elementos de la user Interface

Expo camera

npx expo install expo-camera

./app.json

{
     "expo": {
     ...
          "plugins": [
               [
                    "expo-camera",
                    {
                         "cameraPermission": "Allow to access your camera."
                    }
               ]
          ]
     }
}

./src/components/CameraComponent.js

import { Camera, CameraType } from 'expo-camera';
import { useState, useRef } from 'react';
import { Button, StyleSheet, Text, Pressable, View } from 'react-native';

const CameraComponent = () => {
    const camera = useRef(null)
    const [type, setType] = useState(CameraType.back);
    const [permission, requestPermission] = Camera.useCameraPermissions();

    if (!permission?.granted) {
        // Camera permissions are not granted yet
        return (
            <View style={styles.container}>
                <Text style={{ textAlign: 'center' }}>We need your permission to show the camera</Text>
                <Button onPress={requestPermission} title="grant permission" />
            </View>
        );
    }

    const toggleCameraType = () => {
        setType(current => (current === CameraType.back ? CameraType.front : CameraType.back));
    }

    const takePhoto = async () => {
        if (!camera.current) return;
        const photo = await camera.current.takePictureAsync();
        console.log(photo)
    }

    return (
        <View style={styles.container}>
            <Camera style={styles.camera} type={type} ref={camera}>
                <View style={styles.buttonContainer}>
                    <Pressable style={styles.button} onPress={toggleCameraType}>
                        <Text style={styles.text}>Flip Camera</Text>
                    </Pressable>

                    <Pressable style={styles.button} onPress={takePhoto}>
                        <Text style={styles.text}>Take Photo</Text>
                    </Pressable>
                </View>
            </Camera>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        backgroundColor: 'red',
    },
    camera: {
        flex: 1,
    },
    buttonContainer: {
        flex: 1,
        flexDirection: 'row',
        backgroundColor: 'transparent',
        margin: 64,
    },
    button: {
        flex: 1,
        alignSelf: 'flex-end',
        alignItems: 'center',
    },
    text: {
        fontSize: 24,
        fontWeight: 'bold',
        color: 'white',
    },
});

export default CameraComponent;

./src/screens/Home.js

import { View } from 'react-native';
import CameraComponent from './CameraComponent';

const App = () => <View style={{ flex: 1 }}><CameraComponent /></View >

export default App;

https://www.npmjs.com/package/expo-camera

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

Utilizaremos la siguiente sintaxis para acceder a una variable de entorno definida en el fichero .env:

process.env.EXPO_PUBLIC_API_KEY // No te olvides del prefijo EXPO_PUBLIC_

Subida de imágenes a Firebase

Utilizaremos el expo-image-picker:

npx expo install 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';

// El método de uploadImage que utilizaremos, lo tenemos definido en la lección de firebase.    
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;

Geolocalización

npx expo install expo-location
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

npx expo install @react-native-async-storage/async-storage

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

Podemos usar el siguiente componente para gestionar el valor. Tener en cuenta que debemos almacenar texto (podemos convertir un objeto en texto con JSON.stringify) y que la clave con la que guardamos el valor también debe ser texto.

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

export const storeValue = async (KEY, value) => {
    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:

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

npm install -g eas-cli

"scripts": {
    ...
    "eas-build-dev": "npx eas-cli build --platform android --profile development",
    "eas-build-prev": "npx eas-cli build --platform android --profile preview",
    "eas-build-pro": "npx eas-cli build --platform android --profile production"
}

Ejecutamos en la terminal el comando:

eas login

Debemos tener el siguiente fichero en la raíz del servidor:

eas.json
{
  "cli": {
    "version": ">= 3.2.1"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal"
    },
    "production": {}
  },
  "submit": {
    "production": {}
  }
}

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.

Para pasar parámetros de una pantalla a otra, puedes consultar la documentación que vimos anteriormente.

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;