Edit this page
In this tutorial, learn how to use components such as React Native's Pressable and Expo Image to build a screen.
In this chapter, we'll create the first screen of the StickerSmash app.
The screen above displays an image and two buttons. The app user can select an image using one of the two buttons. The first button allows the user to select an image from their device. The second button allows the user to continue with a default image provided by the app.
Once the user selects an image, they can add a sticker to it. So, let's start creating this screen.
1
Before we build this screen by writing code, let's break it down into some essential elements.
There are two essential elements:
The first button contains multiple components. The parent element provides a yellow border, and contains an icon and text components inside a row.
Now that we've broken down the UI into smaller chunks, we're ready to start coding.
2
We'll use expo-image
library to display the image in the app. It provides a cross-platform <Image>
component to load and render an image.
Stop the development server by pressing Ctrl + c in the terminal. Then, install the expo-image
library:
-
npx expo install expo-image
The npx expo install
command will install the library and add it to the project's dependencies in package.json.
The Image component takes the source of an image as its value. The source can be either a static asset or a URL. For example, the source required from assets/images directory is static. It can also come from Network as a uri
property.
To use the Image component in app/(tabs)/index.tsx file:
Image
from the expo-image
library.PlaceholderImage
variable to use assets/images/background-image.png file as the source
prop on the Image
component.import { View, StyleSheet } from 'react-native';
import { Image } from 'expo-image';
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<Image source={PlaceholderImage} style={styles.image} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
image: {
width: 320,
height: 440,
borderRadius: 18,
},
});
3
Let's divide the code into multiple files as we add more components to this screen. Throughout this tutorial, we'll use the components directory to create custom components.
image
styles.import { StyleSheet } from "react-native";
import { Image, type ImageSource } from "expo-image";
type Props = {
imgSource: ImageSource;
};
export default function ImageViewer({ imgSource }: Props) {
return <Image source={imgSource} style={styles.image} />;
}
const styles = StyleSheet.create({
image: {
width: 320,
height: 440,
borderRadius: 18,
},
});
Since ImageViewer is a custom component, we are placing it in a separate directory instead of the app directory. Every file inside app directory is either a layout file or a route file. For more information, see Non-route files.
Import ImageViewer
and use it in the app/(tabs)/index.tsx:
import { StyleSheet, View } from 'react-native';
import ImageViewer from "@/components/ImageViewer";
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<ImageViewer imgSource={PlaceholderImage} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
});
@
in import statement?The @
symbol is a custom path alias for importing custom components and other modules instead of relative paths. Expo CLI automatically configures it in tsconfig.json.
4
React Native includes a few different components for handling touch events, but <Pressable>
is recommended for its flexibility. It can detect single taps, long presses, trigger separate events when the button is pushed in and released, and more.
In the design, there are two buttons we need to create. Each has a different style and label. Let's start by creating a reusable component for these buttons. Create a Button.tsx file inside the components directory with the following code:
import { StyleSheet, View, Pressable, Text } from 'react-native';
type Props = {
label: string;
};
export default function Button({ label }: Props) {
return (
<View style={styles.buttonContainer}>
<Pressable style={styles.button} onPress={() => alert('You pressed a button.')}>
<Text style={styles.buttonLabel}>{label}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
buttonContainer: {
width: 320,
height: 68,
marginHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
},
button: {
borderRadius: 10,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
buttonLabel: {
color: '#fff',
fontSize: 16,
},
});
The app displays an alert when the user taps any of the buttons on the screen. It happens because <Pressable>
calls alert()
on its onPress
prop. Let's import this component into app/(tabs)/index.tsx file and add styles for the <View>
that encapsulates these buttons:
import { View, StyleSheet } from "react-native";
import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require("@/assets/images/background-image.png");
export default function Index() {
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<ImageViewer imgSource={PlaceholderImage} />
</View>
<View style={styles.footerContainer}>
<Button label="Choose a photo" />
<Button label="Use this photo" />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
paddingTop: 28,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
});
Let's take a look at our app on Android, iOS and the web:
The second button with the label "Use this photo" resembles the actual button from the design. However, the first button needs more styling to match the design.
5
The "Choose a photo" button requires different styling than the "Use this photo" button, so we will add a new button theme prop that will allow us to apply a primary
theme. This button also has an icon before the label. We will use an icon from the @expo/vector-icons
library.
To load and display the icon on the button, let's use FontAwesome
from the library. Modify components/Button.tsx to add the following code snippet:
import { StyleSheet, View, Pressable, Text } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';
type Props = {
label: string;
theme?: 'primary';
};
export default function Button({ label, theme }: Props) {
if (theme === 'primary') {
return (
<View
style={[
styles.buttonContainer,
{ borderWidth: 4, borderColor: '#ffd33d', borderRadius: 18 },
]}>
<Pressable
style={[styles.button, { backgroundColor: '#fff' }]}
onPress={() => alert('You pressed a button.')}>
<FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} />
<Text style={[styles.buttonLabel, { color: '#25292e' }]}>{label}</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.buttonContainer}>
<Pressable style={styles.button} onPress={() => alert('You pressed a button.')}>
<Text style={styles.buttonLabel}>{label}</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
buttonContainer: {
width: 320,
height: 68,
marginHorizontal: 20,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
},
button: {
borderRadius: 10,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
buttonIcon: {
paddingRight: 8,
},
buttonLabel: {
color: '#fff',
fontSize: 16,
},
});
Let's learn what the above code does:
StyleSheet.create()
with an object directly passed in the style
prop.<Pressable>
component in the primary theme uses a backgroundColor
property with a value #fff
to set the button's background to white. If we add this property to the styles.button
, the background color value will be set for both the primary theme and the unstyled one.Now, modify the app/(tabs)/index.tsx file to use the theme="primary"
prop on the first button.
import { View, StyleSheet } from 'react-native';
import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<ImageViewer imgSource={PlaceholderImage} />
</View>
<View style={styles.footerContainer}>
<Button theme="primary" label="Choose a photo" />
<Button label="Use this photo" />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
alignItems: 'center',
},
imageContainer: {
flex: 1,
},
footerContainer: {
flex: 1 / 3,
alignItems: 'center',
},
});
Let's take a look at our app on Android, iOS and the web:
Chapter 3: Build a screen
We've successfully implemented the initial design to start building our app's first screen.
In the next chapter, we'll add the functionality to pick an image from the device's media library.