As my 1st project in React Native for CS50 Mobile I had to implement a Pomodoro Technique Timer. Basically, you set a Work and Rest timer to some number of minutes and then the app switches between them while you work on a task. There are multiple ways how you can mix the timers and use not just two, but I have decided to implement the basic one.
Before implementing the in React Native I took a pen and a piece of paper and made a little draft of the app.
So my app needs the following components:
- Logo 🍅
- Timer Display (Should change color, based on the timer type (Work/Rest))
- Some control for each timer to set it up
- Play/Pause button
- Reset button
So I have decided to implement the Logo, Timer Dsiplay, Play/Pause/Reset buttons in App.js and the set up control in a separate TimePicker component, beacuse it is identical for both timers
I am using Expo as my developing tools for React Native as well as some of their pretty libraries.
So, in my main App.js I first import some predefined and common components and also my own TimePicker.
import React from 'react'
import {View, Text, Button, StyleSheet, Image, TouchableOpacity, Vibration} from 'react-native'
import Constants from 'expo-constants'
import TimePicker from './TimePicker.js'
Then I make some styles using StyleSheet.create()
function. I have not used all the definedd styles, so let me crarify things here.
appContainer
- defines just the overall look of my ApptimerSetContainer
- is used for two TimePicker components and Play/Pause buttonsimgActive
andimgInactive
- used for Images of Play/Pause buttonstouchButton
- used for Play/Pause button component itself
const styles = StyleSheet.create({
appContainer: {
flex: 1,
backgroundColor: '#fab1a0',
},
timerContainer: {
marginTop: "4%",
},
timerSetContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: "10%",
},
imgActive: {
width: 100,
height: 100,
alignSelf: 'center',
marginTop: Constants.statusBarHeight,
},
imgInactive: {
width: 100,
height: 100,
alignSelf: 'center',
opacity: 0.2,
marginTop: Constants.statusBarHeight,
},
touchButton: {
alignSelf: 'center',
marginTop: "10%",
marginRight: 150,
marginLeft: 150,
},
timerPosition: {
alignItems: 'center',
},
text: {
fontSize: 50,
}
})
I also predefined as global constants the colors for Work/Rest timers' text.
const colorWork = '#ED4C67'
const colorRest = '#D980FA'
Looking at App
class, I first defined some state
variables.
time
- current timestart
- start time (depends on the type of the timer at hand)color
- initial color of the timer display textvalueWorkTimer
- value the user picks for the Work Timer through TimePicker componentvalueRestTimer
- value the user picks for the Rest Timer through TimePicker componentisOn
- boolean var for checking if the App's timer is workingisWorkOn
- boolean var for checking if Work Timer is on (set to true, because the App starts with the Work Timer)activePauseButton
- boolean var for checking if the user can press the Pause button
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
time: 0,
start: 0,
color: '#000000',
valueWorkTimer: 0,
valueRestTimer: 0,
isOn: false,
isWorkOn: true,
activePauseButton: false,
}
}
Then I define two functions WorkTimerCallback
and RestTimerCallback
which receive a value from a TimePicker component (minutes) and assign it to the proper state
variable converted into seconds. I also concole log the values for error checking.
WorkTimerCallback = (dataFromTimerPicker) => {
this.setState(prevState => ({
valueWorkTimer: dataFromTimerPicker * 60,
}))
console.log(`Work: ${this.state.valueWorkTimer}`)
}
RestTimerCallback = (dataFromTimerPicker) => {
this.setState(prevState => ({
valueRestTimer: dataFromTimerPicker * 60,
}))
console.log(`Rest: ${this.state.valueRestTimer}`)
}
In the startTimer
function, which is executed when the user presses on the Play button, I first determine color
for the Timer display text. If the Work timer is on (isWorkOn === true
, the text gets reddish color colorWork
, alternatively, the text gets purple color colorRest
.
Then I assign the right current time
. If the App is off (timers are not working, i.e. isOn === true
), then the time
needs to get either the initial Work timer value valueWorkTimer
or Rest timer value valueRestTimer
. This choice is made using isWorkOn
variable. In case App is on time
variable does not change.
I also assign true to the isOn
variable beacause startTimer
function makes the whole App work.
By the way, the Pause button gets activated activePauseButton: true
and the Play button becomes inactive, which is quite logical, as you only want to use Pause button, when the App is in a work mode.
this.interval = setInterval(this.decreaseTimer, 1000)
executes a function decreaseTimer
every second and returns an intervalID
. I will discuss this function a bit later.
I also console log, that the timer has started working.
startTimer = () => {
this.setState(prevState => ({
color: (prevState.isWorkOn) ? colorWork : colorRest,
time: (!prevState.isOn) ? ((prevState.isWorkOn) ? prevState.valueWorkTimer : prevState.valueRestTimer) : prevState.time,
isOn: true,
activePauseButton: true,
}))
this.interval = setInterval(this.decreaseTimer, 1000)
console.log('START')
}
In stopTimer()
function, which I use in the decreaseTimer
function, I clear the interval (clearInterval()
has the access to the same pool of IDs as the setInterval()
)
I also change the state
variable isOn
to false and console log, that the timer has stoped working.
stopTimer = () => {
clearInterval(this.interval)
this.setState(prevState => ({
isOn: false,
}))
console.log('STOP')
}
pauseTimer()
, which is executed when th user presses the Pause button, looks exactly like the stopTimer()
except for the fact that I change only activePauseButton
state variable to state. It is logical as the user only wants the Play button to be active, when the timer is paused.
I also console log "STOP" message for error checking.
pauseTimer = () => {
clearInterval(this.interval)
console.log('STOP')
this.setState(prevState => ({
activePauseButton: false,
}))
}
resetTimer()
is executed when the user presses on the RESET button. It clears the interval and sets the state to its initial values.
I also console log "RESTART" message for error checking.
resetTimer = () => {
clearInterval(this.interval)
this.setState(prevState => ({
time: 0,
color: '#000000',
isOn: false,
isWorkOn: true,
activePauseButton: false,
}))
console.log('RESTART')
}
I'll now discuss the decreaseTimer()
function that every second while the timer is working.
Firstly, it checks if the current time is 0 or less, meaning that it should stop. If it so, the function executes stopTimer()
and makes a the iPhone vibrate for a second. It also changes the isWorkOn
to the opposite value, so that the App could switch between Work and Rest timers. After doing that, decreaseTimer()
starts the timer.
If the time
is not 0 or less than that, time
value decreases by 1 second and console logs "Working" message.
decreaseTimer = () => {
if (this.state.time <= 0) {
this.stopTimer()
Vibration.vibrate(1000)
this.setState(prevState => ({
isWorkOn: !prevState.isWorkOn
}))
console.log(this.state.isWorkOn)
this.startTimer()
} else {
console.log('Working')
this.setState(prevState => ({
time: prevState.time - 1,
}))
}
}
This function is used to render a pretty Timer display text with minutes and seconds and all the trailing zeros.
displayTimer = (time) => {
let minutes = Math.floor(time / 60)
let seconds = time - minutes * 60
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds
}
In this function I render all the components I want on my screen with styles.
Pause/Play buttons are implemented as TouchableOpacity
components, where their style
attribute is defined based on activePauseButton
value.
render() {
return (
<View style={styles.appContainer}>
<Image style={styles.imgActive} source={require('./img/tomato.png')}></Image>
<Text style={{fontSize: 50, alignSelf: 'center', color: this.state.color}}>{this.displayTimer(this.state.time)}</Text>
<Button onPress={this.resetTimer} title='RESET'/>
<View style={styles.timerSetContainer}>
<TimePicker color={colorWork} heading={'Work'} callback={this.WorkTimerCallback}/>
<TimePicker color={colorRest} heading={'Rest'} callback={this.RestTimerCallback}/>
</View>
<View style={styles.timerSetContainer}>
<TouchableOpacity onPress={this.pauseTimer} disabled={!this.state.activePauseButton} style={styles.touchButton}>
<Image style={this.state.activePauseButton ? styles.imgActive : styles.imgInactive} source={require('./img/pause-button.png')}></Image>
</TouchableOpacity>
<TouchableOpacity onPress={this.startTimer} disabled={this.state.activePauseButton} style={styles.touchButton}>
<Image style={!this.state.activePauseButton ? styles.imgActive : styles.imgInactive} source={require('./img/play-button.png')}></Image>
</TouchableOpacity>
</View>
</View>
)
}
Firstly, I'll look back at the render
function in App.js.
<TimePicker color={colorWork} heading={'Work'} callback={this.WorkTimerCallback}/>
<TimePicker color={colorRest} heading={'Rest'} callback={this.RestTimerCallback}/>
TimePicker
component receives three props:
color
- for headingsheading
- title of theTimePicker
("Work" or "Rest")callback
- a function that receives a value thatTimePicker
outputs and assigns it either tovalueWorkTimer
orvalueRestTimer
.
Now I'll discuss the component itself.
I import all common and standard components with the addition of a NumericInput
, beacause the user only needs to provide a number of minutes for Work/Rest timers.
import React from 'react'
import {View, Text, StyleSheet} from 'react-native'
import NumericInput from 'react-native-numeric-input'
Here I define just two simple styles.
const styles = StyleSheet.create({
appContainer: {
alignItems: 'center',
},
head: {
fontSize: 50,
},
})
In state I only save the number of minutes the user has choosen via the NumericInput
.
export default class TimePicker extends React.Component {
constructor(props) {
super(props)
this.state = {
minutes: 0
}
}
In this finction I render a Text
component with the text and color from the heading
and color
props.
I also render a NumericInput
with some component's own props. The most intersting part is onChange
prop. This prop takes a function that determines what to do with the value of the NumericInput
every time the user changes it. In my case the value is passed to the toParentFunction
.
render() {
return(
<View style={styles.appContainer}>
<Text style={[styles.head, {color: this.props.color}]}>{this.props.heading}</Text>
<NumericInput
value = {this.state.minutes}
minValue={0}
maxValue={60}
totalHeight={80}
rounded
type='up-down'
onChange={value => this.toParentFunction(value)}
/>
</View>
)
}
This function receives the value from the NumericInput
and assigns to the minutes
key in state
. Then it calls from the props a callback
function and passes the NumericInput
value. As I have already described this callback
function will assign the received value either to the Work or Rest timer as the starting one.
toParentFunction = (timePickerValue) => {
this.state.minutes = timePickerValue
this.props.callback(timePickerValue)
//console.log(this.state.minutes)
}