How to Build a Mobile App Using React Native
Have you ever dreamed of building your own mobile app? This article will bring you much closer to that dream. In this article, you will learn how to build a simple timer app. The following is a screenshot of the final product:
The upper time picker panel lets you choose how many hours and minutes you would like to set the timer, while the lower panel handles the running of the timer.
Prerequisites:
- Basic knowledge of JavaScript. You don't need to be a JavaScript expert to follow along with the tutorial. But some knowledge of JavaScript is recommended to make the most use of this article.
- Some knowledge of ECMAScript2015 (aka ES6). The code is written in ES6 syntax. If you are not familiar with ES6, don't panic. You will get a quick brush up on the ES6 syntax for importing dependencies, destructuring assignment, using let instead of var to define variables, and using arrow functions.
- Some knowledge of React is recommended. You should know how the state of a component is initialized and updated. You also need to know how props works.
- Prior knowledge of React Native is not required. React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components.
- Some knowledge of CSS is recommended. To be honest with you, I hate dealing with CSS. But if you want to create mobile apps, CSS is unavoidable. Good news is, the CSS for this tutorial is pretty straightforward.
Environment Setup:
To begin with, we need to set up a development environment for using React Native. The official Getting Started Guide of React Native offers two ways. One is the Quick Start way. The other is one is the Building Projects with Native Code way.
1. Quick Start
Quick Start way is the easiest way to start building a new React Native application. It allows you to start a project without installing or configuring any tools to build native code - no Xcode or Android Studio installation required (see Caveats). Please follow the official guide to set up your environment.
To run your React Native application, install the Expo client app on your iOS or Android phone and connect to the same wireless network as your computer. Using the Expo app, scan the QR code from your terminal to open your project.
I tried this method first because it's easy get started and you can use a real phone instead of an iOS or Android simulator to test your app. But there are two main reasons which made me ultimately decide to switch to the other method. First of all, for developing purposes, having a simulator is enough, and your changes are reflected much faster than using a real phone. Secondly, I encountered some Expo specific errors during the debugging phase which took me a while to figure out and move on. When the headache of adding extra dependency eclipses the benefits, it is natural to let it go. So for this tutorial, I use the second Building Projects with Native Code method.
2. Building Projects with Native Code
Use this method if you need to build native code in your project. For example, if you are integrating React Native into an existing application, or if you "ejected" from Create React Native App, you'll need this section.
The instructions are a bit different depending on your development operating system, and whether you are developing for iOS or Android. If you want to develop for both iOS and Android, that's fine - you just have to pick one to start with, since the setup is a bit different.
In the official guide, choose Development OS: macOS Target OS: iOS to set up your environment.
Game Start
If the environment setup process upsets you, apologies. But no pain, no gain, right? Now, let's get our hands dirty.
Mockup
As a developer, having a mockup of your app is important. Before you get to writing the code, you need to know what you want to achieve with your app and what it should look like. Functionally, I expect my app to do the following:
- When the app starts, it should display a time picker with options for hours and minutes. The "Cancel" and "Start" buttons should be displayed under the time picker.
- After the user sets the time and presses the "Start" button, a timer showing the remaining time in the format of "HH:MM:SS" is displayed in the place of the previous time picker. The remaining time should be updated dynamically. The previous "Start" button changes to the "Pause" button.
- If the user presses the "Pause" button before the time runs out, the timer should stop ticking. And the "Pause" button should change to "Resume".
- At any time when the timer is still running, if the user presses the "Cancel" button, it should bring the user to the time picker panel again. The pre-selected values for the hour and minute depend on how much time is left before you cancel the timer. If the remaining time is 02:04:58, then the hour value will be 2, and the minute value will be 5 because we round 4 minutes and 58 seconds into 5 minutes (iPhone timer app works the same way).
- When the time is up, the time picker panel will show again. In the meantime, this app will play an audio file that I specify in the app.
In terms of the layout of the app, it can be roughly be divided into two blocks, one is the time picker block, and the other one is the buttons block. We will align these two blocks and the buttons within the same block will be achieved by CSS.
Import Dependencies
The following shows the dependencies we need for this app:
import React from 'react';
import { TouchableHighlight, StyleSheet, Text, View, PickerIOS } from 'react-native';
If you are not familiar with the import command and the destructuring assignment syntax of ES6, spend a few minutes on those topics. Don't be scared by the unfamiliar components. As you will see later, they are actually straightforward. TouchableHighlight deals with buttons, Stylesheet deals with CSS, Text, and View are fundamental components you will see over and over again, PickerIOS deals with the fancy time picker panel. Pretty straightforward, right?
Initial State
this.state = {
running: false,
timeRemaining: null,
showTimePicker: true,
selectedHour: 0,
selectedMinute: 1
};
It doesn't matter if you don't know why the state property is like this. Just bear in mind that the state property contains some variables we are going to use in this app. If you are not familiar with the state concept, pause here and spend some time on it before continue reading.
Time Picker (Timer) Block
The key ingredient for the time picker block is the PickerIOS component. It is a native component of React Native framework. Look at the following example code to see how PickerIOS is used in general:
<PickerIOS style={styles.picker}
itemStyle={{fontWeight: 'bold'}}
selectedValue=2
onValueChange={this.setHour}>
<PickerIOS.Item value=1 label=1 />
<PickerIOS.Item value=2 label=2 />
<PickerIOS.Item value=3 label=3 />
</PickerIOS>
For the PikerIOS tag, we usually need to handle the style, itemStyle, selectedValue, and the onValueChange properties. Style and itemStyle properties are quite straightforward. The selectedValue property shows which value a user picks. The onValueChange property specifies what action the app should take if a user selects a different value.
For the hour picker, the value varies from 0 to 23, so we would need 24 PickerIOS.Items for hour selection. We use an array named hoursItems to store these items:
this.hoursItems = [];
for (let i = 0; i < 24; i++) {
this.hoursItems.push(<PickerIOS.Item key={i} value={i} label={i.toString()} />);
}
Note we add the key property in the PickerIOS.Item tag. The key property is not required, but when you are displaying multiple PickerIOS.Items in the same block, the app usually gives the following warning message "Warning: Each child in an array or iterator should have a unique “key” prop." So we add a unique key property for each child.
For the minute picker, we need 60 PickerIOS.Items for minute selection, here is how we do it in this app:
this.minutesItems = [];
for (let j = 0; j < 60; j++) {
this.minutesItems.push(<PickerIOS.Item key={j} value={j} label={j.toString()} />);
}
Now we have hoursItems array to store the hour items, and the minutesItems to store the minute items. Let's see how we can put them together to build the time picker panel:
<View style={styles.pickerWrapper}><PickerIOS style={styles.picker}itemStyle={{fontWeight: 'bold'}}
selectedValue={this.state.selectedHour}onValueChange={this.setHour}
>
{this.hoursItems}
</PickerIOS><PickerIOS style={styles.picker}itemStyle={{fontWeight: 'bold'}}
selectedValue={this.state.selectedMinute}onValueChange={this.setMinute}
>
{this.minutesItems}
</PickerIOS>
</View>
So now we have a time picker here, but it is not done yet. We still haven't defined the setHour and setMinute methods. Here are the definitions:
setHour(hour) {
this.setState({
selectedHour: hour
});
}
setMinute(minute) {
this.setState({
selectedMinute: minute
});
}
Basically, the state property contains selectedHour and selectedMinute variables, and these two variables will be set accordingly if the user changes the values.
Don't worry about the styles.pickerWrapper and styles.picker too much. They are just CSS which beautifies the layout of time picker block. I will show the complete code in the end.
Now we have built the time picker block, but what about the use case when a timer is running? Well, in that case, we should show the running timer in the format of "HH:MM:SS". How can we do that? See the following code:
<View style={styles.timerWrapper}><Text style={styles.timer}>
{this.hourMinuteSecondFormat(this.state.timeRemaining)}
</Text>
</View>
When there is a running timer, instead of showing the time picker, we show the running timer. The hourMinuteSecondFormat method takes in the timeRemaining (in ms unit) variable and returns a string in the format of "HH:MM:SS". The hourMinuteSecondFormat method is defined as follows:
hourMinuteSecondFormat(duration) {
if (duration == null) {
return '00:00:00';
}
let totalSeconds = Math.floor(duration / 1000);
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds % 3600) / 60);
let seconds = Math.floor((totalSeconds % 3600) % 60);
hours = hours < 10 ? '0' + hours : hours + '';
minutes = minutes < 10 ? '0' + minutes : minutes + '';
seconds = seconds < 10 ? '0' + seconds : seconds + '';
return hours + ':' + minutes + ':' + seconds;
}
There are some third-party libraries such as moment which can convert the duration to the "HH:MM::SS" format. But introducing extra dependency also means more vulnerabilities and more maintenance effort. For this simple app, I would prefer writing my own helper function.
Hooray! We nailed down the time picker block, now let's move on to the buttons block.
Buttons Block
The layout of the buttons block is quite simple. There are two buttons evenly spaced. The first one is the "Cancel" button, and the second one is the "Start/Pause/Resume" button. The text of the second button varies depending at which stage the timer is. When you first start the app, apparently, you should see the "Cancel" and the "Start" button.
Cancel Button
Here is how to implement the cancel button:
cancelButton() {
return <TouchableHighlightstyle={styles.button}underlayColor="gray"onPress={this.handleCancelPress}
><Text>
Cancel
</Text></TouchableHighlight>
}
Here we use a helper function cancelButton to generate the layout of a cancel button. The handleCancelPress method defines what action it should take after the user presses the button. The handleCancelPress method is defined as follows:
handleCancelPress() {
let duration = this.hourMinuteSecondFormat(this.state.timeRemaining);
let timeArray = duration.split(':');
let selectedHour = parseInt(timeArray[0]);
let selectedMinute = parseInt(timeArray[1]);
let selectedSecond = parseInt(timeArray[2]);
if (selectedMinute == 59 && selectedSecond > 0) {
selectedMinute = 0;
selectedHour += 1;
} else if (selectedSecond > 0) {
selectedMinute += 1;
}
this.setHour(selectedHour);
this.setMinute(selectedMinute);
this.setState({
showTimePicker: true,
timeRemaining: null,
running: false
});
}
Basically, when the user clicks the cancel button, the time picker will be displayed. By looking at how much time remains, it determines what the default values should be for the hour picker and minute picker.
Start/Pause/Resume Button
Here is how to implement the start/pause/resume button:
startResumePauseButton() {
let style = this.state.running ? styles.pauseButton : styles.startResumeButton;
return <TouchableHighlight
underlayColor="gray"
onPress={this.handleStartPress}
style={[styles.button, style]}>
<Text>
{this.state.timeRemaining == null ? 'Start' : (this.state.running ? 'Pause' : 'Resume')}
</Text>
</TouchableHighlight>
}
The handleStartPress method is defined as follows:
handleStartPress() {
if (this.state.timeRemaining == null) {
// Deal with the case when you press the start buttonlet totalTime = (this.state.selectedHour * 60 + this.state.selectedMinute) * 60 * 1000;this.setState({
timeRemaining: totalTime,
running: true,
showTimePicker: false
});
this.runTimer(totalTime);
} else if (this.state.running) {
// Deal with the case when you press the pause button
clearInterval(this.interval);
this.setState({
running: false
});
} else {
// Deal with the case when you press the resume buttonlet totalTime = this.state.timeRemaining;this.setState({
running: true,
});
this.runTimer(totalTime);
}
}
runTimer(totalTime) {
let startTime = new Date();
this.interval = setInterval(() => {
let timeRemaining = totalTime - (new Date() - startTime);
if (timeRemaining > 0) {
this.setState({
timeRemaining: timeRemaining
});
} else {
// Reset the time picker to the default position, reset timeRemaining to null
clearInterval(this.interval);
this.setState({
showTimePicker: true,
timeRemaining: null,
running: false
});
}
},60);
}
The trick here is to use the setInterval method to update the timer every 60 ms (you can choose your own interval). You also need to check when the time is up. When the time is up, you need to set the state accordingly to display the time picker panel.
CSS
Here is the CSS for this app:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'stretch',
},
pickerWrapper: {
flex: 3,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
},
timerWrapper: {
flex: 3,
justifyContent: 'center',
alignItems: 'center'
},
buttonWrapper: {
flex: 3,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center'
},
timer: {
fontSize: 60
},
button: {
borderWidth: 2,
height: 100,
width: 100,
borderRadius: 50,
justifyContent: 'center',
alignItems: 'center'
},
startResumeButton: {
borderColor: '#00CC00'
},
pauseButton: {
borderColor: '#FFF44F'
},
picker: {
flex: 1
}
});
If you can't remember the syntax, don't worry. I am with you. I always check some cheat sheets for CSS implementation. To get a better idea what justifyContent or flexDirection mean, check out this website: A Complete Guide to Flexbox.
Final Product
So far, we have shown how to build the time picker block, the buttons block, and the CSS for this app. The final part is to put all the ingredients together. The following is the final product:
import React from 'react';
import { TouchableHighlight, StyleSheet, Text, View, PickerIOS } from 'react-native';
export default class App extends React.Component {
constructor(props) {
super(props);
this.setHour = this.setHour.bind(this);
this.setMinute = this.setMinute.bind(this);
this.handleStartPress = this.handleStartPress.bind(this);
this.handleCancelPress = this.handleCancelPress.bind(this);
this.startResumePauseButton = this.startResumePauseButton.bind(this);
this.cancelButton = this.cancelButton.bind(this);
this.playSound = this.playSound.bind(this);
this.hourMinuteSecondFormat = this.hourMinuteSecondFormat.bind(this);
this.runTimer = this.runTimer.bind(this);
this.state = {
running: false,
timeRemaining: null,
showTimePicker: true,
selectedHour: 0,
selectedMinute: 1
};
this.hoursItems = [];
this.minutesItems = [];
for (let i = 0; i < 24; i++) {
this.hoursItems.push(<PickerIOS.Item key={i} value={i} label={i.toString()} />);
}
for (let j = 0; j < 60; j++) {
this.minutesItems.push(<PickerIOS.Item key={j} value={j} label={j.toString()} />);
}
}
render() {
return (
<View style={styles.container}>
{this.timePickerOrTimer()}
<View style={styles.buttonWrapper}>
{this.cancelButton()}
{this.startResumePauseButton()}
</View>
</View>
);
}
timePickerOrTimer() {
if (this.state.showTimePicker) {
return (
<View style={styles.pickerWrapper}>
<PickerIOS style={styles.picker}
itemStyle={{fontWeight: 'bold'}}
selectedValue={this.state.selectedHour}
onValueChange={this.setHour}
>
{this.hoursItems}
</PickerIOS>
<PickerIOS style={styles.picker}
itemStyle={{fontWeight: 'bold'}}
selectedValue={this.state.selectedMinute}
onValueChange={this.setMinute}
>
{this.minutesItems}
</PickerIOS>
</View>
)
} else {
return (
<View style={styles.timerWrapper}>
<Text style={styles.timer}>
{this.hourMinuteSecondFormat(this.state.timeRemaining)}
</Text>
</View>
)
}
}
startResumePauseButton() {
let style = this.state.running ? styles.pauseButton : styles.startResumeButton;
return <TouchableHighlight
underlayColor="gray"
onPress={this.handleStartPress}
style={[styles.button, style]}>
<Text>
{this.state.timeRemaining == null ? 'Start' : (this.state.running ? 'Pause' : 'Resume')}
</Text>
</TouchableHighlight>
}
cancelButton() {
return <TouchableHighlight
style={styles.button}
underlayColor="gray"
onPress={this.handleCancelPress}
>
<Text>
Cancel
</Text>
</TouchableHighlight>
}
handleStartPress() {
if (this.state.timeRemaining == null) {
// Deal with the case when you press the start button
let totalTime = (this.state.selectedHour * 60 + this.state.selectedMinute) * 60 * 1000;
this.setState({
timeRemaining: totalTime,
running: true,
showTimePicker: false
});
this.runTimer(totalTime);
} else if (this.state.running) {
// Deal with the case when you press the pause button
clearInterval(this.interval);
this.setState({
running: false
});
} else {
// Deal with the case when you press the resume button
let totalTime = this.state.timeRemaining;
this.setState({
running: true,
});
this.runTimer(totalTime);
}
}
runTimer(totalTime) {
let startTime = new Date();
this.interval = setInterval(() => {
let timeRemaining = totalTime - (new Date() - startTime);
if (timeRemaining > 0) {
this.setState({
timeRemaining: timeRemaining
});
} else {
// Reset the time picker to the default position, reset timeRemaining to null
clearInterval(this.interval);
this.setState({
showTimePicker: true,
timeRemaining: null,
running: false
});
}
},60);
}
/**
* Handle the case when the Cancel button is pressed.
*/
handleCancelPress() {
let duration = this.hourMinuteSecondFormat(this.state.timeRemaining);
let timeArray = duration.split(':');
let selectedHour = parseInt(timeArray[0]);
let selectedMinute = parseInt(timeArray[1]);
let selectedSecond = parseInt(timeArray[2]);
if (selectedMinute == 59 && selectedSecond > 0) {
selectedMinute = 0;
selectedHour += 1;
} else if (selectedSecond > 0) {
selectedMinute += 1;
}
this.setHour(selectedHour);
this.setMinute(selectedMinute);
this.setState({
showTimePicker: true,
timeRemaining: null,
running: false
});
}
setHour(hour) {
this.setState({
selectedHour: hour
});
}
setMinute(minute) {
this.setState({
selectedMinute: minute
});
}
hourMinuteSecondFormat(duration) {
if (duration == null) {
return '00:00:00';
}
let totalSeconds = Math.floor(duration / 1000);
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds % 3600) / 60);
let seconds = Math.floor((totalSeconds % 3600) % 60);
hours = hours < 10 ? '0' + hours : hours + '';
minutes = minutes < 10 ? '0' + minutes : minutes + '';
seconds = seconds < 10 ? '0' + seconds : seconds + '';
return hours + ':' + minutes + ':' + seconds;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'stretch',
},
pickerWrapper: {
flex: 3,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
},
timerWrapper: {
flex: 3,
justifyContent: 'center',
alignItems: 'center'
},
buttonWrapper: {
flex: 3,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center'
},
timer: {
fontSize: 60
},
button: {
borderWidth: 2,
height: 100,
width: 100,
borderRadius: 50,
justifyContent: 'center',
alignItems: 'center'
},
startResumeButton: {
borderColor: '#00CC00'
},
pauseButton: {
borderColor: '#FFF44F'
},
picker: {
flex: 1
}
});
Conclusions
Congratulations! You have successfully built a timer app! Give yourself a round of applause. You have accomplished the transition from 0 to 1. I hope you enjoy the process and keep learning new things.
References:
Challenges:
If the above exercise is not enough, can you make the app above play some audio file when the time is up? Tip: You can use the react-native-sound library.
Love this article? Hate this article? Leave a comment below.