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.
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:
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.
As this is a beginner series, knowledge of the following are not required:
Knowledge of the following is required:
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:
1git clone https://github.com/anchetaWern/increment.git 2 cd increment
1git checkout part2
1npm install
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:
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.
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.
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';
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 },
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:
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.