In this article, I will show you how to save an image from a remote url to your device using react-native.
TL/DR Find the full code in this github repo
I'm assuming you already have node and react-native set up, if you don't, please check out the official docs to learn how to set up a react native project.
The following packages are required for this tutorial
Installing the packages
Install the packages using yarn
yarn add @react-native-community/cameraroll rn-fetch-blob
or npm
npm install @react-native-community/cameraroll rn-fetch-blob --save
Link the native code
if your react-native version >= 0.60, autolink using
cd ios && pod install
else link manually with
react-native link @react-native-community/cameraroll
react-native link rn-fetch-blob
With this, we are done installing the required packages. Navigate to app.js
project_dir -> app.js
delete the default react native code and replace it with this block
// app.js
import React from 'react';
import {SafeAreaView, StyleSheet, StatusBar, Text} from 'react-native';
class App extends React.Component {
render() {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
<Text>Save Image</Text>
</SafeAreaView>
</>
);
}
}
export default App;
const styles = StyleSheet.create({
container: {
backgroundColor: '#2FF345CC',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
Run the app in the simulator with
react-native run-ios # or react-native run-android
You should see something like this
Setting up permissions
The user's permission is required for cameraroll to save images to their device. On ios, NSPhotoLibraryUsageDescription
and NSPhotoLibraryAddUsageDescription
keys must be set up in info.plist with a string that describes how your app will use this data. Navigate to info.plist
project_dir -> ios -> project_name -> info.plist
add the following block to info.plist
<!-- info.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app would like to save images to your device.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app would like to save images to your device.</string>
...
</dict>
</plist>
Bear in mind that the description string must be a bit more descriptive than this.
On android, permission is required to write to external storage. Permission can be requested by adding the following block to AndroidManifest.xml
# navigate to AndroidManifest.xml
project_dir -> android -> app -> src -> main -> AndroidManifest.xml
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.saveremoteimage">
...
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
Implementation
This is an image of what the end product would look like
Let's build the UI. First, start by creating a view that would contain all our components
// app.js
...
import {SafeAreaView, StyleSheet, StatusBar, Text, View} from 'react-native';
class App extends React.Component {
render() {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
<View style={styles.app}>
<Text>Save Image</Text>
</View>
</SafeAreaView>
</>
);
}
}
...
const styles = StyleSheet.create({
...
app: {
backgroundColor: '#11131B',
flex: 1,
alignSelf: 'stretch',
alignItems: 'center',
paddingVertical: 30,
},
});
Then we add a TextInput. This TextInput is where the url of the image would be entered
// app.js
...
import {
...
TextInput,
} from 'react-native';
class App extends React.Component {
render() {
return (
<>
...
<View style={styles.app}>
<Text style={styles.headerText}>React Native Image Downloader</Text>
<View style={styles.textInputWrapper}>
<TextInput
placeholder="Enter image url here"
style={styles.textInput}
/>
</View>
<Text>Save Image</Text>
</View>
...
</>
);
}
}
...
const styles = StyleSheet.create({
...
headerText: {
marginTop: 50,
fontSize: 26,
color: 'white',
},
textInputWrapper: {
marginTop: 30,
alignSelf: 'stretch',
padding: 10,
},
textInput: {
padding: 10,
backgroundColor: '#EFEFEF',
borderWidth: 1,
borderColor: '#DDD',
borderRadius: 3,
},
});
At this point, your app should look like this
Now, let's create a view that would contain a preview of the image to be downloaded
// app.js
...
class App extends React.Component {
render() {
return (
<>
...
<View style={styles.app}>
...
<View style={styles.imagePreview} />
<Text>Save Image</Text>
</View>
...
</>
);
}
}
...
const styles = StyleSheet.create({
...
imagePreview: {
height: 300,
width: 300,
backgroundColor: 'purple',
marginTop: 30,
},
});
Finally, let's add a button we can use to initiate the download.
// app.js
...
import {
...
TouchableOpacity,
} from 'react-native';
class App extends React.Component {
render() {
return (
<>
...
<View style={styles.app}>
...
<View style={styles.imagePreview} />
<TouchableOpacity style={styles.downloadButton}>
<Text>Download Image</Text>
</TouchableOpacity>
</View>
...
</>
);
}
}
...
const styles = StyleSheet.create({
...
downloadButton: {
backgroundColor: 'white',
marginTop: 40,
paddingHorizontal: 40,
paddingVertical: 20,
borderRadius: 3,
},
});
At the this point, your app should look like this
Now we bind the value of the TextInput
to state
so it is accessible elsewhere.
...
class App extends React.Component {
state = {
url: '',
};
updateUrl = url => {
this.setState({url});
};
render() {
const { url } = this.state;
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
<View style={styles.app}>
...
<View style={styles.textInputWrapper}>
<TextInput
placeholder="Enter image url here"
style={styles.textInput}
value={url}
onChangeText={text => this.updateUrl(text)}
/>
</View>
...
</SafeAreaView>
</>
);
}
}
export default App;
...
Next, we'll bind the url in state to an image componenent so it can be displayed.
import React from 'react';
import {
...
Image,
} from 'react-native';
class App extends React.Component {
...
render() {
..
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
<View style={styles.app}>
<Text style={styles.headerText}>React Native Image Downloader</Text>
<View style={styles.textInputWrapper}>
...
</View>
{/* we've replaced the View with an Image*/}
<Image source={{uri: url}} style={styles.imagePreview} />
<TouchableOpacity style={styles.downloadButton}>
<Text>Download Image</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</>
);
}
}
...
Adding download functionality
Let's create a handleDownload
function and attach it to our TouchableOpacity
import React from 'react';
import { ... } from 'react-native';
import CameraRoll from '@react-native-community/cameraroll';
import RNFetchBlob from 'rn-fetch-blob';
class App extends React.Component {
...
handleDownload = async () => {
RNFetchBlob.config({
fileCache: true,
appendExt: 'png',
})
.fetch('GET', this.state.url)
.then(res => {
CameraRoll.saveToCameraRoll(res.data, 'photo')
.then(res => console.log(res))
.catch(err => console.log(err))
})
.catch(error => console.log(error);
};
render() {
...
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.container}>
...
<TouchableOpacity
style={styles.downloadButton}
onPress={this.handleDownload}>
<Text>Download Image</Text>
</TouchableOpacity>
...
</SafeAreaView>
</>
);
}
}
...
For android devices you need to ensure you have permission to save images.
import React from 'react';
import {
...
PermissionsAndroid,
Alert,
} from 'react-native';
import CameraRoll from '@react-native-community/cameraroll';
import RNFetchBlob from 'rn-fetch-blob';
class App extends React.Component {
...
getPermissionAndroid = async () => {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
{
title: 'Image Download Permission',
message: 'Your permission is required to save images to your device',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
return true;
}
Alert.alert(
'Save remote Image',
'Grant Me Permission to save Image',
[{text: 'OK', onPress: () => console.log('OK Pressed')}],
{cancelable: false},
);
} catch (err) {
Alert.alert(
'Save remote Image',
'Failed to save Image: ' + err.message,
[{text: 'OK', onPress: () => console.log('OK Pressed')}],
{cancelable: false},
);
}
};
handleDownload = async () => {
// if device is android you have to ensure you have permission
if (Platform.OS === 'android') {
const granted = await this.getPermissionAndroid();
if (!granted) {
return;
}
}
RNFetchBlob.config({
fileCache: true,
appendExt: 'png',
})
.fetch('GET', this.state.url)
.then(res => {
...
};
render() {
...
}
}
...
Finally lets add an ActivityIndicator and some helpful feedback messages. Your final code should look like this:
Here's a little video demo.
Real simple and concise. Nice one, majiyd.