Building a React Native App #1
Hey buildooors! In this blog post, I'll guide you through my process of creating a simple fitness app using React Native. Let's start by setting up our project environment.
Stack
React Native
Expo
Native Base
Phosphor Icons
I'll be utilizing the Native Base
design system, which comes packed with a lot of handy features.
Finalizing the Interfaces
Now, we can go back and finalize our interfaces, starting with Home. We need a header, menu, and exercises table.
Starting with the header, I created a new component called HomeHeader.tsx
. I imported HStack
and VStack
to stack the texts and then layout the profile picture, text, and logout button. I wrapped the logout button with a TouchableOpacity
block from React Native to give users the option to logout.
import { HStack, VStack, Heading, Text, Icon } from "native-base";
import { MaterialIcons } from '@expo/vector-icons';
import { UserPhoto } from "./UserPhoto";
import { TouchableOpacity } from "react-native";
export function HomeHeader(){
...
}
Then I added this component to our Home.tsx
file:
<HomeHeader/>
Next, we can move onto the exercises tab under the home header by creating a component file with the Group function. Let's utilize Pressable
to not give the "TouchableOpacity" effect once we click on each exercise tab and wrap the text in our file with it:
import { Text, Pressable } from "native-base";
type Props = {
name: string;
}
export function Group({name, ...rest}: Props){
...
}
I created the exercises component, by creating an ExerciseStack.tsx
doc in our components file and inserting the TouchableOpacity clickable button and making use of VStack
and HStack
from Native-base to organize our image, heading, and text inside the component:
import { HStack, Heading, Image, Text, VStack } from "native-base";
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
type Props = TouchableOpacityProps & {
}
export function ExerciseStack({...rest}: Props){
return(
<TouchableOpacity {...rest}>
<HStack bg="gray.500" alignItems="center" p={2} pr={4} rounded="md" mb={3}>
<Image mr={4} w={16} h={16} rounded="md" alt="Exercise image" source={{ uri:'https://conteudo.imguol.com.br/c/entretenimento/0c/2019/12/03/remada-unilateral-com-halteres-1575402100538_v2_600x600.jpg' }} />
<VStack>
<Heading fontSize="lg" color="white">
Remada Unilateral
</Heading>
<Text fontSize="sm" color="gray.200" mt={1} numberOfLines={2}>
3 series x 12 repetitions
</Text>
</VStack>
</HStack>
</TouchableOpacity>
)
}
From there, I created a simple header for the History
page, that will also be reused in the profile screen. To get this started, I created a component for its header:
import { HStack, Heading, Image, Text, VStack } from "native-base";
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
type Props = TouchableOpacityProps & {
}
export function ExerciseStack({...rest}: Props){
return(
<TouchableOpacity {...rest}>
<HStack bg="gray.500" alignItems="center" p={2} pr={4} rounded="md" mb={3}>
<Image mr={4} w={16} h={16} rounded="md" alt="Exercise image" source={{ uri:'https://conteudo.imguol.com.br/c/entretenimento/0c/2019/12/03/remada-unilateral-com-halteres-1575402100538_v2_600x600.jpg' }} />
<VStack>
<Heading fontSize="lg" color="white">
Remada Unilateral
</Heading>
<Text fontSize="sm" color="gray.200" mt={1} numberOfLines={2}>
3 series x 12 repetitions
</Text>
</VStack>
</HStack>
</TouchableOpacity>
)
}
From there, I created a simple header for the History page, that will also be reused in the profile screen. To get this started, I created a component for its header with
import { Center, Heading } from "native-base";
type Props = {
title: string,
}
export function ScreenHeader({title}:Props){
return(
<Center bg="gray.600" pt={16} pb={6}>
<Heading color="gray.100" fontSize="sm">
{title}
</Heading>
</Center>
)
}
Now, to group the exercises by the day they were completed, we will utilize SectionList
from Native Base:
import { HistoryCard } from "@components/HistoryCard";
import { ScreenHeader } from "@components/ScreenHeader";
import { Center, Heading, Text, VStack, SectionList } from "native-base";
import { useState } from "react";
export function History(){
const [exercises, setExercises] = useState([{
title: '08.26.2023',
data: ['Puxada Frontal', 'Remada Unilateral'],
},
{
title: '08.27.2023',
data: ['Remada Unilateral'],
},
])
return(
<VStack flex={1}>
<ScreenHeader title='History' />
<SectionList
sections={exercises}
keyExtractor={item => item}
renderItem={({ item })=> (
<HistoryCard/>
)}
renderSectionHeader={({ section }) => (
<Heading color="gray.200" fontSize="md" mt={10} mb={3}>
{section.title}
</Heading>
)}
px={8}
contentContainerStyle={exercises.length === 0 && {flex: 1, justifyContent: "center"}}
ListEmptyComponent={() => (
<Text color="gray.300" textAlign="center">
There aren't any registered exercises.{'\n'}
Let's workout today?
</Text>
)}
/>
</VStack>
)
}
We are going to head to Profile.tsx
and integrate the ScreenHeader
component, previously created, and add a ScrollView
to make it easier for our users to navigate the app. From here, we will integrate our user's photo component:
import { ScreenHeader } from "@components/ScreenHeader";
import { UserPhoto } from "@components/UserPhoto";
import { Center, ScrollView, Text, VStack } from "native-base";
export function Profile(){
return(
<VStack flex={1}>
<ScreenHeader
title="Profile"
/>
<ScrollView>
<Center>
<UserPhoto source={{ uri: "https://github.com/danielapassos.png"}}
alt="User photo"
size={33}
/>
</Center>
</ScrollView>
</VStack>
)
}
As our profile picture is loading, to give our users a better experience - even when they modify the image and its loading, let's display the Skeleton
effect from Native Base:
import { ScreenHeader } from "@components/ScreenHeader";
import { UserPhoto } from "@components/UserPhoto";
import { Center, ScrollView, VStack, Skeleton } from "native-base";
import { useState } from "react";
const PHOTO_SIZE = 33
export function Profile(){
const [photoIsLoading, setphotoIsLoading] = useState(false)
return(
<VStack flex={1}>
<ScreenHeader
title="Profile"
/>
<ScrollView>
<Center>
{ photoIsLoading ?
<Skeleton
w={PHOTO_SIZE}
h={PHOTO_SIZE}
rounded='full'
startColor="gray.500"
endColor="gray.400"
/>
:
<UserPhoto source={{ uri: "https://github.com/danielapassos.png"}}
alt="User photo"
size={PHOTO_SIZE}
/>
}
</Center>
</ScrollView>
</VStack>
)
}
Now, we have inputs for name, email, and password. Let's start with the first two:
import { Input } from "@components/Input";
<Input bg="gray.500"
placeholder="Name"
/>
<Input bg="gray.500"
placeholder="Email"
isDisabled
/>
Now, let's move on to the password input. In here, we will make use of the components for Inputs and Button. Also, add the secureTextEntry
line to protect our users' password:
<Heading color="gray.200" fontSize="md" mb={2}>
Update your password
</Heading>
<Input bg="gray.600"
placeholder="Old password"
secureTextEntry
/>
<Input bg="gray.600"
placeholder="New password"
secureTextEntry
/>
<Input bg="gray.600"
placeholder="Confirm your new password"
secureTextEntry
/>
<Button
title="Update"
mt={4}
/>
</ScrollView>
</VStack>
To create a screen for each exercise, we will start by creating a route to it. You can not get to it through the bottom navigation, only through the Home
screen. For this, we will create a function that will be called when users press the component for our ExerciseStack
.
const navigation = useNavigation<AppNavigatorRoutesProps>()
function handleOpenExerciseDetails(){
navigation.navigate('exercise')
}
<ExerciseStack
onPress={handleOpenExerciseDetails
}/>
After polishing this screen, let's allow our users to modify their avatar photo. For that, we will use the Expo ImagePicker
library. In the Profile.tsx
file, import ImagePicker as:
import * as ImagePicker from 'expo-image-picker'
const [userPhoto, setUserPhoto] = useState('https://github.com/danielapassos.png')
async function handleUserPhotoSelect(){
const photoSelected = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
aspect: [4, 4],
allowsEditing: true
});
if (photoSelected.canceled){
return
}
setUserPhoto(photoSelected.assets[0].uri)
}
Then, pass this function to the Update photo component:
<TouchableOpacity onPress={handleUserPhotoSelect}>
<Text color="green.500"
mt={2}>
Update photo
</Text>
</TouchableOpacity>
Now, to enable our users to stored their new photos locally on their device, let's use the Expo File System
.
import * as FileSystem from 'expo-file-system'
async function handleUserPhotoSelect(){
const photoSelected = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
aspect: [4, 4],
allowsEditing: true
});
if (photoSelected.canceled){
return
}
const photoSaved = await FileSystem.copyAsync({
from: photoSelected.assets[0].uri,
to: `${FileSystem.documentDirectory}${Date.now()}.jpg`
})
setUserPhoto(photoSaved.uri)
}
This was a sneak peak of my building process! I hope it guided you to get started with your project. We've seen how to build a fitness app with a dynamic interface and robust features using React Native and Native Base.
React Native offers immense possibilities. Match it with some sprinkle of creativity, and it can lead to awesome digital products!
That is it for this article, thanks for reading! โ๏ธ
Subscribe to my newsletter
Read articles from Daniela Passos directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Daniela Passos
Daniela Passos
Hashnode's Developer Relations. Female builder dedicated to creating captivating digital experiences & fostering vibrant developer communities.