Handling responsive layouts in React Native apps

Prerequisites

For this tutorial, you need a basic knowledge of React Native and some familiarity with Expo. You’ll also need the Expo client installed on your mobile device or a compatible simulator installed on your computer Instructions on how to do this can be found here.

Throughout this tutorial, we’ll be using yarn. If you don’t have yarn already installed, then install it here.

Also, make sure you’ve already installed expo-cli.

If it’s not installed already, then go ahead and install it:

To make sure we’re on the same page, the following are the versions used in this tutorial –

  • Node v12.6.0
  • yarn 1.17.3
  • expo 3.0.9

Make sure to update expo-cli if you haven’t updated it in a while, since expo releases are quickly out of date.

To create a new project using expo-cli, type the following in the terminal:

It will then prompt you to choose a template. You should choose blank and hit Enter.

Then it will ask you to name the project—type responsive-react-native-layout and hit Enter again.

Lastly, it will ask you to press Y to install dependencies with yarn or n to install dependencies with npm. Press y.

This will create a folder called responsive-react-native-layout, which will bootstrap a new Expo project.

Now go ahead and change the directory to responsive-react-native-layout using the following command:

Type the following to start running the app:

Press i to run the iOS Simulator. This will automatically run the iOS Simulator even if it’s not opened.

Press a to run the Android Emulator. Note that the emulator must be installed and started already before typing a. Otherwise, it will throw an error in the terminal.

Problem with responsive layout in React Native

When you make a React Native app, you make it work with a device of one dimension. As such, when you open the same app in another device of another dimension, it looks distorted. What happens in these cases, is those different devices have different pixel ratios.

React Native accepts either percentages or density-independent pixels (DP) for its styles. Percentages work well on the web, but they don’t support all the style properties in React Native. Only padding, margin, width, height, minWidth, minHeight, maxWidth, maxHeight, flexBasis can have percentage values according to this commit.

Density independent pixels (DP) are different than traditional pixels. The bigger the device, the more DP it has. However, density-independent pixels (DP) vary from device to device, so it cannot be directly used for creating a responsive layout.

Solution using react-native-responsive-screen

react-native-responsive-screen is a package that makes constructing a responsive layout very easy.

The whole code for the package is as follows:

// packages
import { Dimensions, PixelRatio } from 'react-native';

// Retrieve initial screen's width
let screenWidth = Dimensions.get('window').width;

// Retrieve initial screen's height
let screenHeight = Dimensions.get('window').height;

/**
 * Converts provided width percentage to independent pixel (dp).
 * @param  {string} widthPercent The percentage of screen's width that UI element should cover
 *                               along with the percentage symbol (%).
 * @return {number}              The calculated dp depending on current device's screen width.
 */
const widthPercentageToDP = widthPercent => {
  // Parse string percentage input and convert it to number.
  const elemWidth = typeof widthPercent === "number" ? widthPercent : parseFloat(widthPercent);

  // Use PixelRatio.roundToNearestPixel method in order to round the layout
  // size (dp) to the nearest one that correspons to an integer number of pixels.
  return PixelRatio.roundToNearestPixel(screenWidth * elemWidth / 100);
};

/**
 * Converts provided height percentage to independent pixel (dp).
 * @param  {string} heightPercent The percentage of screen's height that UI element should cover
 *                                along with the percentage symbol (%).
 * @return {number}               The calculated dp depending on current device's screen height.
 */
const heightPercentageToDP = heightPercent => {
  // Parse string percentage input and convert it to number.
  const elemHeight = typeof heightPercent === "number" ? heightPercent : parseFloat(heightPercent);

  // Use PixelRatio.roundToNearestPixel method in order to round the layout
  // size (dp) to the nearest one that correspons to an integer number of pixels.
  return PixelRatio.roundToNearestPixel(screenHeight * elemHeight / 100);
};

/**
 * Event listener function that detects orientation change (every time it occurs) and triggers 
 * screen rerendering. It does that, by changing the state of the screen where the function is
 * called. State changing occurs for a new state variable with the name 'orientation' that will
 * always hold the current value of the orientation after the 1st orientation change.
 * Invoke it inside the screen's constructor or in componentDidMount lifecycle method.
 * @param {object} that Screen's class component this variable. The function needs it to
 *                      invoke setState method and trigger screen rerender (this.setState()).
 */
