Getting started with React Native Part 3: Using native device functionality

getting-started-react-native-part-3-header.png

This three part series takes you through building a workout tracker app using React Native and Expo. In part three, learn how to access the device's camera and file system.

Introduction

In this part, you’ll learn how to use native device functionality in React Native. Specifically, you’ll be accessing the device’s camera and file system to implement the app’s progress page.

This is part three of a three-part series on getting started with React Native. It is recommended to read part one and part two before starting this section.

Here’s what the final output will look like:

Final app demo

The full source code of the app is available on this Github repo. You can run the demo on your browser or on your Expo client app by scanning the QR code found on that page.

Prerequisites

As this is a beginner series, knowledge of the following are not required:

  • React Native
  • React

Knowledge of the following is required:

  • HTML
  • CSS
  • JavaScript – ES5 is required, while familiarity with ES6 features is helpful.
  • Git – cloning repos, switching branches.

The following versions are used in this tutorial:

If you’re reading this at a later time, and you have issues running the code, be sure to check out the following changelogs or release notes. Take note of any breaking changes or deprecations and update the code accordingly. I’ve linked them above, in case you missed it.

Reading the first and second part of the series is not required if you already know the basics of styling and scripting in React Native. Go through the following steps if you want to follow along:

  • Install Expo and the Expo client app for your iOS or Android device. The client app will be used for running the app.
  • Clone the Github repo:
1git clone https://github.com/anchetaWern/increment.git
2    cd increment
  • Switch to the part2 branch:
1git checkout part2
  • Install all the dependencies:
1npm install

What you’ll be building

In this part of the series, you’ll be implementing the progress page of the app. This page allows the user to take selfie pictures of their workout progress. The photos are stored in the device’s filesystem and are viewable from the app at a later time.

At this point, you should be fairly confident in working with React Native. That’s why I’ll point you to the documentation of the two Expo API’s that we’ll be using:

  • Camera
  • FileSystem

You can still follow along with the tutorial if you want. But if you want a bit of a challenge, I encourage you to read those docs and try to implement the features on your own.

Once you’ve decided which path to choose, navigate inside the increment folder and execute the following on the terminal:

1exp start

Scan the QR code like usual so you can start developing.

Updating the Progress page

I’d like to take a different approach in this tutorial. The usual approach that I take is a top-to-bottom approach where I talk about the code as it appears on the file. This time I’ll let the implementation guide the order of things. So expect to be working on multiple files and jump from one line of code to another.

Taking Photos

The first thing that needs to be implemented is the button for opening the camera. Update it so the navigationOptions becomes a function which returns the navigation options. This way we can access the navigation params. We need it because just like in the Log Workout page, there’s a need to call a function which modifies the state. We can’t really call a function from inside the component class so we pass it as a navigation parameter instead:

1// app/screens/Progress.js
2    static navigationOptions = ({ navigation }) => {
3      const { params } = navigation.state;
4
5      return {
6        headerTitle: 'Progress',
7        headerRight: (
8          <IconButton size={25} color="#FFF" onPress={() => {
9            params.openCamera();
10          }} />
11        ),
12        headerStyle: {
13          backgroundColor: '#333'
14        },
15        headerTitleStyle: {
16          color: '#FFF'
17        }
18      }
19    };

Inside the Progress class, create the openCamera function. This will update the state to make the camera visible:

1openCamera = () => {
2      this.setState({
3        is_camera_visible: true
4      });
5    }

Don’t forget to initialize it on the state so the camera isn’t shown by default:

1state = {
2        is_camera_visible: false,
3    }

Add the navigation param once the component is mounted:

1componentDidMount() {
2      this.props.navigation.setParams({
3        'openCamera': this.openCamera
4      });
5    }

The next step is to ask for permission to use the device’s camera. An app cannot simply use native device functionality without asking permission from the user first. Expo comes with the Permissions API for tasks like these. Import it near the top of the file along with the Camera:

1import { Permissions, Camera } from 'expo';

The best time to ask for permission is the time before the Progress page is rendered on the screen. The code below asks for permission to use the camera. If the user grants the request, the state has_camera_permission is updated to true:

1componentWillMount() {
2      Permissions.askAsync(Permissions.CAMERA).then((response) => {
3        this.setState({
4          has_camera_permission: response.status === 'granted'
5        });
6      });
7    }

The next step is to render the camera UI. We don’t really need to create a separate page for it so we’ll just use a modal:

