DEV Community

DETL INC
DETL INC

Posted on

How to implement Image Upload with Expo Image Picker in React Native for a professional app launched app.

Image description

Introduction

At DETL, we specialize in crafting stellar software applications that seamlessly blend hardcore engineering with sleek design. Our mission is to deliver both mobile and web applications that provide exceptional user experiences without compromising on aesthetics or functionality. In this blog post, we will guide you through how to implement an image picker in your Expo React Native application. This feature will allow users to upload a profile image, adding a personal touch to their profiles.

Why Implement a Profile Image Picker?

Personalization is a key factor in enhancing user engagement within an app. Allowing users to upload a profile image not only personalizes their experience but also creates a deeper connection with the application. In this blog post, we’ll show you how to implement this feature using Expo’s ImagePicker, enabling users to select and upload profile pictures seamlessly.

Overview

Before embarking on this journey of explaining to you how to allow users to upload their profile image, I’d like to first break down the topics just so you’ll have a solid idea of what we will be covering in this blog post.

  1. Installing Required Packages
  2. Requesting Permissions
  3. Allowing Users to Pick an Image
  4. Uploading the Image to the Backend
  5. Updating the User’s Profile Picture

We will explain each step in detail so you are not left confused :)

Step 1: Installing Required Packages

Before we start, make sure you have Expo React Native application configured so that you can install the following package called expo-image-picker.

expo install expo-image-picker
Enter fullscreen mode Exit fullscreen mode

Step 2: Importing Necessary Modules and Setting Up State

In your profile component file, import the necessary package and as well as create the necessary states that will handle the uploading of the image.

We have a folder called profile -> index.tsx and we place the code which we will show you below inside of the index.tsx file.

import React, { useCallback, useEffect, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  Image,
  TouchableOpacity,
  ScrollView,
  RefreshControl,
  Alert,
  ActivityIndicator,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';

// State variables
const [selectedImage, setSelectedImage] = useState('');
const [loadingImage, setLoadingImage] = useState(false);
Enter fullscreen mode Exit fullscreen mode

We have declared two states, the first state saves the selected image and the second state is a loading state which we will use when we are making an API call to our backend to save the image to the user's profile. We also import expo-image-picker.

Step 3: Requesting Permissions and Picking an Image

We need to request permission to access the user’s media library and then allow them to select an image.

const pickImage = async () => {
  // Ask for permission to access the gallery
  const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
  if (status !== 'granted') {
    Alert.alert(
      'Permission Denied',
      'Permission to access gallery was denied'
    );
    return;
  }

  // Launch the image picker
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsEditing: true,
    aspect: [1, 1],
    quality: 1,
    base64: true, // Include base64 in the result
  });

  if (!result.canceled) {
    setLoadingImage(true);
    const completeBase64String =
      'data:image/png;base64,' + result.assets[0].base64;
    uploadProfilePicture(completeBase64String, result.assets[0].uri);
  } else {
    setLoadingImage(false);
    Alert.alert('Error', 'Failed to select image');
  }
};
Enter fullscreen mode Exit fullscreen mode

When we request access from the user we check if the status was granted; if not, then we throw an alert. Once the status has been granted, the app will automatically navigate to the user's media library where their images are stored.

Firstly, it is important to note that when we pick the image the user has selected, we make sure that we have access to the base64 format of the image since that is what we will be sending back to the backend when we make our API call.

Secondly, as you can see from the code as shown below is that we attach a string to the result.

const completeBase64String =
      'data:image/png;base64,' + result.assets[0].base64;
    uploadProfilePicture(completeBase64String, result.assets[0].uri);
Enter fullscreen mode Exit fullscreen mode

The selected image returns a base64 without the first part of the image data:image/png;base64, which is important, otherwise our backend will not be able to process the image correctly. What we do is attach a base64 image string to the result we get from the image picker so that our backend is able to process the image accordingly. This is a specific use case and can as well be handled by your backend but it is much easier in its implementation on the frontend.

So far we have selected our image, received the equivalent base64 result of that image and now we will make a backend call with the base64 result of the image which is shown in the above code as such uploadProfilePicture(completeBase64String, result.assets[0].uri).

Step 4: Uploading the Image to the Backend

We need to send the selected image to the backend server to update the user’s object. Whenever the user is re-authenticated, the image is then fetched from their profile and displayed on the app.

const uploadProfilePicture = async (base64Image, uri) => {
  const endpoint = 'user';
  const method = 'PUT'; // Use PUT as per your requirement

  try {
    // Prepare the data object
    const data = {
      profilePicture: base64Image,
    };

    // Make the API request
    const response = await actionProvider.makeRequest(endpoint, method, data);

    if (response.status === 200) {
      Alert.alert('Success', 'Profile picture updated successfully');
      // Optionally, refresh user information
      fetchUserInformation(session);
      setLoadingImage(false);
      setSelectedImage(uri);
    } else {
      Alert.alert('Error', 'Failed to update profile picture');
      setLoadingImage(false);
    }
  } catch (error) {
    setLoadingImage(false);
    console.error('Error uploading profile picture:', error);
    Alert.alert(
      'Error',
      'An error occurred while uploading the profile picture'
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

In simple steps we will explain to you what is happening in the code above.

  • Data Preparation: We create a data object containing the base64 image which we will send as a payload within the API call.

  • API Call: We make a PUT request to the backend API to update the user’s profile picture.

  • Note: Replace actionProvider.makeRequest with your actual API endpoint method (e.g., using fetch or axios). Since we have a hooks file where we make all of our API calls, we have placed our endpoint URL in this file so that we can import it wherever we need to make an API call.

  • Handling Responses: We check the response status and update the UI accordingly.

Step 5: Displaying the Profile Image

We update the UI to display the user’s profile image or a placeholder if none is set.

<View style={styles.profileImageContainer}>
  <TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
    {loadingImage ? (
      <View
        style={[
          styles.profileImage,
          {
            justifyContent: 'center',
            alignItems: 'center',
          },
        ]}
      >
        <ActivityIndicator color={'#4096C1'} />
      </View>
    ) : selectedImage ? (
      <>
        <Image
          source={{ uri: selectedImage }}
          style={styles.profileImage}
          resizeMode='cover'
        />
      </>
    ) : (
      <Image
        source={require('@/assets/images/profile/profileImage.png')}
        style={styles.profileImage}
        resizeMode='contain'
      />
    )}
  </TouchableOpacity>
</View>
Enter fullscreen mode Exit fullscreen mode
  • TouchableOpacity: Makes the image clickable, allowing users to change their profile picture which runs the pickImage function to show the media library.

  • Loading Indicator: Shows while the backend API call is made and the image is uploading.

  • Selected Image: Displays the user’s chosen profile picture.

  • Placeholder Image: Shown if no profile picture is set. This can be stored in a file inside your app like the public → assets → images → profile → profileImage.png folder.

Entire Code

Below we have posted the entire code. This way you can have a bird-eye view of the entire implementation and as well as follow the steps outlined above in a much more thorough way.

import AnimatedWrapper from "@/components/shared/animation";
import GlobalButton from "@/components/shared/Button";
import { Colors, globalStyle } from "@/constants/Colors";
import { useSession } from "@/providers/Auth/AuthProvider";
import React, { useCallback, useEffect, useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  Image,
  ImageBackground,
  TouchableOpacity,
  ScrollView,
  RefreshControl,
  Alert,
  ActivityIndicator,
} from "react-native";
import Subscription from "../../Subscription";

import * as ImagePicker from "expo-image-picker";
import actionProvider from "../../../../providers/actionProvider";
import { useToast } from "../../../../providers/useToast";

const HEIGHT = 80;

const index = () => {
  const { user, session, fetchUserInformation } = useSession();

  const [modalVisible, setModalVisible] = useState(false);

  const [loading, setLoading] = React.useState(false);
  const [loadingImage, setLoadingImage] = useState(false);

  const [selectedImage, setSelectedImage] = useState("");

  const { showToast } = useToast();

  useEffect(() => {
    // If user has a profile picture, set it
    if (user?.profilePicture) {
      setSelectedImage(user.profilePicture);
    }
  }, [user]);

  const onRefresh = useCallback(async () => {
    // Start refreshing
    setLoading(true);
    fetchUserInformation(session);

    // Simulate a network request or any async action
    setTimeout(() => {
      // Stop refreshing after 3 seconds
      setLoading(false);
    }, 3000);
  }, []);

  const pickImage = async () => {
    // Ask for permission to access the gallery
    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (status !== "granted") {
      Alert.alert(
        "Permission Denied",
        "Permission to access gallery was denied"
      );
      return;
    }

    // Launch the image picker
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [1, 1],
      quality: 1,
      base64: true, // Include base64 in the result
    });

    const completeBase64String =
      "data:image/png;base64," + result?.assets[0]?.base64;

    if (!result.cancelled) {
      setLoadingImage(true);
      uploadProfilePicture(completeBase64String, result?.assets[0].uri);
    } else {
      setLoadingImage(false);

      showToast("error", "Failed to update profile picture");
    }
  };

  const uploadProfilePicture = async (base64Image: string, uri: string) => {
    const endpoint = "user";
    const method = "PUT"; // Use PUT as per your requirement

    try {
      // Prepare the data object
      const data = {
        profilePicture: base64Image,
      };

      // Make the API request
      const response = await actionProvider.makeRequest(endpoint, method, data);

      if (response.status === 200) {
        Alert.alert("Success", "Profile picture updated successfully");
        // Optionally, refresh user information
        fetchUserInformation(session);
        setLoadingImage(false);

        setSelectedImage(uri);
      } else {
        showToast("error", "Failed to update profile picture");
        setLoadingImage(false);
      }
    } catch (error) {
      setLoadingImage(false);

      console.error("Error uploading profile picture:", error);
      showToast(
        "error",
        "An error occurred while uploading the profile picture"
      );
    }
  };

  return (
    <View
      style={{
        flex: 1,
        backgroundColor: "white",
      }}
    >
      <ScrollView
        refreshControl={
          <RefreshControl
            refreshing={loading}
            onRefresh={onRefresh}
            colors={["#4096C1"]} // Customize spinner colors
            tintColor="#4096C1" // Customize spinner color for iOS
          />
        }
        contentContainerStyle={{
          flex: 1,
        }}
      >
        <View style={styles.container}>
          <ImageBackground
            style={styles.innerContainer}
            source={require("@/assets/images/profile/profilePageImage.png")}
            imageStyle={styles.backgroundImage}
          >
            <View style={styles.profileImageContainer}>
              {/* This is the profile image section */}
              <TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
                {loadingImage ? (
                  <View
                    style={[
                      styles.profileImage,
                      {
                        justifyContent: "center",
                        alignItems: "center",
                      },
                    ]}
                  >
                    <ActivityIndicator color={"#4096C1"} />
                  </View>
                ) : selectedImage ? (
                  <>
                    <Image
                      source={{ uri: selectedImage }}
                      style={styles.profileImage}
                      resizeMode="cover"
                    />
                  </>
                ) : (
                  <Image
                    source={require("@/assets/images/profile/profileImage.png")}
                    style={styles.profileImage}
                    resizeMode="contain"
                  />
                )}
              </TouchableOpacity>

              {/* This is the profile image section */}
            </View>
          </ImageBackground>
        </View>
      </ScrollView>
    </View>
  );
};

export default index;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "white",
  },
  innerContainer: {
    height: 200,
    backgroundColor: Colors.button.backgroundButtonDark,
    borderBottomLeftRadius: 40,
    borderBottomRightRadius: 40,
    alignItems: "center",
    justifyContent: "flex-end",
  },
  profileImageContainer: {
    position: "absolute",
    bottom: -HEIGHT / 2,
    height: HEIGHT,
    width: HEIGHT,
    borderRadius: HEIGHT / 2,
    backgroundColor: "white",
    padding: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  backgroundImage: {
    borderBottomLeftRadius: 40,
    borderBottomRightRadius: 40,
  },
  profileImage: {
    height: HEIGHT / 1.2,
    width: HEIGHT / 1.2,
    borderRadius: HEIGHT / 1.5,
  },
  infoContainer: {
    alignItems: "center",
    marginTop: 30,
    flex: 1,
  },
  greetingText: {
    fontSize: 20,
    fontWeight: "bold",
    fontFamily: globalStyle.font.fontFamilyBold,
    textAlign: "center",
  },
  planContainer: {
    marginVertical: 10,
    backgroundColor: "rgba(64,150,193,.20)",
    borderRadius: 5,
    paddingVertical: 5,
    paddingHorizontal: 20,
  },
  planText: {
    color: "#000000",
    fontFamily: globalStyle.font.fontFamilyBold,
  },
  buttonContainer: {
    marginHorizontal: globalStyle.container.marginHorizontal,
    bottom: 10,
  },
  premiumContainer: {
    marginTop: 30,
    width: "90%",
    justifyContent: "space-between",
    alignItems: "center",
    backgroundColor: "#EFF6FA",
    flexDirection: "row",
    paddingVertical: 15,
    paddingHorizontal: 10,
    borderRadius: 10,
  },
  premiumContent: {
    flexDirection: "row",
    alignItems: "center",
    flex: 0.8,
  },
  premiumIcon: {
    height: 30,
    width: 30,
  },
  premiumText: {
    fontFamily: globalStyle.font.fontFamilyBold,
    fontSize: 16,
    marginLeft: 15,
  },
  upgradeText: {
    fontFamily: globalStyle.font.fontFamilyBold,
    color: Colors.button.textDark,
  },
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this blog post, we’ve demonstrated how to implement an image picker in your Expo React Native application to allow users to upload and update their profile images. This feature enhances user engagement by adding a personalized touch to their profiles.

If there is anything unclear in this blog post, feel free to reach out to DETL and we will be happy to help you through the journey of implementing this.

Happy Coding!

Top comments (0)