React Native – Differentiating Drawer vs Tab Navigator for Android vs iOS Application

By w s / September 3, 2017

With react native we can build applications easily for both Android and iOS users using an almost “identical” codebase.

While this is an advantage, there are design elements that we must take into considerations when designing for both OSes.

Navigation element is one of the biggest items to consider in designing application for both OSes. There are articles in tutsplus and medium which discuss the topic further.

We’ve also talked about this topic briefly in streetsmartdev while recalling things I wish I knew when starting off with mobile application development.

While iOS users are comfortable with tab bars as their navigation, Android users prefer drawer.

What are we building

Let’s build an application using react-native and react-navigation. This application will change its core navigational element depending on the OS it’s running in. While doing that, let’s try to get most of the screens and components shared!

The application would have 2 main routes which are external and internal routes.

External screens: contains all the screens for users who are not logged in.

Internal screens: contains the screens applicable to users who are logged in.

The main drawer or tab navigator will reside in the internal route.

Initializing the application

Check out debugging react native expo for basic react native setup if you have some issue getting up and running with react native.

Once we have the basic setup for react-native, we can go ahead and install react-navigation for our navigation elements.

react-native install react-navigation

Root Navigator

Let’s build the basic root navigator that gathers the internal and external screens together.

External Screens

The only screen we’ll create for external would be a LoginScreen.

LoginScreen contains a login button that would go to the internal screens on click. To do this, we’re calling navigator.navigate to “Internal” route.

import React from "react";
import {StyleSheet} from "react-native";
import {Button, Text, View} from "react-native";
class LoginScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.title}>React navigator - Login</Text>
<Button
onPress={() => this.props.navigation.navigate("Internal")}
title="Login"
/>
</View>
);
}
}
LoginScreen.navigationOptions = {
title: "Login"
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
title: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
export default LoginScreen;

Internal screens

We’ll start off the internal screens with home screen that contains nothing but a text.

import React from "react";
import {StyleSheet, Text, View} from "react-native";
class HomeScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.title}>React navigator - Home</Text>
</View>
);
}
}
HomeScreen.navigationOptions = {
title: "Home"
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
title: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
export default HomeScreen;

Stack navigator

Root navigator will take a shape of stack navigator that gathers both external and internal screens together.

import React from "react";
import {StackNavigator} from "react-navigation";
import LoginScreen from "./internal/Login";
import HomeScreen from "./external/Home";
export default StackNavigator(
{
External: {screen: LoginScreen},
Internal: {screen: HomeScreen}
},
{
initialRouteName: "Internal"
}
);

We also need to update our App.js to load up this root navigator.

import React from "react";
import RootNavigator from "./src/routes/RootNavigator";
export default class App extends React.Component {
render() {
return <RootNavigator />;
}
}

Now we have an external screen that goes to internal screen on a click of a button.

Nested stack navigator

With the root navigator in place, let’s start working with the drawer and tab navigator.

Let’s create another screen called About. This will allow us to switch to and from Home screen in the internal screen set.

import React from "react";
import {StyleSheet, Text, View} from "react-native";
class AboutScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.title}>React navigator - About</Text>
</View>
);
}
}
AboutScreen.navigationOptions = {
title: "About"
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
title: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
export default AboutScreen;

TabNavigator for iOS

For iOS, we’ll wrap Home and About screen with a TabNavigator.

Let’s create an index (index.ios.js) file for internal route that contains this TabNavigator.

import React from "react";
import {TabNavigator} from "react-navigation";
import HomeScreen from "./Home";
import AboutScreen from "./About";
export default TabNavigator(
{
Home: {screen: HomeScreen},
About: {screen: AboutScreen}
},
{
initialRouteName: "Home"
}
);

DrawerNavigator for Android

For Android, we’ll create a DrawerNavigator. Let’s create the android index file (index.android.js) in internal route.

For DrawerNavigator, we have to render the navbar menu handle as well. We will be using react-native-vector-icon baked into expo for the handle icon.

import React from "react";
import {Text, TouchableOpacity} from "react-native";
import {DrawerNavigator} from "react-navigation";
import HomeScreen from "./Home";
import AboutScreen from "./About";
import {Ionicons} from '@expo/vector-icons';
const Internal = DrawerNavigator(
{
Home: {screen: HomeScreen},
About: {screen: AboutScreen}
},
{
initialRouteName: "Home"
}
);
Internal.navigationOptions = ({navigation}) => ({
headerLeft: (
<TouchableOpacity
style={{paddingHorizontal: 20}}
onPress={() => {
//https://reactnavigation.org/docs/navigators/drawer
//Index is 0 when drawer closed -> https://stackoverflow.com/a/45122848
if (navigation.state.index === 0) {
navigation.navigate("DrawerOpen");
} else {
navigation.navigate("DrawerClose");
}
}}
>
<Ionicons name="md-menu" size={32}/>
</TouchableOpacity>
)
});
export default Internal;

 

Adjusting the screens and RootNavigator

With the navigators created, let’s tune our internal screens, external screens and adjust our RootNavigator

Adjusting internal screens navigationOption

To have a good representation of the menu, we would like to add icons for our internal screens.

For tab bar, we would need to add the navigationOption tabBarIcon, and for drawer it’s drawerIcon.

Since we are deciding this by the OS, we can use Platform.select provided by react-native.

Let’s create a navigationHelper.js to help us with the configuration and update our navigationOption in Home.js and About.js.

// NavigationHelper.js -----------------------------------------------
import React from "react";
import {Platform} from "react-native";
import {Ionicons} from '@expo/vector-icons';
export const getNavigationOption = (title, icon) => ({
title,
...Platform.select({
ios: {
tabBarIcon: ({tintColor}) => (
<Ionicons name={`ios-${icon}`} size={32} color={tintColor}/>
)
},
android: {
drawerIcon: ({tintColor}) => (
<Ionicons name={`md-${icon}`} size={32} color={tintColor}/>
)
}
})
});
// Home.js -------------------------------------------------------------
...
HomeScreen.navigationOptions = getNavigationOption("Home", "home");
...
// About.js ------------------------------------------------------------
...
AboutScreen.navigationOptions = getNavigationOption("About", "person");
...

 

Reset the navigation stack going into internal screens

We would also like to reset the navigation stack when user clicks on login button from external.

We want to do this to reset the stack back to 0 and prevent the back button from showing up in our tab or drawer screens.

We can do this by passing reset navigation action in the Login component instead of simply navigating.

We also want to ensure that login component does not render any header by doing header: null on navigationOption.

...
import {NavigationActions} from "react-navigation";
class LoginScreen extends React.Component {
render() {
const reset = NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: "Internal" })]
});
return (
...
<Button
onPress={() => this.props.navigation.dispatch(reset)}
title="Login"
/>
...
);
}
}
LoginScreen.navigationOptions = {
title: "Login",
header: null //header is null to hide header
};
...

 

Updating RootNavigator

The last thing in our list is to update the RootNavigator to load up proper internal screens.

Change the import of Internal component to index, by importing the folder. React-native will automatically pick the correct file depending on the OS.

import React from "react";
import {StackNavigator} from "react-navigation";
import LoginScreen from "./external/Login";
import Internal from "./internal";
export default StackNavigator(
{
External: {screen: LoginScreen},
Internal: {screen: Internal}
},
{
initialRouteName: "External"
}
);

 

That’s all folks! We now have an application that switches between tab and drawer navigator depending on the OS used.

The full code can be found at https://github.com/streetsmartdev/rn-navigation-ios-and-android