Download Images from URL using React Native and theta-client

How to download an image from the URL in the camera and save it to local mobile app device storage using React Native and Theta-Client

With this comprehensive guide, you can effortlessly download and store images from a URL on your mobile device and gain insights into creating forms efficiently. The step-by-step instructions in this resource are practical and easy to follow, making it a valuable tool for individuals looking to download photos from the Theta-Client list. The guide’s structured approach ensures a seamless learning experience, enabling users to master these essential tasks quickly and effectively.

The target audience for this guide is those in car sales. Since our target audience is car dealers, we’re interested in grabbing input from the user and adding the following data to our images: vehicle ID number, make of car (like Toyota), model of car (like Camry), location of car (like Austin, Texas, USA), and additional notes. The target audience for this guide is for those in car sales, but this can be easily modified based on your preferences

This guide will follow up on the previous article; hence the previous one is a prerequisite. There, you will learn more about the project’s setup. If you have done that, come here, open the project in your IDE, then navigate to your terminal and make sure you are on the right path—“demos/demo-react-native.”

Launch your Android studio virtual device emulator, after which you should return to your project and run “yarn android.”

This guide will be divided into two parts—“Displaying and Verifying the Image URl” and “Downloading the images and creating the data center (form).”

Note this is a continuation of my last article. You need this to understand this guide.

Part 1: Displaying and Verifying the Image URL

Since we need the url to be able to download the image, let’s try to detect what the url looks like. To do this, we will modify the content in the items variable by adding a new button named ViewURL:

<Button onPress={() => console.log(item.fileUrl)}>View URL</Button>

This new button does a simple task: logging out the fileUrl.

const items = files.map((item) => (
  <TouchableOpacity style={styles.cardWrapper} key={item.name}>
    <Card style={{ padding: 10 }}>
      <Card.Title
        title="Testing"
        subtitle={item.dateTimeZone}
        left={LeftContent}
      />
      <Card.Content>
        <Tex variant="titleLarge">{item.name}</Tex>
        <Tex variant="bodyMedium">
          {item?.imageDescription || "Simple Image"}
        </Tex>
      </Card.Content>
      <Card.Cover source={{ uri: item.thumbnailUrl }} />
      <Card.Actions>
        <Button onPress={() => onSelect(item)}>View</Button>
        <Button onPress={() => console.log(item.fileUrl)}>View URL</Button>
      </Card.Actions>
    </Card>
  </TouchableOpacity>
));

Save the newly added changes and reload the build. Open your Virtual device and click on “List Photos.”

Next, click on the newly added button “View URL” for any of the listed images and confirm with the metro environment what was logged out.

For instance, I clicked on the sixth, seventh and eighth images in the list and got these URLs. Also, note that your url might not be in the same format if you are not using the Fake API endpoint in your build.

Verify the Image URL

Before proceeding to the next segment, we must verify if the fileURL is accessible and can be parsed for downloading. To do this, copy one of the url that was logged out and paste it into the Google search box. If it returns the image, it is accessible, and we are good to go.

Now, let’s move on to the second part of this guide.

Part 2: Downloading the Images and Creating the Data Center (form)

To implement this feature (Downloading images) in the React Native Demo, we need to use a package called rn-fetch-blob—this will help us access the file system in the React Native Build. Here are the sub-steps in this section:

  • Install and configure rn-fetch-blob
  • We will need to create a Data center component since we want to create a data center that matches the image path after downloading it.
  • Add the newly created Data Center Component to the Navigation stack in the App.tsx file. (Installing the AsyncStorage package (which is like the localStorage for React Native))
  • Create the Logic to download images from their URL and navigate it to the Data Center Component once downloaded.
  • Add a standard notification handler using the “react-native-toast-notifications” package.
  • Run and test the overall build, access the images, and confirm that the data center matches the image.

Objective 1: Install and Configure rn-fetch-blob

Rn-fetch-blob is a project committed to making file access and data transfer easier and more efficient for React Native developers. In essence, it helps React Native Developers easily access the file system. In this build, we need to access the file system to write to it (add images to the file system).

