Building a React Native App #1

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.

💡
Check out the GitHub repository for this mobile app 🔗 HERE!

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! ⚒️

Let's connect on Twitter and LinkedIn 👋