1render() {
2      return (
3        <View style={styles.wrapper}>
4          <Modal
5            animationType="slide"
6            transparent={false}
7            visible={this.state.is_camera_visible}
8            onRequestClose={() => {
9              this.setState({
10                is_camera_visible: false
11              });
12            }}>
13            <View style={styles.modal}>
14              {/* next: add code for rendering camera */}
15            </View>
16          </Modal>
17        </View>  
18      );
19    }

Use the Camera component from Expo to render a camera UI inside the app:

1{
2      this.state.has_camera_permission &&
3      <Camera style={styles.wrapper} type={this.state.type} ref={ref => { this.camera = ref; }}>
4        <View style={styles.camera_body}>
5          <View style={styles.upper_buttons_container}>
6            <IconButton is_transparent={true} icon="close"
7              styles={[styles.camera_button, styles.camera_close_button]}
8              onPress={this.closeCamera} />
9
10            <IconButton is_transparent={true} icon="flip"
11              styles={[styles.camera_button, styles.camera_flip_button]}
12              onPress={this.flipCamera} />
13          </View>
14
15          <View style={styles.lower_buttons_container}>
16            <IconButton is_transparent={true} icon="photo-camera"
17              styles={styles.camera_photo_button}
18              onPress={this.takePicture} />
19          </View>
20        </View>
21      </Camera>
22    }

In the code above, we have a few new things. First is this bit of code:

1ref={ref => { this.camera = ref; }}

This creates a reference to the Camera component. Coming from the HTML world, it’s like assigning an ID to the component so you can refer to it later. In this case, the reference to the component is assigned to this.camera. We can then use it to perform actions using the camera.

Next, is the type prop passed to the Camera. This allows us to specify the camera type. There can only be two possibilities for this: front or back. front is used when taking a selfie. While for everything else it’s back. We’re storing this information in the state so we can easily flip between front and back cameras:

1type={this.state.type}

Next is the is_transparent prop passed to the IconButton. :

1is_transparent={true}

We’re using this to decide whether to use the regular icon button or a transparent one. We need the transparent for the camera’s buttons because we don’t want any background surrounding the actual button.

To implement the transparent button, open the app/components/IconButton.js file and include the TouchableOpacity component:

1import { TouchableHighlight, TouchableOpacity } from 'react-native';

If the is_transparent prop is passed, use TouchableOpacity instead:

1if(props.is_transparent){
2      return (
3        <TouchableOpacity style={[styles.transparent_icon_button, props.styles]} onPress={props.onPress}>
4          <MaterialIcons name={icon_name} size={icon_size} color={color} />
5        </TouchableOpacity>
6      );
7    }

In the code above, we’re using two separate style declarations for the transparent button (as indicated by the array syntax). One relies on a pre-declared style, and the other relies on a prop.

Update the styles for the component:

1// app/components/IconButton/styles.js
2    transparent_icon_button: {
3      alignItems: 'center',
4    }

Going back to the app/screens/Progress.js file, the IconButton should now be usable as a transparent button.

At the top of the file, don’t forget to include Modal from react-native:

1// app/screens/Progress.js
2    import { /*previously added packages here*/ Modal } from 'react-native';

Next, add the styles to the components that we just used:

1wrapper: {
2      flex: 1
3    },
4    modal: {
5      marginTop: 22,
6      flex: 1
7    },
8    camera_body: {
9      flex: 1,
10      backgroundColor: 'transparent',
11      flexDirection: 'column'
12    },
13    upper_buttons_container: {
14      flex: 1,
15      alignSelf: 'stretch',
16      flexDirection: 'row',
17      justifyContent: 'space-between'
18    },
19    lower_buttons_container: {
20      flex: 1,
21      alignSelf: 'stretch',
22      justifyContent: 'flex-end'
23    },
24    camera_button: {
25      padding: 10
26    },
27    camera_close_button: {
28      alignSelf: 'flex-start',
29      alignItems: 'flex-start'
30    },
31    camera_flip_button: {
32      alignSelf: 'flex-start',
33      alignItems: 'flex-end'
34    },
35    camera_photo_button: {
36      alignSelf: 'center',
37      alignItems: 'center',
38      paddingBottom: 10
39    },

Next, we need to implement the three functions that the camera buttons use:

  • closeCamera – used for closing the camera modal.
  • flipCamera – used for flipping the camera (front or back).
  • takePicture – used for capturing a photo.

I’ll leave it to you to implement the first function.

As for the flipCamera, it will update the type in the state based on its current value. So if it’s currently using the back camera, then it changes it to the front and vice-versa:

1flipCamera = () => {
2      this.setState({
3        type: this.state.type === Camera.Constants.Type.back
4          ? Camera.Constants.Type.front
5          : Camera.Constants.Type.back,
6      });
7    }