Step 1: Install the library

$ yarn add rn-fetch-blob

Step 2: Navigate to your package.json file and check your React Native Version. If it is <0.60, run the command below to Link the library.

$ npx react-native link rn-fetch-blob

Step 3: To modify the file system, we need to request permission from Android. If you are conversant with applications that use your file system, like using your camera or music, you’d have noticed that they pop up a dialogue box that says, “The name of that app wants to access your camera. Do you want to allow it i.e give access, and you also get to specify when exactly you want to be giving it access is it every time or only when the app is active.” We also need to do this for this app to access the camera and photos.

  1. Head over to React Native Docs on Permissions for Android to learn more about the various available permissions.

  2. Now, let’s enable the permissions in our build file. But first, open the app settings for your build in your Virtual Device Manager, and you will notice that permissions are completely disabled.

Next, we want to enable permissions. First, we enable them, then request the ones we need. To do this, navigate to your Android module and add the necessary permissions. In your demo-react-native folder, navigate to the android directory, then app, src, and main. Inside the main directory, open the AndroidManifest.xml, where we will add the new permissions.

$ demo-react-native > android > app > src > main > AndroidManifest.xml

Just to add, the major permissions we need are: “the write to external storage” (which makes us add to the storage - like adding images) and “the read from external storage” (which helps us to read data from the file system - like getting images from the storage). Note that I added many permissions because there is a little variation – “READ_MEDIA_IMAGES” is supported for the Android 13 version, which is what my Virtual Device uses. We won’t need the camera and others, though. Just the “WRITE_EXTERNAL_STORAGE”, “READ_EXTERNAL_STORAGE”, and “READ_MEDIA_IMAGES”. These permissions enable us to read (access files in device storage) and write (add a file to storage) on the entity (device storage).

Now, reload your build and go back to access your app settings.

What we need to give access to based on the Android Permissions added to the build:

Next, since we have enabled permissions, we need to grant access. To do this, we would use the PermissionsAndroid module from ‘react-native’. Then, we request multiple permissions on all apps our build requires to function effectively.

Creating Permission Request Function

  • Navigate into the ListPhotos.tsx file
  • Add the PermissionsAndroid module to the imports modules from ‘react-native’
import {
  StatusBar,
  RefreshControl,
  ScrollView,
  TouchableOpacity,
  Alert,
  PermissionsAndroid,
} from 'react-native';
  • Create the grantPermission function by utilising the requestMultiple method, which React Native provides.
const grantPermission = async (url: any) => {
  const granted = await PermissionsAndroid.requestMultiple([
    PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
    PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
    PermissionsAndroid.PERMISSIONS.CAMERA,
    PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
  ]);

  console.log("granted", granted);
  downloadImage(url);

  return granted;
};

Create a downloadImage function and call it inside the grantPermission function.

const downloadImage = async (url: any) => {
  // logic to write the download image function
  console.log(url);
};

Trigger grantPermission when the download button is clicked by adding this line of code to your Card render:

<Button onPress={async () => {grantPermission(item.fileUrl);}}>Download</Button>

What your Card component should look like now:

const items = files.map((item) => (
  <TouchableOpacity style={styles.cardWrapper} key={item.name}>
    <Card style={{ padding: 10 }}>
      <Card.Title
        title="Testing"
        subtitle={item.dateTimeZone}
        left={LeftContent}
      />
      <Card.Content>
        <Tex variant="titleLarge">{item.name}</Tex>
        <Tex variant="bodyMedium">
          {item?.imageDescription || "Simple Image"}
        </Tex>
      </Card.Content>
      <Card.Cover source={{ uri: item.thumbnailUrl }} />
      <Card.Actions>
        <Button onPress={() => onSelect(item)}>View</Button>
        <Button
          onPress={async () => {
            grantPermission(item.fileUrl);
          }}
        >
          Download
        </Button>
      </Card.Actions>
    </Card>
  </TouchableOpacity>
));

