React Native: Difference between revisions
No edit summary |
|||
(38 intermediate revisions by the same user not shown) | |||
Line 6: | Line 6: | ||
*Shared across platforms | *Shared across platforms | ||
*Good community | *Good community | ||
==Components | ==Components== | ||
*React uses components to build apps | *React uses components to build apps | ||
*React Native has many components | *React Native has many components | ||
*Including translate features | *Including translate features | ||
==Installation== | ==Installation== | ||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
Line 26: | Line 27: | ||
<br> | <br> | ||
App.js | App.js | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="jsx"> | ||
import React from 'react'; | import React from 'react'; | ||
import Home from './app/views/Home.js' | import Home from './app/views/Home.js' | ||
Line 39: | Line 40: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
<br> | <br> | ||
Home.js | |||
<syntaxhighlight lang="js"> | <syntaxhighlight lang="js"> | ||
import React from "react"; | import React from "react"; | ||
Line 56: | Line 57: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Styles= | |||
==Inline Styles== | |||
These can be defined with double brackets. Note React supports flex :) | |||
<syntaxhighlight lang="jsx"> | |||
.. | |||
return ( | |||
<View style={styles.container}> | |||
<Header message = 'Press to Login'></Header> | |||
<Text style={{flex:8}}>This will be the homepage</Text> | |||
<Text style={{flex:6}}>These other lines are here</Text> | |||
</View> | |||
) | |||
.. | |||
</syntaxhighlight> | |||
==Using const Styles== | |||
Simple create a style and attach it to the component | |||
<syntaxhighlight lang="js"> | |||
import React, { useState } from 'react'; | |||
import {StyleSheet, Text, View} from 'react-native'; | |||
const Header = (props) => { | |||
const [loggedIn, setLoggedIn] = useState(false); | |||
const toggleUser = () => { | |||
const newLoggedIn = loggedIn ? false : true | |||
setLoggedIn( newLoggedIn) | |||
} | |||
const display = loggedIn ? 'Sample User' : props.message | |||
return ( | |||
<View style={styles.headStyle}> | |||
<Text style={styles.headText} onPress={toggleUser}>{display}</Text> | |||
</View> | |||
) | |||
} | |||
const styles = StyleSheet.create({ | |||
headText: { | |||
textAlign: 'right', | |||
color: '#ffffff', | |||
fontSize: 30, | |||
}, | |||
headStyle: { | |||
paddingTop: 30, | |||
paddingBottom: 10, | |||
paddingRight: 10, | |||
backgroundColor: '#35605a' | |||
}, | |||
}) | |||
export default Header | |||
</syntaxhighlight> | |||
=Platform Support= | |||
There are API in the Platform package to support the platforms. These provide helpers for things which are platform specific e.g. version, dimensions and others. You can have React load the appropriate js by name a file Home.ios.js and Home.android.js and it will load the correct one. | |||
=Using Images= | |||
This is how to use the image without assets. | |||
<syntaxhighlight lang="jsx"> | |||
... | |||
import { StyleSheet, Text, View, Image } from "react-native"; | |||
... | |||
return ( | |||
<View style={styles.headStyle}> | |||
<Image | |||
style={styles.logoStyle} | |||
source={require("./img/Globo_logo_REV.png")} | |||
/> | |||
<Text style={styles.headText} onPress={toggleUser}> | |||
{display} | |||
</Text> | |||
</View> | |||
); | |||
.... | |||
const styles = StyleSheet.create({ | |||
... | |||
logoStyle: { | |||
flex: 1, | |||
width: undefined, | |||
height: undefined, | |||
}, | |||
}); | |||
</syntaxhighlight> | |||
=Detecting Touch= | |||
==Alert== | |||
Could not get this to work on the web so here is an implementation | |||
<syntaxhighlight lang="js"> | |||
import { Alert, Platform } from 'react-native' | |||
const alertPolyfill = (title, description, options, extra) => { | |||
const result = window.confirm([title, description].filter(Boolean).join('\n')) | |||
if (result) { | |||
const confirmOption = options.find(({ style }) => style !== 'cancel') | |||
confirmOption && confirmOption.onPress() | |||
} else { | |||
const cancelOption = options.find(({ style }) => style === 'cancel') | |||
cancelOption && cancelOption.onPress() | |||
} | |||
} | |||
const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert | |||
export default alert | |||
</syntaxhighlight> | |||
==Detecting Press== | |||
This was the approach. Not sure the benefits between TouchableOpacity. Probably looks nice. | |||
<syntaxhighlight lang="js"> | |||
<TouchableOpacity style={styles.buttonStyles} onPress={onPress}> | |||
<Text style={styles.buttonText}>ABOUT</Text> | |||
</TouchableOpacity> | |||
</syntaxhighlight> | |||
=Navigation= | |||
==Install== | |||
We are going to use react-navigation | |||
<syntaxhighlight lang="json"> | |||
"@react-native-community/masked-view": "^0.1.10", | |||
"@react-navigation/native": "^5.8.10", | |||
"@react-navigation/stack": "^5.12.8", | |||
</syntaxhighlight> | |||
==Configure== | |||
In App.js create an object containing all of the routes and the initial route | |||
<syntaxhighlight lang="jsx"> | |||
import React from "react"; | |||
import { NavigationContainer } from "@react-navigation/native"; | |||
import { createStackNavigator } from "@react-navigation/stack"; | |||
import Contact from "./views/Contact"; | |||
import Home from "./views/Home"; | |||
const Stack = createStackNavigator(); | |||
export function MyStack() { | |||
return ( | |||
<NavigationContainer> | |||
<Stack.Navigator | |||
initialRouteName="HomeRoute" | |||
screenOptions={{ | |||
headerShown: false, | |||
}} | |||
> | |||
<Stack.Screen name="HomeRoute" component={Home} /> | |||
<Stack.Screen name="ContactRoute" component={Contact} /> | |||
</Stack.Navigator> | |||
</NavigationContainer> | |||
); | |||
} | |||
export default MyStack; | |||
</syntaxhighlight> | |||
==Implement in App== | |||
We need to change the app to use the routing instead on a hardcoded activity | |||
<syntaxhighlight lang="js"> | |||
import React from 'react'; | |||
import { MyStack } from './app/Routing'; | |||
function App() { | |||
return ( | |||
<MyStack /> | |||
) | |||
} | |||
export default App | |||
</syntaxhighlight> | |||
==Implement in Component(Activity)== | |||
In the initial component make sure we pass the navigation to the menu. | |||
<syntaxhighlight lang="jsx"> | |||
const Home = (props) => { | |||
const { navigate } = props.navigation; | |||
return ( | |||
<View style={styles.container}> | |||
<Header message="Press to Login"></Header> | |||
<Hero /> | |||
<Menu navigate={navigate} /> | |||
</View> | |||
); | |||
}; | |||
</syntaxhighlight> | |||
==Passing Parameters== | |||
Passing parameters with React Navigation changed in later versions. You can pass parameters to the navigated page with | |||
<syntaxhighlight lang="jsx"> | |||
const onPress = () => { | |||
props.navigate('VideoDetailRoute', { ytubeId: props.id}) | |||
} | |||
</syntaxhighlight> | |||
To receive the values | |||
<syntaxhighlight lang="jsx"> | |||
const { route, navigation } = props; | |||
const tubeId = route.params.ytubeId; | |||
</syntaxhighlight> | |||
==Implement in Menu== | |||
So now we have navigate passed around we can use it in the Menu. | |||
<syntaxhighlight lang="jsx"> | |||
<View style={styles.buttonRow}> | |||
<TouchableOpacity style={styles.buttonStyles} onPress={onPress}> | |||
<Text style={styles.buttonText}>BLOG</Text> | |||
</TouchableOpacity> | |||
<TouchableOpacity style={styles.buttonStyles} onPress={()=>props.navigate('ContactRoute')}> | |||
<Text style={styles.buttonText}>CONTACT</Text> | |||
</TouchableOpacity> | |||
</View> | |||
</syntaxhighlight> | |||
=Using External Data and Libraries= | |||
==Video Library== | |||
They used google to start this project. Log onto https://console.developers.google.com/ and create a project from the My Project dropdown. Hit the + to create a new project and select YouTube Data API v3 and Enable it. Got to credentials and create an API key. To implement the fetch we use fetch and useEffect to retrieve the data at startup. | |||
<syntaxhighlight lang="jsx" | |||
... | |||
useEffect(() => { | |||
getVideo(); | |||
}, []); // <-- empty dependency array | |||
const getVideo = () => { | |||
const key = "xxxxxx"; | |||
const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=pluralsight&type=video&key=${key}` | |||
return fetch(url) | |||
.then((response) => { | |||
return response.json(); | |||
}) | |||
.then((responseJson) => { | |||
const data = Array.from(responseJson.items); | |||
setVideoList(data); | |||
setListLoaded(true); | |||
}) | |||
.catch((error) => { | |||
console.log("error received is :", error); | |||
}); | |||
}; | |||
</syntaxhighlight> | |||
==An example of Flatlist== | |||
Just in case I need it an example of formatting the json in a flatlist | |||
<syntaxhighlight lang="jsx" | |||
<View style={{ paddingTop: 30 }}> | |||
<FlatList | |||
data={videoList} | |||
keyExtractor={(item, index) => item.id.videoId} | |||
renderItem={({ item }) => ( | |||
<TubeItem | |||
navigate={props.navigate} | |||
id={item.id.videoId} | |||
title={item.snippet.title} | |||
imageSrc={item.snippet.thumbnails.high.url} | |||
/> | |||
)} | |||
/> | |||
</View> | |||
</syntaxhighlight> | |||
==Extra Notes== | |||
The WebView component does not work on Web with Expo but does on Android | |||
=AsyncStorage= | |||
==Login== | |||
Here is an example login screen demonstrating AsyncStorage. The onChangeText required react-native-gesture-handler to be installed. Not clear in the errors | |||
<syntaxhighlight lang="jsx"> | |||
import React, { useState, useEffect } from "react"; | |||
import { Text,View, Alert, StyleSheet } from "react-native"; | |||
import { TextInput, TouchableHighlight } from "react-native-gesture-handler"; | |||
import AsyncStorage from '@react-native-community/async-storage' | |||
const Register = (props) => { | |||
useEffect(() => { | |||
console.log(`Register Props =`, props); | |||
}, []); // <-- empty dependency array | |||
const [username, setUsername] = useState(''); | |||
const [password, setPassword] = useState(''); | |||
const [passwordConfirm, setPasswordConfirm] = useState(''); | |||
const cancelRegistration = () => { | |||
props.navigation.navigate("HomeRoute"); | |||
}; | |||
const registerAccount = () => { | |||
if (!username) { | |||
Alert.alert("Please Enter Username"); | |||
console.log('Username is empty') | |||
} else if (password !== passwordConfirm) { | |||
Alert.alert("Password must match"); | |||
console.log('passwords must match') | |||
} else { | |||
AsyncStorage.getItem(username, (err, result) => { | |||
if (result !== null) { | |||
Alert.alert(`${username} already exists`); | |||
console.log('account already exists') | |||
} else { | |||
AsyncStorage.setItem(username, password, (err, result) => { | |||
Alert.alert(`${username} account created`); | |||
console.log('account created') | |||
props.navigation.navigate("HomeRoute"); | |||
}); | |||
} | |||
}); | |||
} | |||
}; | |||
return ( | |||
<View style={styles.container}> | |||
<Text style={styles.label}>Register Account</Text> | |||
<TextInput | |||
style={styles.inputs} | |||
onChangeText={setUsername} | |||
value={username} | |||
/> | |||
<Text style={styles.label}>Enter Username</Text> | |||
<TextInput | |||
style={styles.inputs} | |||
onChangeText={setPassword} | |||
value={password} | |||
secureTextEntry={true} | |||
/> | |||
<Text style={styles.label}>Enter Password</Text> | |||
<TextInput | |||
style={styles.inputs} | |||
onChangeText={setPasswordConfirm} | |||
value={passwordConfirm} | |||
secureTextEntry={true} | |||
/> | |||
<Text style={styles.label}>Confirm Password</Text> | |||
<TouchableHighlight onPress={() => registerAccount()} underlayColor="#31e981"> | |||
<Text style={styles.buttons}>Register</Text> | |||
</TouchableHighlight> | |||
<TouchableHighlight onPress={ () => cancelRegistration()} underlayColor="#31e981"> | |||
<Text style={styles.buttons}>Cancel</Text> | |||
</TouchableHighlight> | |||
</View> | |||
); | |||
}; | |||
const styles = StyleSheet.create({ | |||
container: { | |||
flex: 1, | |||
alignItems: 'center', | |||
paddingBottom: '45%', | |||
paddingTop: '10%' | |||
}, | |||
heading: { | |||
fontSize: 16, | |||
flex: 1 | |||
}, | |||
inputs: { | |||
flex: 1, | |||
width: '80%', | |||
padding: 10, | |||
}, | |||
buttons: { | |||
marginTop: 15, | |||
fontSize: 16, | |||
}, | |||
labels: { | |||
paddingBottom: 10, | |||
}, | |||
}) | |||
export default Register; | |||
</syntaxhighlight> | |||
==Header== | |||
Here is the user of login. The import thing was the useEffect which required me to add a listener to work properly | |||
<syntaxhighlight lang="jsx"> | |||
import AsyncStorage from "@react-native-community/async-storage"; | |||
import React, { useState, useEffect } from "react"; | |||
import { StyleSheet, Text, View, Image, Alert } from "react-native"; | |||
const Header = (props) => { | |||
useEffect(() => { | |||
console.log(`Header Props =`, props); | |||
const unsubscribe = props.navigation.addListener("focus", () => { | |||
AsyncStorage.getItem("userLoggedIn", (err, result) => { | |||
if (result === "none") { | |||
console.log("NOME"); | |||
} else if (result === null) { | |||
AsyncStorage.setItem("userLoggedIn", "none", (err, result) => { | |||
console.log("User set to none"); | |||
}); | |||
} else { | |||
setIsLoggedIn(true); | |||
setLoggedUser(result); | |||
console.log("Setting to logged in"); | |||
} | |||
}); | |||
return unsubscribe; | |||
}); | |||
}, [props.navigation]); // <-- empty dependency array | |||
const [isLoggedIn, setIsLoggedIn] = useState(false); | |||
const [loggedUser, setLoggedUser] = useState(false); | |||
const toggleUser = () => { | |||
if (isLoggedIn) { | |||
AsyncStorage.setItem("userLoggedIn", "none", (err, result) => { | |||
setIsLoggedIn(false); | |||
setLoggedUser(false); | |||
}); | |||
Alert.alert("User logged out"); | |||
console.log("User logged out"); | |||
} else { | |||
navigate("LoginRoute"); | |||
} | |||
}; | |||
const {navigate} = props.navigation | |||
const display = isLoggedIn ? loggedUser : props.message; | |||
return ( | |||
<View style={styles.headStyle}> | |||
<Image | |||
style={styles.logoStyle} | |||
source={require("./img/Globo_logo_REV.png")} | |||
/> | |||
<Text style={styles.headText} onPress={toggleUser}> | |||
{display} | |||
</Text> | |||
</View> | |||
); | |||
}; | |||
const styles = StyleSheet.create({ | |||
headText: { | |||
textAlign: "right", | |||
color: "#ffffff", | |||
fontSize: 20, | |||
flex: 1, | |||
}, | |||
headStyle: { | |||
paddingTop: 30, | |||
paddingRight: 10, | |||
backgroundColor: "#35605a", | |||
flex: 1, | |||
flexDirection: "row", | |||
borderBottomWidth: 2, | |||
borderColor: "#000000", | |||
}, | |||
logoStyle: { | |||
flex: 1, | |||
width: undefined, | |||
height: undefined, | |||
}, | |||
}); | |||
export default Header; | |||
</syntaxhighlight> | |||
=Quiz= | |||
==RenderItem Revisited== | |||
Well the quiz proved to be the hard part here and it was down to render item. In the flatlist the items are not re-rendered by default and therefore any update to the state are not known to the update function. To solve this you pass extraData with a toggled state. In my case refresh | |||
<syntaxhighlight lang="jsx"> | |||
... | |||
// useEffect tracks the changes to questionAnswered and toggles the refresh value in state | |||
useEffect(() => { | |||
console.log("Doing useEffect again"); | |||
setQuestLoaded(true); | |||
setRefresh(refresh => !refresh) | |||
console.log(`questionAnswered in useEffect = `, questionAnswered); | |||
}, [questionAnswered]); // <-- empty dependency array | |||
... | |||
// A much nice declarative renderItem | |||
const renderItem = ({item,index}) => ( | |||
<Questions | |||
question={item.question} | |||
answer1={item.answer1} | |||
answer2={item.answer2} | |||
answer3={item.answer3} | |||
answer4={item.answer4} | |||
correctAnswer={item.correctAnswer} | |||
scoreUpdate={updateScore} | |||
/> | |||
) | |||
... | |||
// Finally the Flatlist which passes the refresh flag | |||
return ( | |||
<View style={styles.container}> | |||
{questLoaded && ( | |||
<View> | |||
<FlatList | |||
extraData={refresh} | |||
data={questList} | |||
renderItem={(item,index)=>RenderItem(item,index)} | |||
keyExtractor={item=>item.key} | |||
/> | |||
</View> | |||
)} | |||
... | |||
</syntaxhighlight> | |||
==Wordpress REST API and React Render== | |||
===Wordpress REST API=== | |||
Finally built the render blog screen. The wordpress REST API was something I did not know came free but the results were disappointing. Anyway here is how to use it | |||
<syntaxhighlight lang="jsx"> | |||
... | |||
const getBlogDetail = () => { | |||
const url = `http://xxx.xxx.xxx.xxx/wordpress/wp-json/wp/v2/posts/${blogId}?_embed`; | |||
fetch(url) | |||
.then((response) => response.json()) | |||
.then((responseJson) => { | |||
setPostLoaded(true); | |||
setPostTitle(responseJson.title); | |||
setPostImage(mediaUrl(responseJson)); | |||
setPostContent(responseJson.content); | |||
setPostID(responseJson.id); | |||
}) | |||
.catch((error) => { | |||
console.error(error); | |||
}); | |||
}; | |||
... | |||
// Extraction of the Featured Media Url | |||
const mediaUrl = (obj) => { | |||
try { | |||
return obj._embedded["wp:featuredmedia"][0].source_url; | |||
} catch (error) { | |||
return ""; | |||
} | |||
}; | |||
</syntaxhighlight> | |||
===React Render=== | |||
Had to install this but code it to work. As I say the result was pretty awful but here is the code. | |||
<syntaxhighlight lang="jsx"> | |||
<HTML | |||
// ignoredStyles={["font-family", 'padding','transform', 'font-weight',"letter-spacing", "display", "color"]} | |||
// style={{textAlign: 'center', alignSelf:'center', paddingSide:10}} | |||
source={{html: postContent.rendered}} | |||
imagesMaxWidth={Dimensions.get('window').width } | |||
classesStyles={blogClassStyles} | |||
onLinkPress={goBack} | |||
/> | |||
</syntaxhighlight> | |||
=Building an APK= | |||
==Install Gradle== | |||
Had to install this so here are the easy googlable commands. No alarms and no surprises here - move along | |||
<syntaxhighlight lang="bash"> | |||
sudo apt -y install vim apt-transport-https dirmngr wget software-properties-common | |||
sudo add-apt-repository ppa:cwchien/gradle | |||
sudo apt-get update | |||
sudo apt -y install gradle | |||
</syntaxhighlight> | |||
==Fix react-native-cli== | |||
I could not get it to build without installing this package and could not get the install to work without brute force | |||
<syntaxhighlight lang="bash"> | |||
sudo npm install -i -g --force react-native-cli | |||
</syntaxhighlight> | |||
==Attempt 1== | |||
I looked at build this according to the websites using Attempt 1 but failed I switch to a clean build with Attempt 2 | |||
===Make a keystore=== | |||
Make a keystore using your own your_key_name and your_key_alias with | |||
<syntaxhighlight lang="bash"> | |||
keytool -genkey -v -keystore your_key_name.keystore -alias your_key_alias -keyalg RSA -keysize 2048 -validity 10000 | |||
</syntaxhighlight> | |||
===Copy to App=== | |||
Put this in the app directory. | |||
<syntaxhighlight lang="bash"> | |||
cp <keystore> ~/dev/<PROJECT>/app/ | |||
</syntaxhighlight> | |||
===Add to Gradle=== | |||
We need to add a prompt for this in the gradle file. You can hard code it is but this is the recommend approach. The build.gradle is in ~/dev/<PROJECT>/android/app/build.gradle | |||
<syntaxhighlight lang="groovy"> | |||
apply plugin: "com.android.application" | |||
import com.android.build.OutputFile | |||
... | |||
release { | |||
storeFile file('bibbledroid.keystore') | |||
storePassword | |||
if(System.console() != null) | |||
System.console().readLine("\nKeystore password:") | |||
keyAlias | |||
if(System.console() != null) | |||
System.console().readLine("\nAlias: ") | |||
keyPassword | |||
if(System.console() != null) | |||
System.console().readLine("\nAlias password: ") | |||
} | |||
... | |||
// Run this once to be able to run the application with BUCK | |||
// puts all compile dependencies into folder libs for BUCK to use | |||
task copyDownloadableDepsToLibs(type: Copy) { | |||
from configurations.compile | |||
into 'libs' | |||
} | |||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) | |||
</syntaxhighlight> | |||
===Final Setup before Build=== | |||
<syntaxhighlight lang="bash"> | |||
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/ | |||
</syntaxhighlight> | |||
This produced the output | |||
<syntaxhighlight lang="bash"> | |||
info Writing bundle output to:, android/app/src/main/assets/index.android.bundle | |||
info Done writing bundle output | |||
info Copying 17 asset files | |||
info Done copying assets | |||
</syntaxhighlight> | |||
==Attempt 2== | |||
Install the NDK using Android Studio SDK manager. Created a new app with | |||
<syntaxhighlight lang="bash"> | |||
npx create-react-native-app globo | |||
</syntaxhighlight> | |||
Added the following to packages.json | |||
<syntaxhighlight lang="json"> | |||
"@react-native-community/async-storage": "^1.12.1", | |||
"@react-native-community/masked-view": "^0.1.10", | |||
"@react-navigation/native": "^5.8.10", | |||
"@react-navigation/stack": "^5.12.8", | |||
"react-native-webview": "^11.0.2", | |||
"react-native-render-html": "^5.0.0", | |||
"react-native-safe-area-context": "^3.1.9", | |||
</syntaxhighlight> | |||
Fixed the use of the network with android by adding usesCleartextTraffic to the AndroidManifest.xml | |||
<syntaxhighlight lang="xml"> | |||
<!-- END OPTIONAL PERMISSIONS --> | |||
... | |||
<application | |||
android:name=".MainApplication" | |||
android:label="@string/app_name" | |||
android:icon="@mipmap/ic_launcher" | |||
android:roundIcon="@mipmap/ic_launcher_round" | |||
android:allowBackup="false" | |||
android:theme="@style/AppTheme" | |||
android:usesCleartextTraffic="true" | |||
... | |||
> | |||
</syntaxhighlight> | |||
Build the package | |||
<syntaxhighlight lang="xml"> | |||
react-native run-android --variant=release | |||
</syntaxhighlight> | |||
This results in a package in ./android/app/build/outputs/apk/release/app-release.apk |
Latest revision as of 23:57, 22 February 2021
Introduction
Why
- True native app
- Performance is great
- Easy to learn
- Shared across platforms
- Good community
Components
- React uses components to build apps
- React Native has many components
- Including translate features
Installation
sudo npm i -g react-native-cli
sudo npm i -g create-react-native
Create Project
Note ignite is a useful tool for creating components in react-native
react-native init reactiveNativeCLI
npx create-react-native-app globo
expo start
Sample App.js
This is not much different to java and Xamarin.
App.js
import React from 'react';
import Home from './app/views/Home.js'
export default class App extends React.Component {
render() {
return (
<Home />
)
}
}
Home.js
import React from "react";
import { Text, View } from "react-native";
export default class Home extends React.Component {
render() {
return (
<View>
<Text>This will be the homepage</Text>
<Text>These other lines are here</Text>
<Text>So you are not mad</Text>
</View>
)
}
}
Styles
Inline Styles
These can be defined with double brackets. Note React supports flex :)
..
return (
<View style={styles.container}>
<Header message = 'Press to Login'></Header>
<Text style={{flex:8}}>This will be the homepage</Text>
<Text style={{flex:6}}>These other lines are here</Text>
</View>
)
..
Using const Styles
Simple create a style and attach it to the component
import React, { useState } from 'react';
import {StyleSheet, Text, View} from 'react-native';
const Header = (props) => {
const [loggedIn, setLoggedIn] = useState(false);
const toggleUser = () => {
const newLoggedIn = loggedIn ? false : true
setLoggedIn( newLoggedIn)
}
const display = loggedIn ? 'Sample User' : props.message
return (
<View style={styles.headStyle}>
<Text style={styles.headText} onPress={toggleUser}>{display}</Text>
</View>
)
}
const styles = StyleSheet.create({
headText: {
textAlign: 'right',
color: '#ffffff',
fontSize: 30,
},
headStyle: {
paddingTop: 30,
paddingBottom: 10,
paddingRight: 10,
backgroundColor: '#35605a'
},
})
export default Header
Platform Support
There are API in the Platform package to support the platforms. These provide helpers for things which are platform specific e.g. version, dimensions and others. You can have React load the appropriate js by name a file Home.ios.js and Home.android.js and it will load the correct one.
Using Images
This is how to use the image without assets.
...
import { StyleSheet, Text, View, Image } from "react-native";
...
return (
<View style={styles.headStyle}>
<Image
style={styles.logoStyle}
source={require("./img/Globo_logo_REV.png")}
/>
<Text style={styles.headText} onPress={toggleUser}>
{display}
</Text>
</View>
);
....
const styles = StyleSheet.create({
...
logoStyle: {
flex: 1,
width: undefined,
height: undefined,
},
});
Detecting Touch
Alert
Could not get this to work on the web so here is an implementation
import { Alert, Platform } from 'react-native'
const alertPolyfill = (title, description, options, extra) => {
const result = window.confirm([title, description].filter(Boolean).join('\n'))
if (result) {
const confirmOption = options.find(({ style }) => style !== 'cancel')
confirmOption && confirmOption.onPress()
} else {
const cancelOption = options.find(({ style }) => style === 'cancel')
cancelOption && cancelOption.onPress()
}
}
const alert = Platform.OS === 'web' ? alertPolyfill : Alert.alert
export default alert
Detecting Press
This was the approach. Not sure the benefits between TouchableOpacity. Probably looks nice.
<TouchableOpacity style={styles.buttonStyles} onPress={onPress}>
<Text style={styles.buttonText}>ABOUT</Text>
</TouchableOpacity>
Install
We are going to use react-navigation
"@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
Configure
In App.js create an object containing all of the routes and the initial route
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import Contact from "./views/Contact";
import Home from "./views/Home";
const Stack = createStackNavigator();
export function MyStack() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="HomeRoute"
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="HomeRoute" component={Home} />
<Stack.Screen name="ContactRoute" component={Contact} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default MyStack;
Implement in App
We need to change the app to use the routing instead on a hardcoded activity
import React from 'react';
import { MyStack } from './app/Routing';
function App() {
return (
<MyStack />
)
}
export default App
Implement in Component(Activity)
In the initial component make sure we pass the navigation to the menu.
const Home = (props) => {
const { navigate } = props.navigation;
return (
<View style={styles.container}>
<Header message="Press to Login"></Header>
<Hero />
<Menu navigate={navigate} />
</View>
);
};
Passing Parameters
Passing parameters with React Navigation changed in later versions. You can pass parameters to the navigated page with
const onPress = () => {
props.navigate('VideoDetailRoute', { ytubeId: props.id})
}
To receive the values
const { route, navigation } = props;
const tubeId = route.params.ytubeId;
Implement in Menu
So now we have navigate passed around we can use it in the Menu.
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.buttonStyles} onPress={onPress}>
<Text style={styles.buttonText}>BLOG</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buttonStyles} onPress={()=>props.navigate('ContactRoute')}>
<Text style={styles.buttonText}>CONTACT</Text>
</TouchableOpacity>
</View>
Using External Data and Libraries
Video Library
They used google to start this project. Log onto https://console.developers.google.com/ and create a project from the My Project dropdown. Hit the + to create a new project and select YouTube Data API v3 and Enable it. Got to credentials and create an API key. To implement the fetch we use fetch and useEffect to retrieve the data at startup.
{
getVideo();
}, []); // <-- empty dependency array
const getVideo = () => {
const key = "xxxxxx";
const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=pluralsight&type=video&key=${key}`
return fetch(url)
.then((response) => {
return response.json();
})
.then((responseJson) => {
const data = Array.from(responseJson.items);
setVideoList(data);
setListLoaded(true);
})
.catch((error) => {
console.log("error received is :", error);
});
};
An example of Flatlist
Just in case I need it an example of formatting the json in a flatlist
<FlatList
data={videoList}
keyExtractor={(item, index) => item.id.videoId}
renderItem={({ item }) => (
<TubeItem
navigate={props.navigate}
id={item.id.videoId}
title={item.snippet.title}
imageSrc={item.snippet.thumbnails.high.url}
/>
)}
/>
</View>
Extra Notes
The WebView component does not work on Web with Expo but does on Android
AsyncStorage
Login
Here is an example login screen demonstrating AsyncStorage. The onChangeText required react-native-gesture-handler to be installed. Not clear in the errors
import React, { useState, useEffect } from "react";
import { Text,View, Alert, StyleSheet } from "react-native";
import { TextInput, TouchableHighlight } from "react-native-gesture-handler";
import AsyncStorage from '@react-native-community/async-storage'
const Register = (props) => {
useEffect(() => {
console.log(`Register Props =`, props);
}, []); // <-- empty dependency array
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const cancelRegistration = () => {
props.navigation.navigate("HomeRoute");
};
const registerAccount = () => {
if (!username) {
Alert.alert("Please Enter Username");
console.log('Username is empty')
} else if (password !== passwordConfirm) {
Alert.alert("Password must match");
console.log('passwords must match')
} else {
AsyncStorage.getItem(username, (err, result) => {
if (result !== null) {
Alert.alert(`${username} already exists`);
console.log('account already exists')
} else {
AsyncStorage.setItem(username, password, (err, result) => {
Alert.alert(`${username} account created`);
console.log('account created')
props.navigation.navigate("HomeRoute");
});
}
});
}
};
return (
<View style={styles.container}>
<Text style={styles.label}>Register Account</Text>
<TextInput
style={styles.inputs}
onChangeText={setUsername}
value={username}
/>
<Text style={styles.label}>Enter Username</Text>
<TextInput
style={styles.inputs}
onChangeText={setPassword}
value={password}
secureTextEntry={true}
/>
<Text style={styles.label}>Enter Password</Text>
<TextInput
style={styles.inputs}
onChangeText={setPasswordConfirm}
value={passwordConfirm}
secureTextEntry={true}
/>
<Text style={styles.label}>Confirm Password</Text>
<TouchableHighlight onPress={() => registerAccount()} underlayColor="#31e981">
<Text style={styles.buttons}>Register</Text>
</TouchableHighlight>
<TouchableHighlight onPress={ () => cancelRegistration()} underlayColor="#31e981">
<Text style={styles.buttons}>Cancel</Text>
</TouchableHighlight>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
paddingBottom: '45%',
paddingTop: '10%'
},
heading: {
fontSize: 16,
flex: 1
},
inputs: {
flex: 1,
width: '80%',
padding: 10,
},
buttons: {
marginTop: 15,
fontSize: 16,
},
labels: {
paddingBottom: 10,
},
})
export default Register;
Header
Here is the user of login. The import thing was the useEffect which required me to add a listener to work properly
import AsyncStorage from "@react-native-community/async-storage";
import React, { useState, useEffect } from "react";
import { StyleSheet, Text, View, Image, Alert } from "react-native";
const Header = (props) => {
useEffect(() => {
console.log(`Header Props =`, props);
const unsubscribe = props.navigation.addListener("focus", () => {
AsyncStorage.getItem("userLoggedIn", (err, result) => {
if (result === "none") {
console.log("NOME");
} else if (result === null) {
AsyncStorage.setItem("userLoggedIn", "none", (err, result) => {
console.log("User set to none");
});
} else {
setIsLoggedIn(true);
setLoggedUser(result);
console.log("Setting to logged in");
}
});
return unsubscribe;
});
}, [props.navigation]); // <-- empty dependency array
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loggedUser, setLoggedUser] = useState(false);
const toggleUser = () => {
if (isLoggedIn) {
AsyncStorage.setItem("userLoggedIn", "none", (err, result) => {
setIsLoggedIn(false);
setLoggedUser(false);
});
Alert.alert("User logged out");
console.log("User logged out");
} else {
navigate("LoginRoute");
}
};
const {navigate} = props.navigation
const display = isLoggedIn ? loggedUser : props.message;
return (
<View style={styles.headStyle}>
<Image
style={styles.logoStyle}
source={require("./img/Globo_logo_REV.png")}
/>
<Text style={styles.headText} onPress={toggleUser}>
{display}
</Text>
</View>
);
};
const styles = StyleSheet.create({
headText: {
textAlign: "right",
color: "#ffffff",
fontSize: 20,
flex: 1,
},
headStyle: {
paddingTop: 30,
paddingRight: 10,
backgroundColor: "#35605a",
flex: 1,
flexDirection: "row",
borderBottomWidth: 2,
borderColor: "#000000",
},
logoStyle: {
flex: 1,
width: undefined,
height: undefined,
},
});
export default Header;
Quiz
RenderItem Revisited
Well the quiz proved to be the hard part here and it was down to render item. In the flatlist the items are not re-rendered by default and therefore any update to the state are not known to the update function. To solve this you pass extraData with a toggled state. In my case refresh
...
// useEffect tracks the changes to questionAnswered and toggles the refresh value in state
useEffect(() => {
console.log("Doing useEffect again");
setQuestLoaded(true);
setRefresh(refresh => !refresh)
console.log(`questionAnswered in useEffect = `, questionAnswered);
}, [questionAnswered]); // <-- empty dependency array
...
// A much nice declarative renderItem
const renderItem = ({item,index}) => (
<Questions
question={item.question}
answer1={item.answer1}
answer2={item.answer2}
answer3={item.answer3}
answer4={item.answer4}
correctAnswer={item.correctAnswer}
scoreUpdate={updateScore}
/>
)
...
// Finally the Flatlist which passes the refresh flag
return (
<View style={styles.container}>
{questLoaded && (
<View>
<FlatList
extraData={refresh}
data={questList}
renderItem={(item,index)=>RenderItem(item,index)}
keyExtractor={item=>item.key}
/>
</View>
)}
...
Wordpress REST API and React Render
Wordpress REST API
Finally built the render blog screen. The wordpress REST API was something I did not know came free but the results were disappointing. Anyway here is how to use it
...
const getBlogDetail = () => {
const url = `http://xxx.xxx.xxx.xxx/wordpress/wp-json/wp/v2/posts/${blogId}?_embed`;
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
setPostLoaded(true);
setPostTitle(responseJson.title);
setPostImage(mediaUrl(responseJson));
setPostContent(responseJson.content);
setPostID(responseJson.id);
})
.catch((error) => {
console.error(error);
});
};
...
// Extraction of the Featured Media Url
const mediaUrl = (obj) => {
try {
return obj._embedded["wp:featuredmedia"][0].source_url;
} catch (error) {
return "";
}
};
React Render
Had to install this but code it to work. As I say the result was pretty awful but here is the code.
<HTML
// ignoredStyles={["font-family", 'padding','transform', 'font-weight',"letter-spacing", "display", "color"]}
// style={{textAlign: 'center', alignSelf:'center', paddingSide:10}}
source={{html: postContent.rendered}}
imagesMaxWidth={Dimensions.get('window').width }
classesStyles={blogClassStyles}
onLinkPress={goBack}
/>
Building an APK
Install Gradle
Had to install this so here are the easy googlable commands. No alarms and no surprises here - move along
sudo apt -y install vim apt-transport-https dirmngr wget software-properties-common
sudo add-apt-repository ppa:cwchien/gradle
sudo apt-get update
sudo apt -y install gradle
Fix react-native-cli
I could not get it to build without installing this package and could not get the install to work without brute force
sudo npm install -i -g --force react-native-cli
Attempt 1
I looked at build this according to the websites using Attempt 1 but failed I switch to a clean build with Attempt 2
Make a keystore
Make a keystore using your own your_key_name and your_key_alias with
keytool -genkey -v -keystore your_key_name.keystore -alias your_key_alias -keyalg RSA -keysize 2048 -validity 10000
Copy to App
Put this in the app directory.
cp <keystore> ~/dev/<PROJECT>/app/
Add to Gradle
We need to add a prompt for this in the gradle file. You can hard code it is but this is the recommend approach. The build.gradle is in ~/dev/<PROJECT>/android/app/build.gradle
apply plugin: "com.android.application"
import com.android.build.OutputFile
...
release {
storeFile file('bibbledroid.keystore')
storePassword
if(System.console() != null)
System.console().readLine("\nKeystore password:")
keyAlias
if(System.console() != null)
System.console().readLine("\nAlias: ")
keyPassword
if(System.console() != null)
System.console().readLine("\nAlias password: ")
}
...
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
Final Setup before Build
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
This produced the output
info Writing bundle output to:, android/app/src/main/assets/index.android.bundle
info Done writing bundle output
info Copying 17 asset files
info Done copying assets
Attempt 2
Install the NDK using Android Studio SDK manager. Created a new app with
npx create-react-native-app globo
Added the following to packages.json
"@react-native-community/async-storage": "^1.12.1",
"@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"react-native-webview": "^11.0.2",
"react-native-render-html": "^5.0.0",
"react-native-safe-area-context": "^3.1.9",
Fixed the use of the network with android by adding usesCleartextTraffic to the AndroidManifest.xml
<!-- END OPTIONAL PERMISSIONS -->
...
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
...
>
Build the package
react-native run-android --variant=release
This results in a package in ./android/app/build/outputs/apk/release/app-release.apk