Home Screen | Detail Screen | New Screen |
---|---|---|
- Node.js
- Git
- Visual Studio Code
- Expo CLI:
npm install -g expo-cli
- Emulator (or Smartphone) with the Expo-Client installed (https://docs.expo.io/get-started/installation/#requirements)
- Expo-Docs: https://docs.expo.io
- create a new empty repo (without Readme.md and .gitignore file) called
my-favorite-food-2021
on GitHub, copy the clone url and clone the project cd my-favorite-food-2021
expo init .
(don't forget the dot! select the blank template in managed workflow)npm start
The app.json (docs) file contains configuration options for your project.
We need to change the following ones:
name
=>my-favorite-food
(the name of your app)slug
=>my-favorite-food
(the friendly URL name for publishing. For example,my-favorite-food
will refer to theexpo.io/@project-owner/my-favorite-food
project)icon
=>./assets/logo.png
(app icon - download)splash.backgroundColor
=>#ff9100
(color to fill the loading screen background)web.favicon
=>./assets/logo.png
(web favicon - download)
- set the
backgroundColor
from#fff
to#ff9100
- add a new style for the text. set the text
color
to#fff
We want to set the background color of the status bar (docs) to #ff9100
:
- install StatusBar package
expo install expo-status-bar
- import package
import { StatusBar } from 'expo-status-bar';
- make usage of the StatusBar Component
<StatusBar backgroundColor={'#ff9100'} translucent={true} style="light" />
- On android devices the StatusBar is not correct until the expo app has been loaded. We can adjust this in the
app.json
file. Add the following optiosn:
"androidStatusBar": {
"backgroundColor": "#ff9100",
"translucent": false,
"barStyle": "light-content"
}
- convert code in the App.js file to an ES6 arrow function
const HomeScreen = () => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
};
- create folder
components
- create file
Text.js
in thecomponents
folder - create a Headline component (use Text from react-native) and assign the following styles
fontSize: 30, color: 'black'
- create a Paragraph component (use Text from react-native) and assign the following styles
fontSize: 16, color: 'black'
- keep in mind to handle passed props (props.children => text to show in this case)
- export components (multiple exports)
- use both components in App.js
- create file
ImagePreview.js
in thecomponents
folder - create a ImagePreview component (use Image from react-native) and assign the following styles:
width: '100%', height: 200, resizeMode: 'cover'
- keep in mind to handle passed
source
prop (use as source in the Image component) - export component (default export)
- use component with local download and remote (https://github.com/karlhorky/my-favorite-food/blob/main/assets/pizza.jpg) sources in App.js
- create file
Button.js
in thecomponents
folder - create a Button component (use TouchableOpacity and Text from react-native) and assign the following styles.
- Wrapper (TouchableOpacity) :
{
display: 'flex',
alignItems: 'center',
backgroundColor: '#ff9100',
borderRadius: 10,
padding: 20,
width: '100%'
}
- Label (Text):
{
textTransform: 'uppercase',
fontSize: 14,
fontWeight: 'bold',
color: 'white',
textAlign: 'center'
}
- keep in mind to handle passed
onPress
prop (use as onPress in the TouchableHighlight component) - keep in mind to handle passed
label
prop (use as text-value in the Text component) - export component (default export)
- use component in App.js
There are multiple libraries to handle navigation in react-native. One of the most used is react-navigation
. See the docs here
If you think of a website, you know that there different URLs that you can go to - some libraries call these Routes. In a mobile app you don't have URLs, but you can have different views, which we call "Screens".
- install the following packages
npm install @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens @react-native-community/masked-view
npm install @react-navigation/stack
- we need to import
react-native-gesture-handler
at the top of ourApp.js
file. Used for handling native touch and gestures in react-navigation
import 'react-native-gesture-handler';
- create NavigatioContainer in
App.js
import { NavigationContainer } from '@react-navigation/native';
...
<NavigationContainer>{/* Rest of your app code */}</NavigationContainer>
- add Stack to the Navigation
import { createStackNavigator } from '@react-navigation/stack';
...
const Stack = createStackNavigator();
...
const HomeScreen = () => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
....
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
- create folder
screens
- create file
HomeScreen.js
in thescreens
folder - make usage of our components and render some of these
- copy the file
HomeScreen.js
and rename it toDetail.js
(also rename the function component in the file) - copy the file
HomeScreen.js
and rename it toNew.js
(also rename the function component in the file) - create two new
Stack.Screen
entries for new ScreensDetail
andNew
- create file
Container.js
in thecomponents
folder - create a Container component (use View from react-native) and assign the following styles (depending on the passed
fill
prop). - if
fill
is not provided assign these styles
{
display: 'flex',
flexDirection: 'column',
padding: 20,
},
- and these if
fill
is provided
{
flex: 1,
display: 'flex',
flexDirection: 'column',
}
- make sure to pass the
children
props to the View - make use of the component in our Screens
- create file
Screen.js
in thecomponents
folder - create a Screen component (use
View
fromreact-native
) and assign the following styles
{
display: 'flex',
flexDirection: 'column',
flex: 1,
backgroundColor: '#fff'
}
- make sure to pass the
children
props to the View - make use of the component in our Screens
- create file
Spacer.js
in thecomponents
folder - create a Spacer component (use
View
fromreact-native
) and assign the following styles
{
height: 25,
width: 25
}
- make use of the component in our Screens
- create file
Header.js
in thecomponents
folder - install
expo install expo-constants
(docs) - create the following component
import React from 'react';
import { SafeAreaView, StyleSheet, View, Text } from 'react-native';
import Constants from 'expo-constants';
const headerStyles = StyleSheet.create({
safeAreaView: {
backgroundColor: '#ff9100',
borderBottomRightRadius: 20,
borderBottomLeftRadius: 20,
},
wrapper: {
paddingHorizontal: 20,
paddingTop: Constants.statusBarHeight + 30,
paddingBottom: 40,
},
label: {
fontSize: 25,
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
},
});
const Header = (props) => (
<SafeAreaView style={headerStyles.safeAreaView}>
<View style={headerStyles.wrapper}>
<Text style={headerStyles.label}>{props.label}</Text>
</View>
</SafeAreaView>
);
- create file
FoodItem.js
in thecomponents
folder - create the following component
import React from 'react';
import { StyleSheet, View, Text, Image, TouchableOpacity } from 'react-native';
import Spacer from './Spacer';
const foodItemStyles = StyleSheet.create({
wrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomColor: 'lightgray',
borderBottomWidth: 1,
},
image: {
marginRight: 10,
height: 50,
width: 50,
},
title: { fontSize: 20, color: 'black', textAlign: 'left' },
});
const FoodItem = (props) => (
<TouchableOpacity onPress={() => props.onPress()}>
<View style={foodItemStyles.wrapper}>
<Image
resizeMode="cover"
source={{
uri: props.item.image,
}}
style={foodItemStyles.image}
/>
<Text style={foodItemStyles.title}>{props.item.title}</Text>
</View>
</TouchableOpacity>
);
- create file
Input.js
in thecomponents
folder - create a Input component (use
TextInput
fromreact-native
) and assign the following styles
{
borderColor: '#ff9100',
borderWidth: 1,
borderRadius: 10,
paddingHorizontal: 20,
paddingVertical: 12
}
- make sure to pass the
value
,onChangeText
andplaceholder
props to the TextInput - make use of the component in our Screens
An asynchronous, unencrypted, persistent, key-value storage API.
- install
expo install @react-native-community/async-storage
(docs) - create some helper functions to store and read from Async Storage
- create folder
helpers
- create file
storage.js
in thehelpers
Screen - import Async Storage
import AsyncStorage from '@react-native-community/async-storage';
- define a storage key for the key-value pair
const STORAGE_KEY = 'my-favorite-food/items';
- storeData
export const storeData = async (value) => {
try {
const json = JSON.stringify(value);
await AsyncStorage.setItem(STORAGE_KEY, json);
} catch (error) {
console.log(error);
}
};
- getData
export const getData = async () => {
try {
const json = await AsyncStorage.getItem(STORAGE_KEY);
if (json === null) return null;
return JSON.parse(json);
} catch (error) {
console.log(error);
}
};
- addData
export const addData = async (value) => {
let data = [];
const existing = await getData();
if (existing) data = [...existing];
data.push(value);
await storeData(data);
};
- we need to combine everything and make sure we have the following code for the different screen
- Home Screen
import React, { useEffect, useLayoutEffect, useState } from 'react';
import Button from '../components/Button';
import Screen from '../components/Screen';
import Spacer from '../components/Spacer';
import { Headline, Paragraph } from '../components/Text';
import Header from '../components/Header';
import Container from '../components/Container';
import FoodItem from '../components/FoodItem';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { getData } from '../helpers/storage';
import { ScrollView } from 'react-native';
const Home = () => {
const navigation = useNavigation();
const [items, setItems] = useState([]);
useFocusEffect(() => {
getData().then((data) => {
if (data) setItems(data);
});
});
return (
<Screen>
<Header label="In the mood for food?" />
<Container fill>
{items.length > 0 && (
<ScrollView style={{ flex: 1 }}>
<Container>
{items.map((item, index) => (
<FoodItem
key={index}
item={item}
onPress={() => navigation.navigate('Detail', { item })}
/>
))}
</Container>
</ScrollView>
)}
</Container>
<Container>
<Button
label="add food"
onPress={() => {
navigation.navigate('New');
}}
/>
</Container>
</Screen>
);
};
export default Home;
- Detail Screen
import React from 'react';
import Button from '../components/Button';
import Screen from '../components/Screen';
import Spacer from '../components/Spacer';
import { Headline } from '../components/Text';
import Header from '../components/Header';
import Container from '../components/Container';
import { ScrollView } from 'react-native';
import { addData } from '../helpers/storage';
import { useNavigation } from '@react-navigation/native';
import ImagePreview from '../components/ImagePreview';
const Detail = (props) => {
const navigation = useNavigation();
const { params } = props.route;
return (
<Screen>
<Header label={params.item.title} />
<Container fill>
<ScrollView style={{ flex: 1 }}>
<Container>
{params.item.image && (
<ImagePreview
source={{
uri: params.item.image,
}}
/>
)}
</Container>
</ScrollView>
</Container>
<Container>
<Button label="go back" onPress={() => navigation.goBack()} />
</Container>
</Screen>
);
};
export default Detail;
- New Screen
import React, { useState } from 'react';
import Button from '../components/Button';
import Screen from '../components/Screen';
import Spacer from '../components/Spacer';
import { Headline } from '../components/Text';
import Header from '../components/Header';
import Container from '../components/Container';
import { ScrollView } from 'react-native';
import { addData } from '../helpers/storage';
import { useNavigation } from '@react-navigation/native';
import * as ImagePicker from 'expo-image-picker';
import ImagePreview from '../components/ImagePreview';
import Input from '../components/Input';
const New = () => {
const navigation = useNavigation();
const [title, setTitle] = useState('');
const [image, setImage] = useState(null);
const selectImage = async () => {
let permissionResult = await ImagePicker.requestCameraRollPermissionsAsync();
if (permissionResult.granted === false) {
alert('Permission to access camera roll is required!');
return;
}
let pickerResult = await ImagePicker.launchImageLibraryAsync();
if (pickerResult.cancelled === true) {
return;
}
setImage(pickerResult.uri);
};
const save = async () => {
await addData({ title, image });
navigation.goBack();
};
return (
<Screen>
<Header label="What food do you like most?" />
<Container fill>
<ScrollView style={{ flex: 1 }}>
<Container>
<Input
value={title}
onChangeText={(text) => setTitle(text)}
placeholder={'insert Title'}
/>
<Spacer />
{image && (
<Fragment>
<ImagePreview
source={{
uri: image,
}}
/>
<Spacer />
</Fragment>
)}
<Button label="select image" onPress={() => selectImage()} />
</Container>
</ScrollView>
</Container>
<Container>
<Button
label="save"
disabled={title === '' || !image}
onPress={() => save()}
/>
</Container>
</Screen>
);
};
export default New;