Here’s a summary of what the grantPermission function does:

  • Requests Permissions: Asks the user for multiple storage and camera access permissions.
  • Logs Permission Statuses: Prints the statuses of the requested permissions to the console.
  • Downloads Image: Calls the downloadImage function with a given URL to download an image.
  • Returns Permission Statuses: Returns the statuses of the requested permissions

Reload the build, click the download button, and a dialogue box will pop out asking you to give this app permission to access your camera, Photos, etc. Grant access, then navigate to your app settings in your virtual device to confirm if the access has been granted.

Allow app to access the file system:

You should get an interface identical to the snapshot below if access has been granted.

This is what is logged out:

Objective 2: Creating the logic for downloading the image.

Here, we will use the rn-fetch-blob package to access and write to the file system.

Step 1: Import the RNFetchBlob package

import RNFetchBlob from 'rn-fetch-blob';

Step 2: Navigate to the downloadImage function to create the logic for downloading images via the URL.

const downloadImage = async (url: any) => {
  // Define the path where you want to save the file
  const { config, fs } = RNFetchBlob;
  const downloads = fs.dirs.DownloadDir;
  const path = `${downloads}/image_${Date.now()}.jpg`;

  // Start downloading the image
  config({
    fileCache: true,
    addAndroidDownloads: {
      useDownloadManager: true,
      notification: true,
      path: path,
      description: "Downloading image",
    },
  })
    .fetch("GET", url)
    .then((res) => {
      console.log("The file is saved to:", res.path());
      Alert.alert(`Image downloaded successfully to: ${res.path()}`);
    })
    .catch((error) => {
      console.error("Error downloading image:", error);
      Alert.alert(`Failed to download image due to: ${error}`);
    });
};
  • We downloaded this image using the RNFetchBlob package by destructuring the config and fs methods. We then defined the directory to which we wanted to save the images. Here, we chose the download directory of the storage, but you can also choose another directory, like the picture directory, which is also available. The next thing is defining the path we want the image to take - using template literals (``), adding a string “image_” to signify the type, Date.now() to create a random number and finally adding the extension .jpg

  • Next, configure the download using rn-fetch-blob by enabling file caching and Android’s download manager for a better user experience, including showing a notification (while downloading, you will see “Downloading image”).

  • We then fetched the image from the url via the GET method. This initiates a download request for the image from the provided URL.

  • Lastly, we handled the request type by displaying the right message via the Alert method for each scenario.

Now, back to testing the current state of the build. Reload the build, navigate to the ListPhotos component, click on the download button, enable access and wait till you see the notification that the image download has been successful.

Drag down your slide drawer to view the notification:

Click on the notification to access the image.

Try accessing the image from your photos. It should be in the download directory.

There is an image in the Picture directory because my first test used the Picture directory before switching it to Downloads.

The image file location is logged out.

Objective 3: Create a data file with the local device and match the image to the data file.

This feature requires three steps. But first, let’s define its flow.

The flow of this Feature

  1. Download the image successfully.
  2. Click “OK” to confirm the download. The app will automatically navigate to the Data Center Component, where the forms are embedded.
  3. Input the information, click save, and the file is saved to the local device using the AsyncStorage.
  4. The app navigates to the previous page (ListPhotos) component.

Step 1: Install the AsyncStorage right in the demo-react-native using the command below:

yarn add @react-native-async-storage/async-storage

AsyncStorage is an asynchronous, unencrypted, persistent key-value storage for React Native apps. It has a simple API and is a good choice for storing small amounts of data, such as user preferences or app states. If you are familiar with the web, you will know this is similar to LocalStorage.

Step 2: Modify the downloadImage function to allow navigation to the Data Center component once the image has been downloaded. With this navigation, we will also share data, such as the imagePath.

To do this, add this line of code to your downloadImage function.

navigation.navigate('dataCenter', { imagePath: res.path() });

What the overall function should look like:

