How to do it...

If we want our app to adapt, we have to be able to answer several questions in our code:

  • How can we tell if the device is a tablet or a handset?
  • How can we learn if it's in the portrait or landscape modes?
  • How do we code a component that will render differently depending on the device type?
  • How can we make a component redraw itself automatically upon a screen orientation change?

Let's go over all these questions now. Let's first look at how we can learn about the device type and orientation. RN includes an API, Dimensions, that provides data that's necessary to render the app, such as the screen dimensions. How can we, then, learn the device type and orientation? The second question is easier: since there are no square devices (at least so far!), it's enough to see which of the two dimensions is bigger—if the height is bigger, then the device is in portrait mode, and otherwise it's in landscape mode. 

The first question, however, is harder. There's no strict rule that defines, in terms of screen sizes, where handsets end and where tablets start, but if we look at information on devices and calculate form factors (the ratio of the longest side to the shortest side), a simple rule appears: if the calculated ratio is 1.6 or below, it's more likely a tablet, and higher ratios suggest handsets.

If you need more specific data, check http://iosres.com/ for information on iOS devices, or https://material.io/tools/devices and http://screensiz.es for a larger variety of devices, in particular for Android, which is used on devices with a much greater variety of screen sizes.

With the following code, we basically return all the information provided by Dimensions, plus a couple of attributes (.isTablet and .isPortrait) to simplify the coding:

// Source file: src/adaptiveApp/device.js

/* @flow */

import { Dimensions } from "react-native";

export type deviceDataType = {
isTablet: boolean,
isPortrait: boolean,
height: number,
width: number,
scale: number,
fontScale: number
};

export const getDeviceData = (): deviceDataType => {
const { height, width, scale, fontScale } = Dimensions.get("screen");

return {
isTablet: Math.max(height, width) / Math.min(height, width) <= 1.6,
isPortrait: height > width,
height,
width,
scale,
fontScale
};
};

Using the preceding code, we have all we'd need to draw a view in a manner that is suitable for all kinds of devices, sizes, and both possible orientations—but how would we use this data? Let's look at this now, and make our app adjust properly in all cases.

For more on the Dimensions API, read https://facebook.github.io/react-native/docs/dimensions.

We could directly use the information provided by getDeviceData() in our components, but that would pose some problems:

  • The components would not be as functional as before, because they would have a hidden dependency in the function
  • As a result, testing components would then become a bit harder, because we'd have to mock the function
  • Most importantly, it wouldn't be so easy to set the components to re-render themselves when the orientation changes

The solution for all of this is simple: let's put the device data in the store, and then the relevant components (meaning those that need to change their way of rendering) can be connected to the data. We can create a simple component to do this:

// Source file: src/adaptiveApp/deviceHandler.component.js

/* @flow */

import React from "react";
import PropTypes from "prop-types";
import { View } from "react-native";

class DeviceHandler extends React.PureComponent<{
setDevice: () => any
}> {
static propTypes = {
setDevice: PropTypes.func.isRequired
};

onLayoutHandler = () => this.props.setDevice();

render() {
return <View hidden onLayout={this.onLayoutHandler} />;
}
}

export { DeviceHandler };

The component won't be seen onscreen, so we can add it to our main view anywhere. Connecting the component is the other necessary step; when the onLayout event fires (meaning the device's orientation has changed), we'll have to dispatch an action:

// Source file: src/adaptiveApp/deviceHandler.connected.js

/* @flow */

import { connect } from "react-redux";

import { DeviceHandler } from "./deviceHandler.component";
import { setDevice } from "./actions";

const getDispatch = dispatch => ({
setDevice: () => dispatch(setDevice())
});

export const ConnectedDeviceHandler = connect(
null,
getDispatch
)(DeviceHandler);

Of course, we need to define both the actions and the reducer, as well as the store. Let's look at how to do this—we'll begin with the actions. The very minimum we'd need (apart from other actions needed by our hypothetical app) would be as follows:

// Source file: src/adaptiveApp/actions.js

/* @flow */

import { getDeviceData } from "./device";

import type { deviceDataType } from "./device"

export const DEVICE_DATA = "device:data";

export type deviceDataAction = {
type: string,
deviceData: deviceDataType
};

export const setDevice = (deviceData?: object) =>
({
type: DEVICE_DATA,
deviceData: deviceData || getDeviceData()
}: deviceDataAction);

/*
A real app would have many more actions!
*/

We are exporting a thunk that will include the deviceData in it. Note that by allowing it to be provided as a parameter (or a default value being used instead, created by getDeviceData()), we will simplify testing; if we wanted to simulate a landscape tablet, we'd just provide an appropriate deviceData object.

Finally, the reducer would look like the following (obviously, for a real app, there would be many more actions!):

// Source file: src/adaptiveApp/reducer.js

/* @flow */

import { getDeviceData } from "./device";

import { DEVICE_DATA } from "./actions";

import type { deviceAction } from "./actions";

export const reducer = (
state: object = {
// initial state: more app data, plus:
deviceData: getDeviceData()
},
action: deviceAction
) => {
switch (action.type) {
case DEVICE_DATA:
return {
...state,
deviceData: action.deviceData
};

/*
In a real app, here there would
be plenty more "case"s
*/

default:
return state;
}
};

So, now that we have our device information in the store, we can study how to code adaptive, responsive components.

We can see how to code adaptive and responsive components by using a very basic component that simply displays whether it's a handset or a tablet, and its current orientation. Having access to all of the deviceData objects means that we can take any kind of decisions: what to show, how many elements to display, what size to make them, and so on. We'll be making this example short, but it should be clear how to expand it:

// Source file: src/adaptiveApp/adaptiveView.component.js

/* @flow */

import React from "react";
import PropTypes from "prop-types";
import { View, Text, StyleSheet } from "react-native";

import type { deviceDataType } from "./device";

const textStyle = StyleSheet.create({
bigText: {
fontWeight: "bold",
fontSize: 24
}
});

export class AdaptiveView extends React.PureComponent<{
deviceData: deviceDataType
}> {
static propTypes = {
deviceData: PropTypes.object.isRequired
};

renderHandset() {
return (
<View>
<Text style={textStyle.bigText}>
I believe I am a HANDSET currently in
{this.props.deviceData.isPortrait
? " PORTRAIT "
: " LANDSCAPE "}
orientation
</Text>
</View>
);
}

renderTablet() {
return (
<View>
<Text style={textStyle.bigText}>
I think I am a
{this.props.deviceData.isPortrait
? " PORTRAIT "
: " LANDSCAPE "}
TABLET
</Text>
</View>
);
}

render() {
return this.props.deviceData.isTablet
? this.renderTablet()
: this.renderHandset();
}
}
Don't worry about the textStyle definition—soon we'll be getting into how it works, but for now I think it should be easy to accept that it defines bold, largish, text.

Given this.props.deviceData, we can use the .isTablet prop to decide which method to call (.renderTablet() or .renderHandset()). In those methods, we can then use .isPortrait to decide what layout to use: portrait or landscape. Finally—although we don't show this in our example—we could use .width or .height to show more or fewer components, or to calculate the components' sizes, and so on. We only need to connect the component to the store as follows, and we'll be set:

// Source file: src/adaptiveApp/adaptiveView.connected.js

/* @flow */

import { connect } from "react-redux";

import { AdaptiveView } from "./adaptiveView.component";

const getProps = state => ({
deviceData: state.deviceData
});

export const ConnectedAdaptiveView = connect(getProps)(AdaptiveView);

We have everything we need now; let's see it working!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset