Overview
This video demonstrates a simple React Native Expo application that integrates Firebase Storage and Expo Image Picker to capture and upload images to Firebase Storage.
We also included the react-native-dotenv
npm package to manage our firebase environment variables.
It showcases the use of Firebase SDK functions, React Hooks, and the Expo Image Picker library to interact with the device's camera and Firebase Storage.
Below is additional information in the final files that were create to build the application and steps to set it all
Create Project and Install npm Packages
# create project
npx create-expo-app my-app-camera-fb
#install dotenv for env variables
npm install -D react-native-dotenv
# using camera from image picker
npx expo install expo-image-picker
# using firebase javascript API
npx expo install firebase
# ensuring the firebase api is packaged correctly
npx expo customize metro.config.js
Updates to metro.config.js
const { getDefaultConfig } = require('@expo/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
defaultConfig.resolver.assetExts.push('cjs');
module.exports = defaultConfig;
.env
File Setup and Configuration
you need to update the babel.config.js
, see the addition to the plugins
property
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
"plugins": [
["module:react-native-dotenv"]
]
};
};
you also need to create a .env
file in the root of you project to hold the firebase
keys needed for application
FIREBASE_API_KEY=""
FIREBASE_AUTH_DOMAIN=""
FIREBASE_DATABASE_URL=
FIREBASE_PROJECT_ID=""
FIREBASE_STORAGE_BUCKET=""
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=""
Permissions Configuration
In app.json
you need to set information for ios plist
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "Allow $(PRODUCT_NAME) to access your photo",
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
}
]
]
Firebase Code
This code snippet is an example of how to use Firebase Storage in a JavaScript or TypeScript project. Let's go through it step by step:
The code begins by importing specific variables from the @env package. This package is commonly used to store environment variables securely.
import {
FIREBASE_API_KEY,
FIREBASE_STORAGE_BUCKET,
FIREBASE_APP_ID,
FIREBASE_PROJECT_ID,
FIREBASE_AUTH_DOMAIN,
} from "@env";
Next, it imports various functions and objects from the Firebase App and Firebase Storage modules. These modules are part of the Firebase SDK, which provides tools for building Firebase-powered apps.
import { initializeApp, getApp, getApps } from "firebase/app";
import {
getStorage,
ref,
uploadBytesResumable,
getDownloadURL,
listAll,
} from "firebase/storage";
The code defines an object called firebaseConfig that contains the configuration options for your Firebase project. These options include the API key, storage bucket, app ID, project ID, and authentication domain. These values are obtained from the Firebase console when setting up a project.
// Initialize Firebase
const firebaseConfig = {
apiKey: FIREBASE_API_KEY,
storageBucket: FIREBASE_STORAGE_BUCKET,
appId: FIREBASE_APP_ID,
projectId: FIREBASE_PROJECT_ID,
authDomain: FIREBASE_AUTH_DOMAIN,
};
The console.log(firebaseConfig) line outputs the configuration object to the console for debugging purposes.
The if (getApps().length === 0)
condition checks if there are any Firebase apps initialized. This is to ensure that the Firebase app is only initialized once and prevents errors if this code is executed multiple times.
If no Firebase apps are initialized, the initializeApp(firebaseConfig)
function is called to initialize the Firebase app using the provided configuration.
if (getApps().length === 0) {
initializeApp(firebaseConfig);
}
The getApp()
function is then used to retrieve the default Firebase app instance, and getStorage()
is used to get a reference to the Firebase Storage service. I never actually needed these variables, but it was left in the code to show how they can be exported for use in other parts of application.
The listFiles
function is defined as an asynchronous function that lists all the files in a specific Firebase Storage reference. It first creates a reference to the desired location using ref(storage, "images")
, where "images"
is the path to the directory you want to list files from. It then calls listAll
on the reference to retrieve a list of files and directories under that path.
const listFiles = async () => {
const storage = getStorage();
// Create a reference under which you want to list
const listRef = ref(storage, "images");
// Find all the prefixes and items.
const listResp = await listAll(listRef);
return listResp.items;
};
The uploadToFirebase
function is defined to upload a file to Firebase Storage. It takes three parameters: uri (the URI of the file to upload), name (the desired name of the uploaded file), and onProgress (an optional callback to track the upload progress). This function fetches the file data from the provided URI, creates a reference to the desired location using ref(getStorage(), "images/${name}")
, and then uses uploadBytesResumable
to upload the file in chunks. The function returns a Promise that resolves with the download URL and metadata of the uploaded file.
const uploadToFirebase = async (uri, name, onProgress) => {
const fetchResponse = await fetch(uri);
const theBlob = await fetchResponse.blob();
const imageRef = ref(getStorage(), `images/${name}`);
const uploadTask = uploadBytesResumable(imageRef, theBlob);
return new Promise((resolve, reject) => {
uploadTask.on(
"state_changed",
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
onProgress && onProgress(progress);
},
(error) => {
// Handle unsuccessful uploads
console.log(error);
reject(error);
},
async () => {
const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref);
resolve({
downloadUrl,
metadata: uploadTask.snapshot.metadata,
});
}
);
});
};
Finally, the code exports the fbApp
, fbStorage
, uploadToFirebase
, and listFiles
variables, which can be imported and used in other parts of your application.
export { fbApp, fbStorage, uploadToFirebase, listFiles };
Overall, this code initializes the Firebase app and provides functions for listing files in Firebase Storage and uploading files to Firebase Storage.
Main Application Code
The code imports necessary components and functions from various packages, including expo-status-bar
, React Native components, expo-image-picker
, Firebase-related modules (listFiles
and uploadToFirebase
functions from firebase-config.js
), and the custom MyFilesList
component.
The App function is the main component of the application. It uses React Hooks (useState and useEffect) to manage state and lifecycle events.
We have defined the following code to leverage the useCameraPermissions
hook to get current permission and also a function to call to prompt the user for permission, we will reference that later in this module.
const [permission, requestPermission] = ImagePicker.useCameraPermissions();
const [files, setFiles] = useState([]);
Inside the useEffect hook, the code fetches the initial list of files from Firebase Storage when the component mounts. It calls the listFiles
function from firebase-config.js
to retrieve a list of files. It then maps the response to create an array of file objects with just the name property (extracted from the fullPath property of each file). Finally, it updates the state with the array of files using the setFiles function.
The console.log(files) statement logs the current state of the files array to the console for debugging purposes.
The takePhoto
function is an asynchronous function that handles capturing an image using the device's camera. It utilizes the launchCameraAsync
function from expo-image-picker
to open the camera interface. The function passes some configuration options such as allowing editing, capturing all media types, and setting the image quality to 1. Once an image is captured, it checks if the operation was canceled or not.
const takePhoto = async () => {
try {
const cameraResp = await ImagePicker.launchCameraAsync({
allowsEditing: true,
mediaTypes: ImagePicker.MediaTypeOptions.All,
quality: 1,
});
if (!cameraResp.canceled) {
const { uri } = cameraResp.assets[0];
const fileName = uri.split("/").pop();
const uploadResp = await uploadToFirebase(uri, fileName, (v) =>
console.log(v)
);
console.log(uploadResp);
listFiles().then((listResp) => {
const files = listResp.map((value) => {
return { name: value.fullPath };
});
setFiles(files);
});
}
} catch (e) {
Alert.alert("Error Uploading Image " + e.message);
}
};
If the image capture was not canceled, it extracts the URI (file path) of the captured image and obtains the file name by splitting the URI and retrieving the last segment.
It then calls the uploadToFirebase
function from firebase-config.js
, passing the URI
, file name
, and an optional callback
function to track the upload progress. The function uploads the image to Firebase Storage using the Firebase SDK. The returned promise resolves with an object containing the download URL
and metadata
of the uploaded file. This information is logged to the console for debugging purposes.
After a successful upload, the code fetches the updated list of files from Firebase Storage by calling the listFiles
function again. It follows the same process as before, mapping the response to create an array of file objects and updating the state with the new array of files.
If any error occurs during the image capture or upload process, an error message is displayed using Alert.alert
function from React Native.
The user should only be able to take pictures if permission in granted; There are some addition configuration actions required for this to work.
The code checks if camera permission is granted by comparing the status property of the permission object obtained from ImagePicker.useCameraPermissions()
with ImagePicker.PermissionStatus.GRANTED
. If permission is not granted, a view is rendered that displays a message indicating the permission status and a button to request permission.
// permission check
if (permission?.status !== ImagePicker.PermissionStatus.GRANTED) {
return (
<View style={styles.container}>
<Text>Permission Not Granted - {permission?.status}</Text>
<StatusBar style="auto" />
<Button title="Request Permission" onPress={requestPermission}></Button>
</View>
);
}
If camera permission is granted, the main UI is rendered. It includes a component displaying a message, the MyFilesList component (which renders the list of files), a component, and a component that triggers the takePhoto function when pressed.
return (
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<Text>Working With Firebase and Image Picker</Text>
<MyFilesList files={files} />
<StatusBar style="auto" />
<Button title="Take Picture" onPress={takePhoto}></Button>
</View>
</SafeAreaView>
);
Custom MyList
Component
This code defines a custom React Native component called MyFilesList
. It is used to display a list of files in a flat list format. Here's a breakdown of the code:
The MyFilesList function is the main component. It receives a prop called files, which is an array of file objects.
export default function MyFilesList({ files }) {
Inside the MyFilesList
component, there is a nested component called Item
. This component represents each item in the list and receives a prop called name
, which represents the name of the file.
The Item component renders a View component with a custom style (styles.item). Inside the view, there is a Text component that displays the file name using the name prop. The text style is defined by the styles.title style
.
const Item = ({ name }) => (
<View style={styles.item}>
<Text style={styles.title}>{name}</Text>
</View>
);
The MyFilesList
component returns a FlatList
component from React Native. The FlatList
is the main component responsible for rendering the list of files.
return (
<FlatList
data={files}
renderItem={({ item }) => <Item name={item.name} />}
keyExtractor={(item) => item.name}
/>
);
The FlatList component receives several props:
-
data
is set to the files prop, which represents the array of file objects. -
renderItem
is a callback function that renders each item in the list. It receives the item object, and in this case, it renders the Item component and passes the name property of the item object as the name prop of the Item component. -
keyExtractor
is a function that extracts a unique key for each item in the list. In this case, it uses thename
property of each item as the key.
Finally, the code defines a styles object using StyleSheet.create(). It defines two styles: item and title. The item style sets margin vertical and horizontal spacing, and the title style sets the font size to 18.
This code provides a reusable component MyFilesList that takes an array of files and displays them in a flat list format. It demonstrates the usage of React Native's FlatList component, custom nested components, and styles defined using StyleSheet.create().