Initially, when I launched the first article on making a simple math learning application, I did not think I would continue writing Part 2! ^. ^. But I myself found that the app has a lot of directions for development so feel free to code more, so it’s also convenient to write part 2 for it: v. So from now on, I will start this series, turn an app that looks like nothing, but it is actually empty (joking: v), over time, I will gradually apply the techniques in React. Native that I was exposed as Notifications, Authentication, Biometric, etc. and clouds …
I think the introduction should be enough, right: 3, let’s start now
1) Install
In this article, there are two things I will add to the app, as well as help you continue to familiarize yourself with the most basic things in React Native, namely @react-navigation/native
and mobx
, where @react-navigation/native
used to switch between screens, while mobx
used for state
management (for new friends, mobx
is not a bad choice to start learning about managing state)
mobx
: https://mobx.js.org/README.html#getting-started , https://reactnavigation.org/docs/getting-started
2) react-navigation / native
We will edit some files and create some new files, the first is App.js
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import 'react-native-gesture-handler'; import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import WelcomeStack from './src/navigation/welcomeStack'; export default function App() { return ( <NavigationContainer> <WelcomeStack /> </NavigationContainer> ); } |
Here I have created 1 Stack Welcome, the goal is to contain 3 other sub screens, including welcome, practice and failed
WelcomeStack.js
here: v
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import React from 'react'; import Practice from '../screen/practice'; import Failed from '../screen/failed'; import WelcomeScreen from '../screen/welcome'; import { StackRoute } from '../constants/route'; import { createStackNavigator } from '@react-navigation/stack'; const Stack = createStackNavigator(); const WelcomeStack = () => { return ( <Stack.Navigator initialRouteName={StackRoute.Main.Welcome}> <Stack.Screen name={StackRoute.Main.Welcome} component={WelcomeScreen} options={{ headerShown: false }} backBehavior="none" /> <Stack.Screen name={StackRoute.Main.Practice} component={Practice} options={{ headerShown: false }} /> <Stack.Screen name={StackRoute.Main.Failed} component={Failed} options={{ headerShown: false }} /> </Stack.Navigator> ); }; export default WelcomeStack; |
For the sake of further development, the names of the monitors, or the Stack, I will inform into a common file named route.js
as above, you should also have a reasonable folder and file arrangement layout. so that later if the project is enlarged, there is no need to rearrange. You can refer to your route.js
file:
1 2 3 4 5 6 7 8 | export const StackRoute = { Main: { Welcome: 'Welcome', Practice: 'Practice', Failed: 'Failed', }, }; |
Next are 3 files respectively welcome.js
, practice.js
, failed.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | // welcome.js import React, { useRef, useEffect } from 'react'; import { StyleSheet, View, Text, TouchableHighlight, Image, Animated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { colors, fonts, spaces, borderRadius, borderWidth, } from '../constants/theme'; import { PlayIcon } from '../assets/icons/index'; import { StackRoute } from '../constants/route'; export default function WelcomeScreen() { const Navigate = useNavigation(); const fadeAnim = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.timing(fadeAnim, { toValue: 1, duration: 3000, useNativeDriver: true, }).start(); }, []); return ( <View style={styles.container}> <View style={styles.titleContainer}> <Text style={[styles.styleTitle, styles.title1]}>Happy</Text> <Text style={[styles.styleTitle, styles.title2]}>Math</Text> </View> <View style={styles.expressionContainer}> <View style={styles.numberContainer}> <Text style={styles.number}>1 + 1</Text> <Text style={styles.number}>= 3</Text> </View> <Animated.View style={{ opacity: fadeAnim }}> <Text style={styles.questionMark}>?</Text> </Animated.View> </View> <TouchableHighlight style={styles.imageContainer} onPress={() => Navigate.navigate(StackRoute.Main.Practice)}> <Image source={PlayIcon} /> </TouchableHighlight> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg_primary, justifyContent: 'center', alignItems: 'center', }, titleContainer: { flexDirection: 'row', alignItems: 'center', borderWidth: borderWidth.normal, borderColor: 'transparent', paddingHorizontal: spaces.space3, borderRadius: borderRadius.header, }, styleTitle: { textTransform: 'uppercase', color: colors.text, }, title1: { fontSize: fonts.header1, marginRight: spaces.space2, }, title2: { fontSize: fonts.header4, fontWeight: 'bold', }, expressionContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: spaces.space4, }, numberContainer: { alignItems: 'center', }, number: { color: colors.white_milk, fontSize: fonts.header2, fontWeight: 'bold', }, questionMark: { color: 'white', fontSize: fonts.header6 + fonts.largest, marginLeft: spaces.space4, fontWeight: 'bold', transform: [ { rotate: '8deg', }, ], }, imageContainer: { width: 150, height: 100, backgroundColor: colors.white, borderRadius: borderRadius.header, justifyContent: 'center', alignItems: 'center', }, }); |
Also, in this update, I also applied a basic React native animation, but since it’s a pretty interesting and interesting topic, deserving a separate article, I won’t write it here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | // practice.js import { observer } from 'mobx-react'; import 'mobx-react-lite/batchingForReactNative'; import React, { useState, useEffect } from 'react'; import { StyleSheet, View, Text, Dimensions, TouchableHighlight, Image, } from 'react-native'; import { colors, fonts, spaces, borderWidth } from '../constants/theme'; import { RightIcon, WrongIcon } from '../assets/icons/index'; import { useNavigation } from '@react-navigation/native'; import { StackRoute } from '../constants/route'; import PracticeStore from '../stores/practiceStore'; const windowWidth = Dimensions.get('window').width; const PracticeScreen = observer(() => { const Navigate = useNavigation(); const [result, setResult] = useState(PracticeStore.calculateResult()); function randomNumber(to, from) { return Math.floor(Math.random() * from) + to; } useEffect(() => { PracticeStore.setFirstParameter(randomNumber(1, 9)); PracticeStore.setSecondParameter(randomNumber(1, 9)); setResult(PracticeStore.calculateResult()); }, []); function pressAnswer(type) { const isTrue = PracticeStore.FirstParameter + PracticeStore.SecondParameter === result; if ((type === 'wrong' && isTrue) || (type === 'right' && !isTrue)) { Navigate.navigate(StackRoute.Main.Failed); return; } if ((type === 'wrong' && !isTrue) || (type === 'right' && isTrue)) { PracticeStore.setFirstParameter(randomNumber(1, 9)); PracticeStore.setSecondParameter(randomNumber(1, 9)); setResult(PracticeStore.calculateResult()); return PracticeStore.setPoint(PracticeStore.Point + 1); } } const Header = () => { return ( <View style={styles.header}> <Text style={styles.point}>{PracticeStore.Point} Điểm</Text> </View> ); }; const Body = () => { return ( <View style={styles.body}> <View style={styles.expressionContainer}> <View style={styles.numberContainer}> <Text style={styles.number}> {PracticeStore.FirstParameter} + {PracticeStore.SecondParameter} </Text> <Text style={styles.number}>= {result}</Text> </View> <Text style={styles.questionMark}>?</Text> </View> </View> ); }; const Footer = () => { return ( <View style={styles.footer}> <TouchableHighlight style={[styles.buttonLeft, styles.buttonFooter]} onPress={() => pressAnswer('right')}> <Image source={RightIcon} /> </TouchableHighlight> <TouchableHighlight style={[styles.buttonRight, styles.buttonFooter]} onPress={() => pressAnswer('wrong')}> <Image source={WrongIcon} /> </TouchableHighlight> </View> ); }; return ( <View style={styles.container}> <Header /> <Body /> <Footer /> </View> ); }); export default PracticeScreen; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg_primary, }, header: { flex: 1, flexDirection: 'row', justifyContent: 'space-around', paddingTop: 20, width: '100%', }, point: { color: '#fff', fontSize: 40, fontWeight: 'bold', }, body: { width: windowWidth, flex: 1, justifyContent: 'center', alignItems: 'center', }, expressionContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: spaces.space4, }, numberContainer: { alignItems: 'center', }, number: { color: colors.white_milk, fontSize: fonts.header2, fontWeight: 'bold', }, questionMark: { color: 'white', fontSize: fonts.header6 + fonts.largest, marginLeft: spaces.space4, fontWeight: 'bold', transform: [ { rotate: '8deg', }, ], }, footer: { width: windowWidth, flex: 1, flexDirection: 'row', alignItems: 'flex-end', paddingBottom: spaces.space2, }, buttonFooter: { width: windowWidth / 2 - 15, alignItems: 'center', justifyContent: 'center', borderRadius: 10, backgroundColor: colors.white, borderBottomWidth: borderWidth.bolder, borderLeftWidth: borderWidth.bolder, borderColor: colors.black_light, }, buttonLeft: { marginLeft: 10, }, buttonRight: { marginHorizontal: 10, }, }); |
In this file, if you pay attention to read a bit (if not read okay, just a bit of error when running it) will see mobx
and PracticeStore
have been added, I’ll talk about it in the next section.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | // failed.js import React, { useRef, useEffect } from 'react'; import { StyleSheet, View, Text, TouchableHighlight, Image, Animated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { colors, fonts, spaces, borderRadius, borderWidth, } from '../constants/theme'; import { PlayIcon } from '../assets/icons/index'; import { StackRoute } from '../constants/route'; import PracticeStore from '../stores/practiceStore'; export default function FailedScreen() { const fadeAnim = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.timing(fadeAnim, { toValue: 1, duration: 3000, useNativeDriver: true, }).start(); }, []); const Navigate = useNavigation(); const handleReStart = () => { PracticeStore.setPoint(0); PracticeStore.setFirstParameter(PracticeStore.randomNumber(1, 9)); PracticeStore.setSecondParameter(PracticeStore.randomNumber(1, 9)); Navigate.navigate(StackRoute.Main.Practice); }; return ( <View style={styles.container}> <View style={styles.titleContainer}> <Text style={[styles.styleTitle, styles.title1]}>Happy</Text> <Text style={[styles.styleTitle, styles.title2]}>Math</Text> </View> <View style={styles.expressionContainer}> <View style={styles.numberContainer}> <Text style={styles.number}> {PracticeStore.FirstParameter} + {PracticeStore.SecondParameter} </Text> <Text style={styles.number}> = {PracticeStore.FirstParameter + PracticeStore.SecondParameter} </Text> </View> <Animated.View style={{ opacity: fadeAnim }}> <Text style={styles.questionMark}>!</Text> </Animated.View> </View> <View style={styles.pointContainer}> <Text style={styles.pointText}>Điểm: </Text> <Text style={styles.pointText}>{PracticeStore.Point}</Text> </View> <TouchableHighlight style={styles.imageContainer} onPress={() => handleReStart()}> <Image source={PlayIcon} /> </TouchableHighlight> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg_primary, justifyContent: 'center', alignItems: 'center', }, titleContainer: { flexDirection: 'row', alignItems: 'center', borderWidth: borderWidth.normal, borderColor: 'transparent', paddingHorizontal: spaces.space3, borderRadius: borderRadius.header, }, styleTitle: { textTransform: 'uppercase', color: colors.text, }, title1: { fontSize: fonts.header1, marginRight: spaces.space2, }, title2: { fontSize: fonts.header4, fontWeight: 'bold', }, expressionContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: spaces.space4, }, numberContainer: { alignItems: 'center', }, number: { color: colors.white_milk, fontSize: fonts.header2, fontWeight: 'bold', }, questionMark: { color: 'white', fontSize: fonts.header6 + fonts.largest, marginLeft: spaces.space4, fontWeight: 'bold', transform: [ { rotate: '8deg', }, ], }, pointContainer: { backgroundColor: colors.bg_primary, borderRadius: borderRadius.header, borderWidth: borderWidth.bolder, borderColor: colors.white, marginBottom: spaces.space8, flexDirection: 'row', paddingHorizontal: spaces.space6, paddingVertical: spaces.space2, }, pointText: { fontSize: fonts.header2, fontWeight: 'bold', color: colors.text, }, imageContainer: { width: 150, height: 100, backgroundColor: colors.white, borderRadius: borderRadius.header, justifyContent: 'center', alignItems: 'center', }, }); |
Note: If during the copy paste, you have discovered a strange library, then please help me: 3
In the 3 files above, except for the animation
and mobx
, just the UI update for the app is more beautiful and feels closer to the user only. So I will not explain much about these 3 files
3) mobx
mobx
if you’ve ever done through redux
, the purpose of it is also to manage the state in the store effectively. Let’s take a quick look at the PracticeStore.js
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | import { observable, action, computed, decorate } from 'mobx'; class PracticeStore { firstParameter = 0; secondParameter = 0; isCorrect = true; point = 0; setFirstParameter(item) { this.firstParameter = item; } setSecondParameter(item) { this.secondParameter = item; } setPoint(item) { this.point = item; } get FirstParameter() { return this.firstParameter; } get SecondParameter() { return this.secondParameter; } get IsCorrect() { return this.isCorrect; } get Point() { return this.point; } randomNumber = (to, from) => { return Math.floor(Math.random() * from) + to; }; calculateResult = () => { const isTrue = Math.floor(Math.random() * 4); if (isTrue === 0) { return this.firstParameter + this.secondParameter; } const isBigger = Math.floor(Math.random() * 4); let resultFalse = 0; if (isBigger === 0) { resultFalse = this.firstParameter + this.secondParameter + Math.floor(Math.random() * 2); } else { resultFalse = this.firstParameter + this.secondParameter - Math.floor(Math.random() * 2); } return resultFalse < 0 ? 0 : resultFalse; }; } decorate(PracticeStore, { firstParameter: observable, secondParameter: observable, isCorrect: observable, point: observable, FirstParameter: computed, SecondParameter: computed, IsCorrect: computed, Point: computed, setFirstParameter: action, setSecondParameter: action, setPoint: action, }); const practiceStore = new PracticeStore(); export default practiceStore; |
If you read carefully, you will see that the variables decorated with the keyword are observable
, what does that mean? It can be said that observable
similar to the state
in React, when the variable decorated by observable
changes the value, it will cause the Component that uses them to be re-rendered, provided that the Component must be wrapped by observer like this export default observer(Component)
. For functions that get data, it will be decorated as computed
, and functions that affect and handle observable
variables will be decorated as action
. It sounds simple, right? If you have used Redux
, you will see mobx
compact mobx
is, or if you have not read it, read through your comparison written earlier, link here
You can read more information about mobx in its homepage.
4) Finish
So we have finished updating the math app, now let’s look at its shape.
When I first came in
Click the play button
Win 2 sentences and get 2 points)
Go to the 3rd sentence, it will be lost
My article here is over, hope to see you in part 3, thank you for taking the time to read my article.