Next is the takePicture function. It uses the Camera reference we’ve created earlier to call the takePictureAsync method. This method captures a picture and saves it to the app’s cache directory:

1takePicture = () => {
2      if(this.camera){ // check whether there's a camera reference
3        this.camera.takePictureAsync().then((data) => {
4          // next: add code for processing the response data from the camera
5        });
6      }
7
8    }

The next step is to move the photo to a permanent directory. But before that, we first have to generate a filename to be used for the photo. To make it unique, we’ll just stick with the current date and time:

1let datetime = getPathSafeDatetime(); // use a file path friendly datetime

Here’s the getPathSafeDatetime() function. All it does is the format the current date and time to one that is safe for use as a filename. So 4/20/2018, 9:38:12 PM becomes 4-20-2018+9_38_51+PM:

1// app/lib/general.js
2    function getPathSafeDatetime() {
3      let datetime = getLocalDateTime(new Date()).replace(/\//g, '-').replace(',', '').replace(/:/g, '_').replace(/ /g, '+');
4      return datetime;
5    }

Don’t forget to import it:

1// app/screens/Progress.js
2    import { getPathSafeDatetime } from '../lib/general';

The only problem left is how to determine the full path in which the file will be saved. Remember that we can’t simply use a relative path like so:

1let file_path = `./${datetime}.jpg`;

This is because the relative path isn’t actually a directory where we can store files. Remember that the JavaScript files aren’t actually compiled when the app is generated. So the relative path is still the app/screens directory.

For us to get the full path, we need to use the FileSystem API from Expo:

1import { Permissions, Camera, FileSystem } from 'expo';

We’ll need to re-use the full path a few times so we declare it once inside the constructor along with the file name prefix:

1constructor(props) {
2      super(props);
3      this.document_dir = FileSystem.documentDirectory; // the full path to where the photos should be saved (includes the trailing slash)
4      this.filename_prefix = 'increment_photo_'; // prefix all file names with this string
5    }

Going back inside the response for taking pictures, we can now bring the full path and the file name together:

1this.camera.takePictureAsync().then((data) => {
2      let datetime = getPathSafeDatetime();
3      let file_path = `${this.document_dir}${this.filename_prefix}${datetime}.jpg`;
4      // next: add code for moving the photo to its permanent location
5    });

At this point, we can now move the photo to its permanent location:

1FileSystem.moveAsync({
2      from: data.uri, // the path to where the photo is saved in the cache directory
3      to: file_path 
4    })
5    .then((response) => {
6      // next: add code for storing the file name and updating the state
7    });

Now, construct the data to be saved in local storage. We need this for fetching the actual photos later:

1let photo_data = {
2      key: uniqid(), // unique ID for the photo
3      name: datetime // the photo's filename
4    };
5    store.push('progress_photos', photo_data); // save it on local storage

Also, update the state so the picture that has just been taken will be rendered in the UI:

1let progress_photos = [...this.state.progress_photos];
2    progress_photos.push(photo_data);
3
4    this.setState({
5      progress_photos: progress_photos
6    });
7
8    Alert.alert(
9      'Saved',
10      'Your photo was successfully saved!',
11    );

Don’t forget to include the React Native Simple Store library and the components and functions we’ve used:

1import store from 'react-native-simple-store';
2    import { /*other packages*/ Alert } from 'react-native';
3    import { /*other functions*/ uniqid } from '../lib/general';

Rendering Photos

The last thing that needs to be implemented is the rendering of previously captured photos. To do that, we first have to fetch the names of the previously saved photos from local storage and update the state:

1componentDidMount() {
2      /* previously added code: setting additional navigation params */
3      store.get('progress_photos')
4        .then((response) => {
5          if(response){
6            this.setState({
7              progress_photos: response
8            });
9          }
10        });
11    }

Next, update the render method to show an alert box if there are no photos added yet. Otherwise, use the FlatList component to render the photos. Add the numColumns prop to specify how many numbers of columns you want to render for each row:

1render() {
2
3      return (
4        <View style={styles.wrapper}>
5          /* previously added code: camera modal */
6          {
7            this.state.progress_photos.length == 0 &&
8            <AlertBox text="You haven't taken any progress pictures yet." type="info" />
9          }
10
11          {
12            this.state.progress_photos.length > 0 &&
13            <FlatList data={this.state.progress_photos} numColumns={2} renderItem={this.renderItem} />
14          }
15        </View>
16      );
17
18    }

Don’t forget to include the AlertBox component at the top of the file:

1import AlertBox from '../components/AlertBox';

Next, update the renderItem function to use the data from the state. Also update the function for handling the onPress event:

1renderItem = ({item}) => {
2
3      let name = friendlyDate(item.name);
4      let photo_url = `${this.document_dir}${this.filename_prefix}${item.name}.jpg`;
5
6      return (
7        <TouchableHighlight key={item.key} style={styles.list_item} underlayColor="#ccc" onPress={() => {
8          this.showPhoto(item);
9        }}>
10          <View style={styles.image_container}>
11            <Image
12              source={{uri: photo_url}}
13              style={styles.image}
14              ImageResizeMode={"contain"} />
15            <Text style={styles.image_text}>{name}</Text>
16          </View>
17        </TouchableHighlight>
18      );
19
20    }

Update the app/lib/general.js file to include the friendlyDate function. All this function does is format the file path friendly date back to its original form:

1function friendlyDate(str) {
2      let friendly_date = str.replace(/-/g, '/').replace(/\+/g, ' ').replace(/_/g, ':');
3      return friendly_date;
4    }
5
6    export { /*previously exported functions*/, friendlyDate };

When one of the rendered photos is pressed, the showPhoto function is executed. This updates the state to show the modal for viewing the full-size photo. The current_image stores the relevant data for the selected photo:

1showPhoto = (item) => {
2      this.setState({
3        is_photo_visible: true,
4        current_image: {
5          url: `${this.document_dir}${this.filename_prefix}${item.name}.jpg`,
6          label: friendlyDate(item.name)
7        }
8      });
9    }

Inside the render function, add the modal for viewing the photo. To make the image occupy the entire screen, add flex: 1 for the styling and ImageResizeMode should be contain. After the Image is the button for closing the photo and the label. Note that it should be rendered after the Image because you want it to be laid on top of the image. As an alternative, you can specify the zIndex style property:

1<Modal
2      animationType="slide"
3      transparent={false}
4      visible={this.state.is_photo_visible}
5      onRequestClose={
6        this.setState({
7          is_photo_visible: false
8        });
9      }>
10      <View style={styles.modal}>
11        {
12          this.state.current_image &&
13          <View style={styles.wrapper}>
14            <Image
15              source={{uri: this.state.current_image.url}}
16              style={styles.wrapper}
17              ImageResizeMode={"contain"} />
18
19            <IconButton is_transparent={true} icon="close"
20              styles={styles.close_button}
21              onPress={this.closePhoto} />
22
23            <View style={styles.photo_label}>
24              <Text style={styles.photo_label_text}>{this.state.current_image.label}</Text>
25            </View>
26          </View>
27        }
28      </View>
29    </Modal>

Lastly, update the styles:

1close_button: {
2      position: 'absolute',
3      top: 0,
4      left: 0,
5      padding: 10
6    },
7    photo_label: {
8      position: 'absolute',
9      bottom: 0,
10      right: 0,
11      padding: 5
12    },
13    photo_label_text: {
14      color: '#FFF'
15    },

Conclusion and next steps

That’s it! In this tutorial series, you’ve learned the basics of creating a React Native app. Over the course of three tutorials, you’ve built a fully functional workout tracking app. Along the way, you’ve learned how to add styles and functionality to a React Native app. You’ve also learned about React’s basic concepts such as UI componentization, state management, and the component lifecycle methods.

Now that you’ve got your hands dirty, it’s time to move on to the “real” React Native development. As mentioned in the first part of this series, we only made use of Expo in order to avoid the headaches which come with setting up a React Native development environment.

At the time of writing this tutorial, Expo locks you in their platform until you “eject” your app to a plain React Native project. This means that you don’t have much choice on which tools and native packages to use for your app. You’re just locked into using the tools they provide. It’s the price you have to pay for the convenience that it provides.

If you want to continue developing apps with React Native, it’s important that you learn how to work with plain React Native. It’s a good thing that Expo provides two ways to convert an existing Expo project to a plain React Native project:

  • Eject to a plain React Native project – allows you to convert your Expo app to a plain React Native project. But all the code which uses any of Expo’s API’s needs to be updated to the plain React Native equivalent.
  • Eject to ExpoKit – the same as the above option, but it also ejects the Expo native libraries. This enables you to still use Expo’s API’s.

With that out of the way, here are a few topics that I recommend you to explore:

That’s all folks! I hope this series has given you the relevant skills to jump-start your React Native development journey. The complete source code for this app is available on Github. Each part has their own branch so don’t forget to checkout the part3 branch.