const listenOrientationChange = that => {
  Dimensions.addEventListener('change', newDimensions => {
    // Retrieve and save new dimensions
    screenWidth = newDimensions.window.width;
    screenHeight = newDimensions.window.height;

    // Trigger screen's rerender with a state update of the orientation variable
    that.setState({
      orientation: screenWidth < screenHeight ? 'portrait' : 'landscape'
    });
  });
};

/**
 * Wrapper function that removes orientation change listener and should be invoked in
 * componentWillUnmount lifecycle method of every class component (UI screen) that
 * listenOrientationChange function has been invoked. This should be done in order to
 * avoid adding new listeners every time the same component is re-mounted.
 */
const removeOrientationListener = () => {
  Dimensions.removeEventListener('change', () => {});
};

export {
  widthPercentageToDP,
  heightPercentageToDP,
  listenOrientationChange,
  removeOrientationListener
};

The two most important functions we need for making a responsive layout are widthPercentageToDP and heightPercentageToDP.

widthPercentageToDP takes in a percentage of a screen’s width that a UI element should cover and returns a calculated, DP depending on the current device’s screen width.

Similarly, heightPercentageToDP takes in a percentage of a screen’s height that a UI element should cover and returns a calculated DP, depending on the current device’s screen height.

Consider an example where the device has a width of 480 DP. If we do

then it will be translated to

because 80% of 480 = (80/100) * 480 = 384 DP

Consider another example: If a device has a width of 390 DP

then it will be translated to

because 80% of 390 = (80/100) * 390 = 312 DP.

So no matter what the device width is, the above View will translate it to 80% of its device’s width. Similarly, if you use heightPercentageToDP instead of widthPercentageToDP, then it will translate accordingly to the device’s height. This way, UI elements scale up and down depending on device resolutions.

Implementation

To show how this works in practice, we’re going to build a chat app that looks like this:

On a Phone (iPhone X)

On a Tablet (iPad Air 2)

Let’s go ahead and install react-native-responsive-screen first. Open up your terminal and type:

Also, download pikachu_heart_eyes.png and paste it into assets/ folder.

Now open App.js and add the following:

import React from 'react'
import { Image, SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native'
import {
 heightPercentageToDP as hp,
 widthPercentageToDP as wp,
} from 'react-native-responsive-screen'

export default function App() {
 return (
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <Image
        source={require('./assets/pikachu_heart_eyes.png')}
        style={styles.img}
        />
      </ScrollView>
    </SafeAreaView>
 )
}


const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
  },
  img: {
    width: wp('70%'),
    height: hp('30%'),
    alignSelf: 'flex-end',
    resizeMode: 'contain',
  },
})

Here, we have SafeAreaView, which renders content within the safe area boundaries of a device. Inside that, we have ScrollView, which allows us to scroll content if it’s beyond the boundaries of a device. And then we’ve added an Image tag showing a Pikachu image.

We’ve also imported heightPercentageToDP and widthPercentageToDP and aliased them as hp and wp respectively.

Then we’ve used them in the StyleSheet as width: wp(‘70%’) and height: hp(‘30%’). This means that the Pikachu image will always be 70% of the device’s width and 30% of the device’s height.

Now, let’s add the chats to our app.

Go ahead and put the following dummy chats above the default export App:

const chats = [
 { name: 'Prem', text: 'My God in heaven, you look like an angel' },
 { name: 'Pooja', text: 'Are you for real?' },
 { name: 'Prem', text: 'You have the best pics I have ever seen' },
 { name: 'Prem', text: 'Everything about you is perfect' },
 { name: 'Pooja', text: 'OMG ahaha, thank you so so much 🙌' },
 { name: 'Prem', text: 'I dont want your thank you & all' },
 { name: 'Pooja', text: 'So what do you want?' },
 { name: 'Prem', text: 'Plz just teach me how to edit pics 🙏' },
 { name: 'Pooja', text: 'Go to hell 😡' },
 { name: 'Prem', text: 'Are you there?' },
 { name: 'Prem', text: 'Did you just block me? 😅' },
]

Now paste the following below the Image tag:

{chats.map((chat, i) => (
  <Text
    key={i}
    style={[
      styles.commonChat,
      chat.name === 'Prem' ? styles.rightChat : styles.leftChat,
    ]}
  >
    <Text style={styles.username}>@{chat.name}: </Text>
    {chat.text}
  </Text>
))}

Here, we loop over the chats and style the chats appropriately.

Also, add the styles in the StyleSheet below img:

commonChat: {
 color: 'white',
 borderRadius: 10,
 borderWidth: 1,
 borderColor: 'white',
 overflow: 'hidden',
 padding: 10,
 margin: 10,
 fontSize: hp('1.6%'),
},
leftChat: {
 backgroundColor: '#c83660',
 alignSelf: 'flex-start',
},
rightChat: {
 backgroundColor: 'rebeccapurple',
 alignSelf: 'flex-end',
},
username: {
 fontWeight: 'bold',
},

The only thing regarding react-native-responsive-screen is the fontSize property in commonChat styles. The font size is 1.6% of the device’s height.

The padding, margin, borderRadius, and borderWidth are simple density-independent pixels (DP). However, they can also be added as a variant of hp and wp.

hp and wp can be used for any style property that accepts DP as a value. DP values are the ones of type number over the props mentioned in RN docs: View style props, Text style props, Image style props, Layout props, and Shadow props.

Finally, the App.js file should look like this:

import React from 'react'
import { Image, SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native'
import {
  heightPercentageToDP as hp,
  widthPercentageToDP as wp,
} from 'react-native-responsive-screen'

const chats = [
  { name: 'Prem', text: 'My God in heaven, you look like an angel' },
  { name: 'Pooja', text: 'Are you for real?' },
  { name: 'Prem', text: 'You have the best pics I have ever seen' },
  { name: 'Prem', text: 'Everything about you is perfect' },
  { name: 'Pooja', text: 'OMG ahaha, thank you so so much 🙌' },
  { name: 'Prem', text: 'I dont want your thank you & all' },
  { name: 'Pooja', text: 'So what do you want?' },
  { name: 'Prem', text: 'Plz just teach me how to edit pics 🙏' },
  { name: 'Pooja', text: 'Go to hell 😡' },
  { name: 'Prem', text: 'Are you there?' },
  { name: 'Prem', text: 'Did you just block me? 😅' },
]

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <ScrollView>
        <Image
          source={require('./assets/pikachu_heart_eyes.png')}
          style={styles.img}
        />
        {chats.map((chat, i) => (
          <Text
            key={i}
            style={[
              styles.commonChat,
              chat.name === 'Prem' ? styles.rightChat : styles.leftChat,
            ]}
          >
            <Text style={styles.username}>@{chat.name}: </Text>
            {chat.text}
          </Text>
        ))}
      </ScrollView>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
  },
  img: {
    width: wp('70%'),
    height: hp('30%'),
    alignSelf: 'flex-end',
    resizeMode: 'contain',
  },
  commonChat: {
    color: 'white',
    borderRadius: 10,
    borderWidth: 1,
    borderColor: 'white',
    overflow: 'hidden',
    padding: 10,
    margin: 10,
    fontSize: hp('1.6%'),
  },
  leftChat: {
    backgroundColor: '#c83660',
    alignSelf: 'flex-start',
  },
  rightChat: {
    backgroundColor: 'rebeccapurple',
    alignSelf: 'flex-end',
  },
  username: {
    fontWeight: 'bold',
  },
})

Now go ahead and open your app on different devices to see how it scales up and down depending on the device size. Notice how the Pikachu image remains 70% wide and 30% high. The font size of the chat should also remain 1.6% of the height of the device.

A best practice is to start developing the layout from bigger devices like tablets and then work your way down to smaller devices like phones. This way, it becomes easy to make UI elements responsive quickly. The only properties that need to be changed to make them responsive are the ones with type number.

Complete code for this tutorial can be found over at GitHub:

Conclusion

In this tutorial, we learned about the problems encountered with responsive layouts and how to solve them. Then, we learned about the package react-native-responsive-screen, which makes building responsive layouts easy.

We finally used the exposed methods from react-native-responsive-screen for all of the type number properties used in our app to make our app fully responsive to all screen sizes.

Now making responsive layouts should be a piece of cake.

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square