Añade react-navigation a tu app de expo
La navegación es uno de los pilares básicos de cualquier aplicación móvil, en el post de hoy vamos a añadir a nuestra app de expo la navegación usando react-navigation, una de las mejores librerías y de las más utilizadas por la comunidad.
expo
Vamos a utilizar la típica arquitectura de una aplicación móvil, un tab-navigator sobre el cual cuando pulsamos por ejemplo en una tarjeta nos lleva a pantalla que se sitúa por encima de este.
Instalar react-navigation
Ejecutamos desde la raíz del proyecto lo necesario para instalar react-navigation
yarn add @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
yarn add @react-navigation/stack
yarn add @react-navigation/bottom-tabs
Estructura del navegador
El objetivo es crear un main stack-navigator que contendrá al tab-navigator y a cada una de las pantallas que iremos añadiendo a nuestra aplicación.
.
├── src
│ ├── navigation
│ │ ├── index.js
│ │ ├── MainStackNavigator.js
│ │ └── TabNavigator.js
│ ├── scenes
│ └── components
└── App.js
Al igual que hicimos con el botón vamos a crear un index.js, un MainStackNavigator.js y un TabNavigator.js.
Si no sabes de lo que hablo puedes visitar el post de Organziación del proyecto y buenas prácticas
Creando las pantallas
Antes de crear los navegadores vamos a crear tres pantallas básicas que compondrán nuestra aplicación, un home, una pantalla de búsqueda y un perfil.
├── src
│ ├── navigation
│ ├── scenes
│ │ ├── home
│ │ │ └── index.js
│ │ ├── search
│ │ │ └── index.js
│ │ └── profile
│ │ └── index.js
│ └── components
└── App.js
De momento van a ser pantallas básicas con un texto y un color de fondo pues vamos a centrarnos en la navegación.
import React from 'react';
import { SafeAreaView, StyleSheet, Text } from 'react-native';
export const HomeScreen = ({}) => {
return(
<SafeAreaView style={styles.container}>
<Text>Home</Text>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'red',
alignItems: 'center',
justifyContent: 'center',
},
});
De igual manera para la SeachScreen.js y la ProfileScreen.js
SafeAreaView es un componente para evitar que nuestra pantalla entre en la zona del notch en los iPhones.
Creando el Tab Navigator
Instanciamos el TabNavigator con unas opciones básicas: - Especificamos que la ruta inicial será el Home - No utilizaremos ningun header por el momento - La tab bar será un componente que realizaremos a continuación - Especificamos que el teclado tapa la tab bar - Por último decimos que cuando pulsamos el botón de atras no haga nada
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
export const TabNavigator = () => {
return (
<SafeAreaView style={ { flex: 1, backgroundColor: '#fff' }}>
<Tab.Navigator
initialRouteName='Home'
header={null}
headerMode='none'
tabBar={props => <TabBar {...props} />}
tabBarOptions= {{
keyboardHidesTabBar: true
}}
backBehavior={'none'}
>
</Tab.Navigator>
</SafeAreaView>
)
}
Ahora vamos a añadir nuestras pantallas, las importamos
import { HomeScreen } from '@scenes/home';
import { SearchScreen } from '@scenes/search';
import { ProfileScreen } from '@scenes/profile';
y las añadimos al tab navigator:
export const TabNavigator = () => {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
<Tab.Navigator
initialRouteName='Home'
header={null}
headerMode='none'
tabBar={props => <TabBar {...props} />}
tabBarOptions={{
keyboardHidesTabBar: true
}}
backBehavior={'none'}
>
<Tab.Screen
name='Home'
component={HomeScreen}
/>
<Tab.Screen
name='Search'
component={SearchScreen}
/>
<Tab.Screen
name='Profile'
component={ProfileScreen}
/>
</Tab.Navigator>
</SafeAreaView>
)
}
Por último tenemos que crear la TabBar, creamos una View con dirección horizontal y altura 64 cuando sea visible (ahora definiremos la variable visible no te preocupes) y con un borde superior.
const TabBar = ({ state, descriptors, navigation }) => {
return (
<View style={{
flexDirection: 'row',
backgroundColor: '#000',
maxHeight: visible ? 64 : 0,
borderTopWidth: 0.5,
borderTopColor: 'black'
}}>
</View>
);}
Faltaría crear cada uno de los botones que componen la TabBar, para elegir iconos te recomiendo visitar las librerías de iconos de expo y escoger los que más te gusten.
Añadimos el import de los paquetes de iconos elegidos, en mi caso los siguientes:
import { Ionicons, Entypo, FontAwesome } from '@expo/vector-icons';
Y los incorporamos dentro del componente TabBar:
const visible = true;
return (
<View style={{
flexDirection: 'row',
backgroundColor: '#000',
maxHeight: visible ? 64 : 0, borderTopWidth: 0.5,
borderTopColor: 'black'
}}>
{ state.routes.map((route, index) => {
const label = route.name;
const isFocused = state.index === index;
const onPress = () => {
if(!isFocused){
navigation.navigate(route.name);
}
};
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={1}
key={label}
style={[{
minHeight: 48,
flex: 1,
alignItems: 'center',
justifyContent: 'center', marginBottom: 2 }]}
>
{label === 'Home' && <Ionicons name="home" size={24} color="white" />}
{label === 'Search' && <Entypo name="magnifying-glass" size={24} color="white" />}
{label === 'Profile' && <FontAwesome name="user" size={24} color="white" />}
</TouchableOpacity>
);
})}
</View>
);
Vamos a explicar un poco el código que acabrás de copiar.
Iteramos sobre cada una de las pantallas que componen nuestro TabNavigator con states.routes.map y creamos un botón para cada uno de ellos. Le asociamos que cuando pulsamos naveguemos a la pantalla indicada siempre que no nos encontremos ya en ella lo cual controlamos con la opción isFocused y le asociamos el icono correspondiente usando la label.
Ocultar la barra de navegación cuando se muestra el teclado
Por último vamos a definir que pasa cuando el teclado se muestra en la aplicación. Para ello vamos a usar el hook de useEffect que se ejecuta cuando la pantalla se monta y el de useState que sirve para mantener el estado dentro de la pantalla.
Si no tienes muy claro para que sirven los hooks no te preocupes lo explicaremos en detalle en un post mas adelante o puedes visitar la documentación.
Añadimos dentro del componente de TabBar antes del return el siguiente código:
const [visible, setVisible] = useState(true);
const keyboardWillShow = () => {
setVisible(false);
};
const keyboardWillHide = () => {
setVisible(true);
};
useEffect(() => {
const keyboardWillShowSub = Keyboard.addListener(Platform.select({ android: 'keyboardDidShow', ios: 'keyboardWillShow' }), keyboardWillShow);
const keyboardWillHideSub = Keyboard.addListener(Platform.select({ android: 'keyboardDidHide', ios: 'keyboardWillHide' }), keyboardWillHide);
return () => {
keyboardWillShowSub.remove();
keyboardWillHideSub.remove();
}
}, [])
Añadimos un listener que hace que la TabBar se oculte cuando el teclado se muestra y que vuelva a aparecer cuando el teclado se oculta, utilizando para ello la variable de estado visible.
Nuestro TabNavigator.js debería tener un aspecto parecido a este:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons, Entypo, FontAwesome } from '@expo/vector-icons';
import { HomeScreen } from '@scenes/home';
import { SearchScreen } from '@scenes/search';
import { ProfileScreen } from '@scenes/profile';
const TabBar = ({ state, navigation }) => {
const [visible, setVisible] = useState(true);
const keyboardWillShow = () => {
setVisible(false);
};
const keyboardWillHide = () => {
setVisible(true);
};
useEffect(() => {
const keyboardWillShowSub = Keyboard.addListener(Platform.select({ android: 'keyboardDidShow', ios: 'keyboardWillShow' }), keyboardWillShow);
const keyboardWillHideSub = Keyboard.addListener(Platform.select({ android: 'keyboardDidHide', ios: 'keyboardWillHide' }), keyboardWillHide);
return () => {
keyboardWillShowSub.remove();
keyboardWillHideSub.remove();
}
}, [])
return (
<View style={{
flexDirection: 'row',
backgroundColor: '#000',
maxHeight: visible ? 64 : 0,
borderTopWidth: 0.5,
borderTopColor: 'black' }}
>
{ state.routes.map((route, index) => {
const label = route.name;
const isFocused = state.index === index;
const onPress = () => {
if(!isFocused){
navigation.navigate(route.name);
}
};
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={1}
key={label}
style={[{
minHeight: 48,
flex: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 2
}]}
>
{label === 'Home' && <Ionicons name="home" size={24} color="white" />}
{label === 'Search' && <Entypo name="magnifying-glass" size={24} color="white" />}
{label === 'Profile' && <FontAwesome name="user" size={24} color="white" />}
</TouchableOpacity>
);
})}
</View>
);
}
const Tab = createBottomTabNavigator();
export const TabNavigator = () => {
return (
<SafeAreaView style={{
flexGrow: 1,
backgroundColor: 'purple'
}}>
<Tab.Navigator
initialRouteName='Home'
header={null}
headerMode='none'
tabBar={props => <TabBar {...props} />}
tabBarOptions={{
keyboardHidesTabBar: true
}}
backBehavior={'none'}
>
<Tab.Screen
name='Home'
component={HomeScreen}
/>
<Tab.Screen
name='Search'
component={SearchScreen}
/>
<Tab.Screen
name='Profile'
component={ProfileScreen}
/>
</Tab.Navigator>
</SafeAreaView>
)
}
Como siempre es recomendable leer la documentación de React-navigation TabNavigator para continuar con el desarrollo.
Creando el Stack Navigator
Si esto te está pareciendo largo no te preocupes que la parte más difícil ya está hecha. Para el stack navigator solo tenemos que instanciarlo y añadirle lo que acabamos de hacer.
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { TabNavigator } from './TabNavigator';
const Stack = createStackNavigator();
export const MainStackNavigator = ({}) => {
return (
<Stack.Navigator
initialRouteName={'Tab'}
header={null}
headerMode='none'
mode={'card'}
>
<Stack.Screen
name='Tab'
component={TabNavigator}
/>
</Stack.Navigator>
)}
Le especificamos que no utilizamos header alguno y que la ruta inicial es la Screen que tiene como componente nuestro TabNavigator.
Añadir los navegadores a la aplicación
Desde el index.js de navigation vamos a crear el navegador de la aplicación.
import React from 'react';
import { NavigationContainer} from '@react-navigation/native';
import { MainStackNavigator } from './MainStackNavigator';
const RootNavigator = ({}) =>
return (
<NavigationContainer >
<MainStackNavigator />
</NavigationContainer>
)
}
export default RootNavigator;
Para que funcionen los navegadores tienen que estar contendos en un NavigationContainer. Ahora parace que no sirve para mucho pero hay muchas propiedades del navegador que se instancian desde el NavigationContainer como el comportamiento del Deep Linking pero ya lo veremos con calma en otro post.
Como es el único componente que queremos exportar desde aquí podemos hacer un export default.
Ahora vamos a nuestro App.js para añadirlo
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Navigator from './src/navigation';
export default function App() {
return (
<View style={styles.container}>
<Navigator />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
Es importarte eliminar del estilo las propiedades:
alignItems: 'center',
justifyContent: 'center'
Este debería ser el aspecto actual de tu aplicación. Si tienes alguna duda o quieres saber más de como funciona react-navigation tocará volver a leer documentación.
Añadir otra pantalla y un botón de navegación
Vamos a crear una pantalla de detalles que se llegue a través de un botón, en el futuro esto puede ser una tarjeta que te lleve a una nueva pantalla que contenga más información.
.
├── src
│ ├── navigation
│ ├── scenes
│ │ ├── home
│ │ │ └── index.js
│ │ ├── search
│ │ │ └── index.js
│ │ ├── profile
│ │ │ └── index.js
│ │ └── details
│ │ └── index.js
│ └── components
└── App.js
E incorporamos la pantalla al stack navigator
export const MainStackNavigator = ({}) => {
return (
<Stack.Navigator
initialRouteName={'Tab'}
header={null}
headerMode='none'
mode={'card'}
>
<Stack.Screen
name='Tab'
component={TabNavigator}
/>
<Stack.Screen
name='Details'
component={DetailsScreen}
/>
</Stack.Navigator>
)
}
Ahora en la pantalla del HomeScreen.js vamos a crear el botón para navegar a esta pantalla.
export const HomeScreen = ({navigation}) => {
return(
<SafeAreaView style={styles.container}>
<Text>Home</Text>
<Button
style={{ width: 200, heigh: 50, backgroundColor: 'white'}}
text='navigate'
onPress={() => navigation.navigate('Details')}
/>
</SafeAreaView>
)
}
Tenemos que hacer un pequeño cambio en nuestro Button.js para añadirle el onPress.
export const Button = ({text, style, onPress}) => {
return(
<TouchableOpacity style={{...style, ...styles.container}} onPress={onPress}>
<Text>{text}</Text>
</TouchableOpacity>
)
}
Y si has copiado bien el código y seguido todos los pasos debería estar funcionando.
Barra de navegación superior
Finalmente vamos a añadirle una barra de navegación superior para poder volver atrás entre otras cosas, vamos a añadírselo a las pantallas del MainStackNavigator.js.
Eliminamos de las props de stack navigator
header={null}
headerMode='none'
Y especificamos que el título de las pantallas este centrado mediante la prop de options del Stack.Screen.
export const MainStackNavigator = ({}) => {
return (
<Stack.Navigator
initialRouteName={'Tab'}
mode={'card'}
>
<Stack.Screen
name='Tab'
component={TabNavigator}
options={{
headerTitleAlign: 'center',
}}
/>
<Stack.Screen
name='Details'
component={DetailsScreen}
options={{
headerTitleAlign: 'center',
}}
/>
</Stack.Navigator>
)
}
Ahora como no queremos que el título de las pantallas del TabNavigator sea siempre el mismo vamos a utilizar la función getFocusedRouteNameFromRoute para escoger el título del TabNavigator dinamicamente en MainStackNavigator.js.
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';
const getHeaderTitle(route) = () => {
// If the focused route is not found, we need to assume it's the initial screen
// This can happen during if there hasn't been any navigation inside the screen
// In our case, it's "Home" as that's the first screen inside the navigator
const routeName = getFocusedRouteNameFromRoute(route) ?? 'Home';
switch (routeName) {
case 'Home':
return 'Home';
case 'Search':
return 'Search';
case 'Profile':
return 'Profile';
}
}
Y especificamos en la prop de option que el título se escoge con esta función.
<Stack.Screen
name='Tab'
component={TabNavigator}
options={({ route }) => ({
headerTitle: getHeaderTitle(route),
headerTitleAlign: true
})}
/>
Si quieres conocer todas las opciones siempre puedes visitar la documentación de react-navigation .
Modificar la animación de navegación
Te habrás dado cuenta que tu animación de cambio de pantaña y la que aparece en los videos es diferente. Esto es porque se me olvidó quitarla cuando estaba haciendo los videos para el post, pero bueno así tengo excusa para explicar como configurar la animación para que sea lateral. Para ello tenemos que modificar la propiedad screen options de nuestro stack navigator.
Configuramos el objeto de configuración:
const navigatorOptions = {
gestureDirection: 'horizontal',
cardStyleInterpolator: ({ current, layouts }) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
],
opacity: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
})
},
};
},
};
y lo añadimos a la prop de screen options
<Stack.Navigator
initialRouteName={'Tab'}
mode={'card'}
screenOptions={navigatorOptions}
>
<Stack.Screen
name='Tab'
component={TabNavigator}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name='Details'
component={DetailsScreen}
options={{
headerTitleAlign: 'center',
}}
/>
</Stack.Navigator>
Este post puede ser que no haya sido entretenido pero al menos espero que te haya resultado útil.
Nos vemos en el siguiente!