const downloadImage = async (any) => {
  // Define the path where you want to save the file
  const { config, fs } = RNFetchBlob;
  const downloads = fs.dirs.DownloadDir;
  const path = `${downloads}/image_${Date.now()}.jpg`;

  // Start downloading the image
  config({
    fileCache: true,
    addAndroidDownloads: {
      useDownloadManager: true,
      notification: true,
      path: path,
      description: "Downloading image",
    },
  })
    .fetch("GET", url)
    .then((res) => {
      console.log("The file is saved to:", res.path());
      Alert.alert(`Image downloaded successfully to: ${res.path()}`);
      navigation.navigate("dataCenter", { imagePath: res.path() });
    })
    .catch((error) => {
      console.error("Error downloading image:", error);
      Alert.alert(`Failed to download image due to: ${error}`);
    });
};

Step 3: Create the Data Center component
The target audience for this guide is those in car sales, but this can be modified based on your preferences. Since our target audience is Car dealers, then the Data Center will be taking in the following inputs:

  • vehicle ID number
  • make of car (like Toyota)
  • model of car (like Camry)
  • local of car (like Austin, Texas, USA)
  • additional notes
import React, { useState } from "react";
import { View, TextInput, Button, Alert, StyleSheet } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";

const DataCenter = ({ navigation, route }) => {
  const [vehicleId, setVehicleId] = useState("");
  const [make, setMake] = useState("");
  const [model, setModel] = useState("");
  const [location, setLocation] = useState("");
  const [notes, setNotes] = useState("");
  const { imagePath } = route.params;

  const saveData = async () => {
    const data = {
      vehicleId,
      make,
      model,
      location,
      notes,
      imagePath,
    };

    try {
      const jsonValue = JSON.stringify(data);
      console.log(jsonValue);
      await AsyncStorage.setItem(`@vehicle_${vehicleId}`, jsonValue);
      Alert.alert("Data saved successfully");
      navigation.goBack();
    } catch (e) {
      console.error("Error saving data", e);
      Alert.alert("Failed to save data");
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Vehicle ID"
        value={vehicleId}
        onChangeText={setVehicleId}
      />
      <TextInput
        style={styles.input}
        placeholder="Make"
        value={make}
        onChangeText={setMake}
      />
      <TextInput
        style={styles.input}
        placeholder="Model"
        value={model}
        onChangeText={setModel}
      />
      <TextInput
        style={styles.input}
        placeholder="Location"
        value={location}
        onChangeText={setLocation}
      />
      <TextInput
        style={styles.input}
        placeholder="Additional Notes"
        value={notes}
        onChangeText={setNotes}
      />
      <Button title="Save Data" onPress={saveData} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  input: {
    height: 40,
    borderColor: "gray",
    borderWidth: 1,
    marginBottom: 10,
    padding: 10,
  },
});

export default DataCenter;

Explanation of the code above

Imports necessary modules:

  • React and useState from ‘react’.
  • View, TextInput, Button, Alert, StyleSheet from ‘react-native’.
  • AsyncStorage from ‘@react-native-async-storage/async-storage’.

Component definition:

  • DataCenter is a functional component that takes navigation and route as props.

State declarations:

  • useState hooks are used to declare state variables: vehicleId, make, model, location, and notes, all initialized to empty strings.
  • imagePath is extracted from route.params.

Function to save data:

  • saveData is an asynchronous function that:
    • Creates a data object with the values of vehicleId, make, model, location, notes, and imagePath.
    • Converts the data object to a JSON string to save it.
    • Store the JSON string in AsyncStorage with a key formatted as @vehicle_{vehicleId}.
    • On successful storage, shows an alert “Data saved successfully” and navigates back.
    • If there’s an error, log the error to the console and show an alert “Failed to save data”.

Step 4: Updating the Stack navigator in the App.tsx file to render the Data Center component.

Here is the new Stack screen we are adding:

<Stack.Screen options={{ title: 'Data Center' }} name="dataCenter" component={DataCenter} />

Import the Data Center component at the top of your App.tsx file:

import DataCenter from './DataCenter';

The entire App.tsx file should look like this:

import * as React from "react";
import { PaperProvider } from "react-native-paper";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import MainMenu from "./MainMenu";
import TakePhoto from "./TakePhoto";
import ListPhotos from "./ListPhotos";
import PhotoSphere from "./PhotoSphere";
import DataCenter from "./DataCenter";

const Stack = createNativeStackNavigator();

const screenOptions = {
  headerStyle: {
    backgroundColor: "#6200ee",
  },
  headerTintColor: "#fff",
  headerTitleStyle: {
    fontWeight: "bold",
  },
  headerBackTitle: "",
};

const App = () => {
  return (
    <PaperProvider>
      <NavigationContainer>
        <Stack.Navigator screenOptions={screenOptions}>
          <Stack.Screen
            options={{ title: "Theta SDK sample app" }}
            name="main"
            component={MainMenu}
          />
          <Stack.Screen
            options={{ title: "Take Photo" }}
            name="take"
            component={TakePhoto}
          />
          <Stack.Screen
            options={{ title: "List Photos" }}
            name="list"
            component={ListPhotos}
          />
          <Stack.Screen
            options={{ title: "Sphere" }}
            name="sphere"
            component={PhotoSphere}
          />
          <Stack.Screen
            options={{ title: "Data Center" }}
            name="dataCenter"
            component={DataCenter}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </PaperProvider>
  );
};

export default App;

With these steps, your application should now allow you to download images, save vehicle data, and match the images with the data using AsyncStorage.

Now, when you click on the download button:

To access the data file saved, we will need to pass the key to the getItem method that AsyncStorage provides. You can read up on that from the official documentation and try to get the data you stored. Also, this time, you will use the JSON.parse() to read the data back to its original state.

Conclusion

In this guide, we examined how to use the RN-fetch-Blob to download images via their url and how to use AsyncStorage to save our data.

If you enjoyed this, please like, share and comment!

3 Likes

Build with iOS

I’m attempting to follow the tutorial for building on iOS, but ran into some challenges. I plan to go back to the issue at a later time.

Testing Environment

  • THETA X firmware version 2.51.0
  • MacOS version Sonoma 14.6
  • iOS version 17.5.1
  • XCode version 15.4
  • theta-client version 1.10

After downloading rn-fetch-blob, I followed these instructions to enable permissions for iOS. Here are the steps I did as per the instructions by the documentation:

Current Challenges

After these steps, I ran yarn ios and received the No Bundle URL error. I attempted to delete the build folder and rerun yarn react-native ios, but the issue didn’t resolve. However, it seems like a common issue for building on iOS, so I’ll look into it more later.

You probably were able to save the camera images to iOS with flutter in the past.

You can also try and go through this tutorial

Download Image - RICOH THETA Client React Native Demo

Though, the problem you’re encountering seems to be with iOS permissions.

There may be some issues in react-native-blog that are addressed in react-native-blog-util

The GitHub repo looks pretty recent

To save the images to iOS filesystem, I’ve been using React Native Share. However, I’m getting a permissions error.

After spending more time with react-native-blob-util, I was able to save an image to the application storage on iOS.

const remoteImageUrl='https://fake-theta.vercel.app/files/150100525831424d42075b53ce68c300/100RICOH/R0010015.JPG';

const getSampleImage = async () => {
  await ReactNativeBlobUtil.config({
    fileCache: true,
    appendExt: 'jpg',
  }).fetch('GET', remoteImageUrl).then((res) => {
    let status = res.info().status;
    console.log();
    console.log('status: ' + status);
    if (status == 200) {

To focus more on ReactNativeBlobUtil, I’m not using theta-client in this example, just fake-theta.

Start new React Native Project

npx @react-native-community/cli init ThetaLocalImage

Did not install CocoaPods

:heavy_check_mark: Do you want to install CocoaPods now? Only needed if you run your project in Xcode directly … no

  • cd into ios and run pod install
  • npx react-native run-ios

select run on iOS

Delete Most of Boilerplate Code

import React from 'react';
import type {PropsWithChildren} from 'react';
import {
  SafeAreaView,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  useColorScheme,
  View,
} from 'react-native';

function App(): React.JSX.Element {
  return (
    <SafeAreaView >
      <StatusBar/>
      <ScrollView>

      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
  },
});

export default App;

Simulator is blank

install react-native-blob-util

npm install --save react-native-blob-util
cd ios
pod install
cd ..

...
...
Pod install took 7 [s] to run
Integrating client project
Pod installation complete! There are 65 dependencies from the Podfile and 64 total pods installed.

[!] [Codegen] warn: using experimental new codegen integration
craig@craigs-air ios % 

test blob with lorem picsum

function App(): React.JSX.Element {

  const getRemoteImage = () => {
    ReactNativeBlobUtil.fetch('GET', 'https://picsum.photos/200/100')
    .then((res) => {
      let status = res.info().status;
      console.log('status: ', status.toString());
    })

  }
  return (
    <SafeAreaView >
      <StatusBar/>
      <ScrollView>
        <Button onPress={getRemoteImage} title='Get Image'/> 
      </ScrollView>
    </SafeAreaView>
  );
}

image

test blob with fake-theta


  const remoteImageUrl = 'https://fake-theta.vercel.app/files/150100525831424d42075b53ce68c300/100RICOH/R0010015.JPG';

  const getRemoteImage = () => {
    ReactNativeBlobUtil.fetch('GET', remoteImageUrl)
    .then((res) => {
      let status = res.info().status;
      console.log('status: ', status.toString());
    })

image

cache file to local iOS storage

const getRemoteImage = () => {
  ReactNativeBlobUtil
    .config({
      fileCache:true,
    })
    .fetch('GET', remoteImageUrl)
      .then((res) => {
        let status = res.info().status;
        console.log('status: ', status.toString());
        console.log('file path: ', res.path());
    })

image

add file extension .JPG

    ReactNativeBlobUtil
      .config({
        fileCache:true,
        appendExt: 'JPG',
      })

image

set up useState

import React, {useState} from 'react';
...
...
function App(): React.JSX.Element {

  const [responseStatus, setResponseStatus ] = useState('');
  const [ localFilePath, setLocalFilePath ] = useState('');

...
...
      .then((res) => {
        let status = res.info().status;
        console.log('status: ', status.toString());
        setResponseStatus(status.toString());
        console.log('file path: ', res.path());
        setLocalFilePath(res.path());

After return

      <ScrollView>
        <Button onPress={getRemoteImage} title='Get Image'/> 
        <Text>status: {responseStatus}</Text>
        <Text>local file: {localFilePath}</Text>
      </ScrollView>

Display Local Image

      <ScrollView>
        <Button onPress={getRemoteImage} title='Get Image'/> 
        <Text>status: {responseStatus}</Text>
        <Text>local file: {localFilePath}</Text>
        <Image source={{uri: localFilePath}} 
          style={{width: 400, height: 200}}/>
      </ScrollView>

Simple Text Styling

const styles = StyleSheet.create({
  responseText: {
    padding: 15,
    fontSize: 18,
  },

Will try this next
React Native Series: How to save an image from a remote url in React Native - DEV Community

The article above references camera-roll which is now replaced with GitHub - react-native-cameraroll/react-native-cameraroll: CameraRoll is a react-native native module that provides access to the local camera roll or photo library.

There’s some additional comments that rn-fetch-blob may be obsolete

Cannot read property 'DocumentDir' of null, js engine: hermes · Issue #832 · joltup/rn-fetch-blob · GitHub


Update: Sat Aug 17 early morning

I wasn’t able to use the article above due outdated packages. I’m now looking at this:

Using react-native-cameraroll to enable camera roll access - LogRocket Blog


Aug 17 late morning after fishing

Tried the dev.to again. Getting a little further, but still getting errors.

[Error: The operation couldn’t be completed. (PHPhotosErrorDomain error -1.)]

Final update.

Solution for iOS is here: