Transform your React Native app with offline audio & video downloads!

Transform your React Native app with offline audio & video downloads!

In this tutorial, we’ll explore how to integrate essential features such as audio and video download, deleting specific content, clearing all downloads at once, and displaying a progress bar while downloading in a React Native application. This comprehensive guide will equip you with the tools and knowledge needed to enhance your app’s offline capabilities, providing users with a seamless and interactive experience using the rn-fetch-blob library.📱🎵📹

The Offline Content Management feature in React Native using RNFetchBlob allows users to download audio and video content for offline playback. This documentation provides a comprehensive guide on setting up, implementing, and managing offline content in a React Native application.📚🔧

Let’s dive in and unlock the potential of offline audio and video downloads in React Native!💡

Features🎯🚀

Audio and Video Download: Seamlessly download audio and video content to enjoy offline.📥

Check Download Status: Easily check if content is already downloaded with intuitive icons.✔️❌

Interactive Progress Bar: Stay informed with a circular progress bar and percentage display during downloads.🔄📊

Efficient Deletion: Delete individual downloaded content with a single tap for decluttering.🗑️

Bulk Deletion: Clear out your offline library by deleting all downloaded content at once.🧹

🎬 Let’s kick things off with a bang – check out our captivating demo video showcasing the download service in action!🚀🚀

Prerequisites📚🛠️

Knowledge of React Native development.
Familiarity with JavaScript and React Native libraries.
Installation of Node.js, npm/yarn, and React Native CLI.
Basic understanding of file handling and storage concepts.

Setting Up the Project🛠️🔧

1). Create a new React Native project using the following command:

npx react-native init OfflineDownloadApp

2). Navigate to the project directory:

cd OfflineDownloadApp

3).Install the required dependencies:

npm install –save rn-fetch-blob
— or —
yarn add rn-fetch-blob

Package.json Dependencies
Below are the dependencies specified in the package.json file for a React Native project:

{
“dependencies”: {
“react”: “18.2.0”,
“react-native”: “0.72.3”,
“rn-fetch-blob”: “^0.12.0”
},
}

Event Emission📢📢

The React Native Download Service emits events using DeviceEventEmitter to provide feedback on download progress and completion.

downloadProgress: Emits progress updates during the download process.

downloadDone: Emits an event when a download is completed.

downloadError: Emits an event in case of download errors.

Additional Notes📝📝

This service uses RNFetchBlob for file operations and download management.
Metadata related to downloaded content is stored locally in a JSON file.
Error handling and notifications are managed using react-native-toast-message.

Build UI🛠️

Delve into intuitive Audio Player UI, designed for immersive music experiences. Explore playback controls, and artist details. Additionally, discover app’s Video Grid Rail, Audio Grid Rail, and Downloaded Content Rail, making it easy to explore and organize multimedia content with convenience and style.

Video Grid Design Code:📹🔲

This code is for a Video Rail component, which displays a horizontal list of trending videos.

It contains a function handlePlay to navigate to a player screen when a video thumbnail is pressed.

The renderSongItem function renders each video item in the FlatList, displaying the thumbnail, title, and index number.

Overall, the VideoGrid component creates a visually appealing grid layout for showcasing trending videos with a play feature that navigates to a player screen for selected videos.

import {
FlatList,
ImageBackground,
StyleSheet,
Text,
TouchableOpacity,
View,
} from ‘react-native’;
import React from ‘react’;
import {VIDEO_DATA} from ‘../../data’;

const VideoGrid = ({navigation}) => {
const handlePlay = (url, thumbnail, item) => {
navigation.navigate(‘PlayerScreen’, {
source: url,
posterImage: thumbnail,
data: item,
});
};

const renderSongItem = ({item, index}) => (
<TouchableOpacity
style={styles.songItemContainer}
onPress={() => handlePlay(item.videoUrl, item.thumbnailUrl, item)}>
<ImageBackground
resizeMode=”cover”
source={{uri: item.thumbnailUrl}}
style={styles.songItem}>
<View style={styles.overlay} />
<Text style={styles.title}>{item.title}</Text>
<View style={styles.box}>
<Text style={styles.numberStyle}>{index + 1}</Text>
</View>
</ImageBackground>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<Text style={styles.heading}>Trending Videos</Text>
<FlatList
data={VIDEO_DATA}
renderItem={renderSongItem}
keyExtractor={item => item.id}
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.flatlistContent}
bounces={false}
/>
</View>
);
};

export default VideoGrid;

const styles = StyleSheet.create({
container: {
backgroundColor: ‘#000’,
},
heading: {
fontSize: 18,
fontWeight: ‘bold’,
marginBottom: 10,
marginHorizontal: 10,
color: ‘#fff’,
},
flatlistContent: {
alignItems: ‘center’,
},
songItemContainer: {
marginHorizontal: 5,
},
songItem: {
width: 160,
height: 250,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
padding: 20,
borderRadius: 4,
overflow: ‘hidden’,
},
title: {
fontSize: 18,
textAlign: ‘center’,
fontWeight: ‘bold’,
color: ‘#fff’,
marginBottom: -10,
width: ‘150%’,
},
artist: {
fontSize: 14,
color: ‘#888’,
fontWeight: ‘700’,
},
overlay: {
…StyleSheet.absoluteFillObject,
backgroundColor: ‘rgba(0,0,0,0.5)’,
},
playIcon: {
width: 30,
height: 30,
tintColor: ‘#fff’, // Adjust the play icon color as needed
},
numberStyle: {
fontSize: 70,
color: ‘#fff’,
fontWeight: ‘bold’,
},
box: {position: ‘absolute’, top: -10, left: 0},
});

Audio Grid Design Code:🎵🔲

This code is for a Audio Rail component, which displays a horizontal list of trending videos.

The renderSongItem function renders each audio item in the FlatList, displaying the artwork, title, and artist.

Overall, the AudioGrid component creates a visually appealing grid layout for browsing and playing audio content.

import React from ‘react’;
import {
View,
Text,
FlatList,
StyleSheet,
TouchableOpacity,
ImageBackground,
Image,
} from ‘react-native’;
import {AUDIO_DATA} from ‘../../data’;

const AudioGrid = ({navigation}) => {

const handlePlay = (url, thumbnail, item) => {
// Handle play action here
navigation.navigate(‘PlayerScreen’, {
source: url,
posterImage: thumbnail,
isAudio: true,
data: item,
});
};

// Render item for FlatList
const renderSongItem = ({item}) => (
<TouchableOpacity
style={styles.gridItem}
onPress={() => handlePlay(item.url, item.artwork, item)}>
<ImageBackground
resizeMode=”cover”
source={{uri: item.artwork}}
style={styles.songItem}>
<View style={styles.overlay} />
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.artist}>{item.artist}</Text>
<View
style={{position: ‘absolute’, top: 5, right: 5}}
onPress={() => {}}>
<Image
style={{width: 25, height: 25}}
source={require(‘../../icons/playGreen.png’)}
/>
</View>
</ImageBackground>
</TouchableOpacity>
);

return (
<View style={styles.container}>
<Text style={styles.heading}>Browse all Audio</Text>
<FlatList
data={AUDIO_DATA}
renderItem={renderSongItem}
keyExtractor={item => item.id}
numColumns={2}
scrollEnabled={false}
contentContainerStyle={{backgroundColor: ‘#000’, marginBottom: 50}}
/>
</View>
);
};

export default AudioGrid;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: ‘#000’,
marginTop: 40,
},
heading: {
fontSize: 18,
fontWeight: ‘bold’,
marginBottom: 10,
marginHorizontal: 10,
color: ‘#fff’,
},
gridItem: {
flex: 1,
margin: 5,
},
songItem: {
flex: 1,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
padding: 20,
borderRadius: 4,
overflow: ‘hidden’,
},
title: {
fontSize: 14,
fontWeight: ‘bold’,
color: ‘#fff’,
marginBottom: 5,
},
artist: {
fontSize: 12,
color: ‘#fff’,
fontWeight: ‘600’,
},
overlay: {
…StyleSheet.absoluteFillObject,
backgroundColor: ‘rgba(0,0,0,0.7)’,
},
playIcon: {
width: 30,
height: 30,
tintColor: ‘#fff’, // Adjust the play icon color as needed
},
});

Downloaded Grid Design Code:📥🔲

This code is for a Downloaded Rail component, which displays a horizontal list of Downloaded audio/videos.

The OfflineDownloadGrid component efficiently manages and displays downloaded content in a visually appealing grid layout, providing playback and deletion functionalities as needed.

import {
FlatList,
ImageBackground,
StyleSheet,
Text,
TouchableOpacity,
View,
Image,
DeviceEventEmitter,
Platform,
} from ‘react-native’;
import React, {useEffect, useState} from ‘react’;
import {
deleteAllDownloadDataFromLocal,
deleteContentFromLocalDir,
fetchDownloadedDataFromLocalDir,
} from ‘../../services/downloadService’;
import DeletionModal from ‘../../components/DeletionModal’;
import {useIsFocused} from ‘@react-navigation/core’;
import RNFetchBlob from ‘rn-fetch-blob’;

const OfflineDownloadGrid = ({navigation}) => {
const [data, setData] = useState([]);
const [reRender, setReRender] = useState(false);
const [isDeletionModalVisible, setDeletionModalVisible] = useState(false);
const [isSelectedPress, setSelectedPress] = useState(false);

const isFocused = useIsFocused();

var TrackFolder =
Platform.OS === ‘android’
? RNFetchBlob.fs.dirs.CacheDir
: RNFetchBlob.fs.dirs.DocumentDir;

const fetchDownloadedData = () => {
fetchDownloadedDataFromLocalDir(item => {
const sortedData = item.sort((a, b) => {
const dateA = new Date(a.downloadDate);
const dateB = new Date(b.downloadDate);
return dateB – dateA;
});

setData(sortedData);
});
};

useEffect(() => {
fetchDownloadedData();
}, [reRender, isFocused]);

useEffect(() => {
setSelectedPress(true);
}, [isFocused]);

useEffect(() => {
const downloadListenerStatus = DeviceEventEmitter.addListener(
‘downloadDone’,
e => {
setReRender(true);
},
);
return () => {
downloadListenerStatus.remove();
};
}, []);

const handlePlay = (url, thumbnail, item) => {
const finalUrl = url.split(‘/’).pop();
const offlineUrl = TrackFolder + ‘/’ + finalUrl;

const thumbnailImage =
Platform.OS === ‘android’ ? ‘file://’ + thumbnail : thumbnail;

navigation.navigate(‘PlayerScreen’, {
source: offlineUrl,
posterImage: thumbnailImage,
data: item,
offline: true,
});
};

const onInsideMenuPress = () => {
setDeletionModalVisible(true);
};

const onDeletionPress = id => {
deleteContentFromLocalDir(id);
setTimeout(() => {
fetchDownloadedData();
}, 500);
};

const onToggleSelectPress = () => {
setSelectedPress(!isSelectedPress);
};

const onDeleteAllPress = () => {
deleteAllDownloadDataFromLocal();
fetchDownloadedData();
};

const renderSongItem = ({item, index}) => {
const {source, posterImage, songName, artistName, id} = item;

const offlinePosterImageUrl =
Platform.OS === ‘android’ ? ‘file://’ + posterImage : posterImage;

return (
<>
<TouchableOpacity
key={index}
style={styles.songItemContainer}
onPress={() => handlePlay(source, posterImage, item)}>
<ImageBackground
resizeMode=”cover”
source={{uri: offlinePosterImageUrl}}
style={styles.songItem}>
<View
style={isSelectedPress ? styles.overlay : styles.overlayAlt}
/>
<Text style={styles.title}>{songName}</Text>
<Text style={styles.artist}>{artistName}</Text>
{isSelectedPress ? (
<TouchableOpacity
onPress={onInsideMenuPress}
style={styles.insideMenuContainer}>
<Image
style={styles.insideMenu}
source={require(‘../../icons/menu.png’)}
/>
</TouchableOpacity>
) : (
<Image
style={styles.insideMenuContainerAlt}
source={require(‘../../icons/greenIcon.png’)}
/>
)}
</ImageBackground>
</TouchableOpacity>

<DeletionModal
isModalVisible={isDeletionModalVisible}
onClosePress={() => setDeletionModalVisible(false)}
onDeletionPress={() => onDeletionPress(id)}
/>
</>
);
};
return (
<View style={styles.container}>
<View style={styles.wrapper}>
<Text style={styles.heading}>Your Downloads</Text>

{data?.length > 1 ? (
<View style={{flexDirection: ‘row’}}>
{!isSelectedPress ? (
<TouchableOpacity onPress={onDeleteAllPress}>
<Image
style={{width: 25, height: 25, marginRight: 20}}
source={require(‘../../icons/delete.png’)}
/>
</TouchableOpacity>
) : null}
<TouchableOpacity onPress={onToggleSelectPress}>
<Image
style={{width: 25, height: 25}}
source={
isSelectedPress
? require(‘../../icons/unselected.png’)
: require(‘../../icons/aa.png’)
}
/>
</TouchableOpacity>
</View>
) : null}
</View>
{data?.length > 0 ? (
<FlatList
data={data}
renderItem={renderSongItem}
keyExtractor={item => item.id}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.flatlistContent}
bounces={false}
numColumns={3}
/>
) : (
<>
<Image
resizeMode=”cover”
style={styles.downloadContainer}
source={require(‘../../icons/404-removebg.png’)}
/>
<Text style={styles.downloadText}>No download found</Text>
</>
)}
</View>
);
};

export default OfflineDownloadGrid;

const styles = StyleSheet.create({
container: {
backgroundColor: ‘#000’,
},
heading: {
fontSize: 18,
fontWeight: ‘bold’,
marginBottom: 10,
marginHorizontal: 10,
color: ‘#fff’,
},
flatlistContent: {
paddingBottom: 60,
alignItems: ‘flex-start’,
},
songItemContainer: {
marginHorizontal: 5,
marginBottom: 10,
},
songItem: {
width: 120,
height: 200,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
padding: 20,
borderRadius: 4,
overflow: ‘hidden’,
},
title: {
fontSize: 18,
textAlign: ‘center’,
fontWeight: ‘bold’,
color: ‘#fff’,
marginBottom: 5,
width: ‘150%’,
},
artist: {
fontSize: 13,
textAlign: ‘center’,
color: ‘#fff’,
fontWeight: ‘600’,
width: ‘150%’,
marginBottom: -17,
},
overlay: {
…StyleSheet.absoluteFillObject,
backgroundColor: ‘rgba(0,0,0,0.7)’,
},
overlayAlt: {
…StyleSheet.absoluteFillObject,
backgroundColor: ‘rgba(0,0,0,0.8)’,
},
playIcon: {
width: 30,
height: 30,
tintColor: ‘#fff’,
},
numberStyle: {
fontSize: 70,
color: ‘#fff’,
fontWeight: ‘bold’,
},
insideMenu: {
width: 20,
height: 20,
},
wrapper: {
flexDirection: ‘row’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
marginBottom: 10,
},
insideMenuContainer: {
position: ‘absolute’,
top: 8,
right: 2,
},
insideMenuContainerAlt: {
position: ‘absolute’,
top: 8,
right: 2,
width: 20,
height: 20,
},
downloadContainer: {
width: ‘80%’,
height: 200,
marginTop: -40,
alignSelf: ‘center’,
marginTop: 10,
},
downloadText: {
color: ‘#fff’,
textAlign: ‘center’,
fontSize: 18,
marginVertical: 20,
fontWeight: ‘400’,
},
});

Deletion Modal Design Code:🗑️📝

import {Image, StyleSheet, Text, TouchableOpacity, View} from ‘react-native’;
import React from ‘react’;
import CustomModal from ‘../customModal’;

const DeletionModal = ({
isModalVisible = false,
onClosePress = () => {},
onDeletionPress = () => {},
}) => {
return (
<CustomModal
isModalVisible={isModalVisible}
onClosePress={onClosePress}
modalContainerStyle={{}}
modalStyle={{backgroundColor: ‘#28282B’}}>
<View style={styles.wrapper}>
<Image
style={{width: 30, height: 30}}
source={require(‘../../icons/deletionIcon.png’)}
/>
</View>
<Text style={styles.heading}>Are you sure you want to delete this?</Text>

<View style={styles.buttonContainer}>
<TouchableOpacity onPress={onClosePress} style={styles.button1}>
<Text style={styles.btn}>No, Keep it.</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
onDeletionPress();
onClosePress();
}}
style={styles.button2}>
<Text style={styles.btn}>Yes, Delete!</Text>
</TouchableOpacity>
</View>
</CustomModal>
);
};

export default DeletionModal;

const styles = StyleSheet.create({
wrapper: {
width: 40,
height: 40,
backgroundColor: ‘#ffcccb’,
borderRadius: 20,
alignItems: ‘center’,
justifyContent: ‘center’,
alignSelf: ‘center’,
},
heading: {
color: ‘#fff’,
textAlign: ‘center’,
marginTop: 10,
fontWeight: ‘bold’,
fontSize: 15,
},
button1: {
width: 120,
height: 40,
backgroundColor: ‘#353935’,
borderRadius: 50,
justifyContent: ‘center’,
alignItems: ‘center’,
marginRight: 10,
},
button2: {
width: 130,
height: 40,
backgroundColor: ‘#F75454’,
borderRadius: 50,
justifyContent: ‘center’,
alignItems: ‘center’,
},
buttonContainer: {
flexDirection: ‘row’,
alignSelf: ‘center’,
marginVertical: 10,
},
btn: {
color: ‘#fff’,
fontWeight: ‘500’,
},
});

Audio/Video Player Design Code🎧📺

// VideoPlayer.js
import React, {useLayoutEffect, useRef, useState} from ‘react’;
import {
View,
Text,
StyleSheet,
Image,
TouchableOpacity,
Platform,
} from ‘react-native’;

import Video from ‘react-native-video’;
import Slider from ‘@react-native-community/slider’;
import LinearGradient from ‘react-native-linear-gradient’;

import {toHHMMSS} from ‘../../constants’;
import DownloadModal from ‘../../components/DownloadModal’;
import {
fetchDownloadedDataFromLocalDir,
sendDownloadedDataToLocalDir,
} from ‘../../services/downloadService’;
import Toast from ‘react-native-toast-message’;

const AudioPlayer = ({route, navigation}) => {
const videoRef = useRef(null);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sliderValue, setSliderValue] = useState(0);
const [totalVideoDuration, setTotalVideoDuration] = useState(0);
const [isModalVisible, setModalVisible] = useState(false);
const [isAlreadyDownload, setAlreadyDownload] = useState(false);

const src = route?.params?.source;
const posterImage = route?.params?.posterImage;
const isAudio = route.params?.isAudio || false;
const data = route?.params?.data;
const artistName = data?.artist ? data?.artist : data?.author;
const songName = data?.title;
const offlineMode = route?.params?.offline;
const isFromAudio = data?.isAudio;

useLayoutEffect(() => {
fetchDownloadedDataFromLocalDir(item => {
if (item?.length > 0) {
item.find(obj => {
if (obj?.contentId === data.id) {
setTimeout(() => {
setAlreadyDownload(true);
}, 100);
}
});
} else {
setAlreadyDownload(false);
}
});
}, []);

const onSliderValueChange = value => {
videoRef.current.seek(value);
setCurrentTime(value);
};

const onEnd = () => {
videoRef.current.seek(0);
setCurrentTime(0);
setIsPaused(true);
};

const onLoad = data => {
data?.duration > 0 && setTotalVideoDuration(data?.duration – 0.1 || 0);
setDuration(data.duration);
};

const onProgress = data => {
let currentTime = Math.floor(data.currentTime) || 0;
setSliderValue(currentTime);
setCurrentTime(data.currentTime);
};

const togglePlay = () => {
setIsPaused(!isPaused);
};

const onDownloadPress = () => {
setIsPaused(true);
setModalVisible(true);

sendDownloadedDataToLocalDir(
callback => {},
data.id,
src,
artistName,
songName,
posterImage,
isAudio,
);
};

const onAlreadyDownloadPress = () => {
showToast(
‘success’,
‘Already downloaded’,
‘This content is already downloaded 👋’,
);
};

const getPosterImage = () => {
if (isAudio || isFromAudio) {
return posterImage;
} else {
return null;
}
};

const poster = getPosterImage();

return (
<>
<Video
ref={videoRef}
source={{uri: src}}
style={styles.video}
resizeMode=”contain”
onLoad={onLoad}
onProgress={onProgress}
onEnd={onEnd}
paused={isPaused}
audioOnly={true}
autoplay={true}
ignoreSilentSwitch={‘ignore’}
playWhenInactive={true}
playInBackground={true}
poster={poster}
posterResizeMode=”cover”
/>
<LinearGradient
colors={[‘#000000’, ‘#0000000F’, ‘#000000’, ‘#000000’, ‘#0000000F’]}
start={{x: 0, y: 0.06}}
end={{x: 0, y: 1.4}}
style={styles.controlsContainer}
/>

<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.iconContainer}>
<Image
style={styles.closeIcon}
source={require(‘../../icons/closeAlt.png’)}
/>
</TouchableOpacity>

<View style={styles.titleContainer}>
<Text style={styles.songName}>{songName}</Text>
<Text style={styles.artistName}>{artistName}</Text>
</View>

<View style={styles.wrapper}>
{isAlreadyDownload ? (
<TouchableOpacity onPress={onAlreadyDownloadPress}>
<Image
style={[styles.suffelIcon, {marginRight: 20}]}
source={require(‘../../icons/downloaded.png’)}
/>
</TouchableOpacity>
) : offlineMode ? null : (
<TouchableOpacity onPress={onDownloadPress}>
<Image
style={[styles.suffelIcon, {marginRight: 20}]}
source={require(‘../../icons/downloadIcon.png’)}
/>
</TouchableOpacity>
)}

<Image
style={[styles.suffelIcon, {}]}
source={require(‘../../icons/likeAlt.png’)}
/>
</View>

<Slider
step={1}
thumbTouchSize={{
width: 10,
height: 10,
}}
style={styles.slider}
minimumValue={0}
maximumValue={duration}
value={sliderValue}
onValueChange={() => {}}
minimumTrackTintColor=”#fff”
maximumTrackTintColor=”#888″
thumbTintColor=”#fff”
/>

<View style={{position: ‘absolute’, bottom: 150, left: 22}}>
<Text style={{fontSize: 10, color: ‘#fff’}}>
{toHHMMSS(currentTime)}
</Text>
</View>

<View style={{position: ‘absolute’, bottom: 150, right: 20}}>
<Text style={{fontSize: 10, color: ‘#fff’}}>
{toHHMMSS(totalVideoDuration)}
</Text>
</View>

<PlayPauseComponent isPaused={isPaused} togglePlay={togglePlay} />
<FastForwardComponent />
<BackWardComponent />
<RepeatComponent />
<SuffelComponent />
<DownloadModal
isModalVisible={isModalVisible}
onClosePress={() => setModalVisible(false)}
contentId={data.id}
onDownloadFinished={item => {
if (item) {
setAlreadyDownload(true);
}
}}
/>
</>
);
};

const styles = StyleSheet.create({
video: {
top: 0,
left: 0,
right: 0,
bottom: 0,
position: ‘absolute’,
backgroundColor: ‘black’,
width: ‘100%’,
height: ‘100%’,
},
slider: {
width: Platform.OS === ‘ios’ ? ‘90%’ : ‘100%’,
alignSelf: ‘center’,
position: ‘absolute’,
bottom: Platform.OS === ‘ios’ ? 160 : 170,
},
controls: {
flexDirection: ‘row’,
justifyContent: ‘center’,
marginTop: 20,
},
controlButton: {
color: ‘#000’,
fontSize: 200,
},
playIcon: {
width: 70,
height: 70,
},
fastIcon: {
width: 60,
height: 60,
backgroundColor: ‘#fff’,
borderRadius: 50,
},
fastContainer: {
position: ‘absolute’,
bottom: 90,
right: 90,
alignSelf: ‘center’,
},
backContainer: {
position: ‘absolute’,
bottom: 90,
left: 90,
alignSelf: ‘center’,
},
reloadContainer: {
position: ‘absolute’,
bottom: 100,
left: 25,
alignSelf: ‘center’,
},
reloadIcon: {
width: 40,
height: 40,
backgroundColor: ‘#fff’,
borderRadius: 50,
},
suffelContainer: {
position: ‘absolute’,
bottom: 104,
right: 25,
alignSelf: ‘center’,
},
suffelIcon: {
width: 30,
height: 30,
},
closeIcon: {
width: 30,
height: 30,
},
iconContainer: {
alignItems: ‘flex-end’,
paddingTop: Platform.OS === ‘ios’ ? 60 : 20,
paddingHorizontal: 20,
borderWidth: 0,
},
controlsContainer: {
flex: 1,
top: 0,
left: 0,
right: 0,
bottom: 0,
position: ‘absolute’,
justifyContent: ‘flex-end’,
},
wrapper: {
position: ‘absolute’,
flexDirection: ‘row’,
bottom: 200,
alignSelf: ‘flex-end’,
right: 20,
},
titleContainer: {
position: ‘absolute’,
bottom: 200,
marginHorizontal: 20,
},
songName: {
color: ‘#fff’,
fontSize: 18,
fontWeight: ‘bold’,
marginBottom: 5,
},
artistName: {color: ‘#888’, fontSize: 14},
});

export default AudioPlayer;

const PlayPauseComponent = ({togglePlay, isPaused}) => {
return (
<TouchableOpacity
onPress={togglePlay}
style={{
position: ‘absolute’,
bottom: 85,
alignSelf: ‘center’,
borderRadius: 50,
}}>
{isPaused ? (
<Image
style={styles.playIcon}
source={require(‘../../icons/playWhite.webp’)}
/>
) : (
<Image
style={styles.playIcon}
source={require(‘../../icons/pauseWhite.jpeg’)}
/>
)}
</TouchableOpacity>
);
};

const FastForwardComponent = () => {
return (
<TouchableOpacity style={styles.fastContainer}>
<Image style={styles.fastIcon} source={require(‘../../icons/fast.png’)} />
</TouchableOpacity>
);
};

const BackWardComponent = () => {
return (
<TouchableOpacity style={styles.backContainer}>
<Image style={styles.fastIcon} source={require(‘../../icons/back.png’)} />
</TouchableOpacity>
);
};

const RepeatComponent = () => {
return (
<TouchableOpacity style={styles.reloadContainer}>
<Image
style={styles.reloadIcon}
source={require(‘../../icons/rewindAlt.png’)}
/>
</TouchableOpacity>
);
};

const SuffelComponent = () => {
return (
<TouchableOpacity style={styles.suffelContainer}>
<Image
style={styles.suffelIcon}
source={require(‘../../icons/new.png’)}
/>
</TouchableOpacity>
);
};

export const showToast = (type, text1, text2) => {
Toast.show({
type: type,
text1: text1,
text2: text2,

position: ‘top’,
topOffset: 50,
visibilityTime: 4000,
autoHide: true,
});
};

Offline Content Management with RNFetchBlob in React Native 📱🔒

1). Save Downloaded Content To User Device💾💾

Overview

The sendDownloadedDataToLocalDir function is a crucial part of implementing offline content management in a React Native application using RNFetchBlob. This function handles the download process for audio and video content, saves downloaded content to local storage, and emits events to track download progress.

Parameters:

callback: A callback function to be executed after the download completes.

contentId: The ID of the downloaded content.

src: The URL of the content to be downloaded.

artistName: The artist name associated with the content.

songName: The name of the song or content.

posterImage: The URL of the poster image associated with the content.

isAudio: A boolean indicating whether the content is audio or video.

These parameters can be customized according to your needs.

Usage Example:

sendDownloadedDataToLocalDir(callback, contentId, src, artistName, songName, posterImage, isAudio);

Code:

export const sendDownloadedDataToLocalDir = async (
callback = () => {},

contentId,
src,
artistName,
songName,
posterImage,
isAudio,
) => {
const {dirs} = RNFetchBlob.fs;
const dirToSave = Platform.OS === ‘ios’ ? dirs.DocumentDir : dirs.CacheDir;
const path = RNFetchBlob.fs.dirs.CacheDir + `/.file.json`;

var offlineMusicPlayerUrl = ”;
var imageUrl = ”;
var roundOffValue = 0;
let getNewTime = new Date().getTime();

const commonConfig = {
fileCache: true,
useDownloadManager: true,
notification: true,
title: songName,
path: isAudio
? `${dirToSave}/${getNewTime}.mp3`
: `${dirToSave}/${getNewTime}.mp4`,
mediaScannable: true,
description: ‘file download’,
};

const configOptions = Platform.select({
ios: {
fileCache: commonConfig.fileCache,
title: commonConfig.title,
path: commonConfig.path,
appendExt: isAudio ? ‘mp3’ : ‘mp4’,
},
android: commonConfig,
});

const startDownloadingTheRestContent = async cb => {
// for Images
try {
let res = await RNFetchBlob.config({
fileCache: true,
path: `${dirToSave}/${contentId}.webp`,
IOSBackgroundTask: true,
}).fetch(‘GET’, posterImage, {});
if (res) {
imageUrl = res.path();
}
} catch (e) {}

var offlineObjData = {
contentId: contentId,
source: offlineMusicPlayerUrl,
artistName: artistName,
songName: songName,
downloadDate: new Date(),
posterImage: imageUrl,
isAudio: isAudio,
};

let offlinDonwloadList = [];
//fetching local downloads from storage
try {
let localDownloads = await RNFetchBlob.fs.readFile(path, ‘utf8’);
localDownloads = JSON.parse(localDownloads);
if (Array.isArray(localDownloads)) {
offlinDonwloadList = localDownloads;
}
} catch (e) {}

//adding new downloads
offlinDonwloadList.push(offlineObjData);
await RNFetchBlob.fs
.writeFile(path, JSON.stringify(offlinDonwloadList), ‘utf8’)
.then(r => {
cb && cb();
})
.catch(e => {});
};

// for video
if (src) {
RNFetchBlob.config(configOptions)
.fetch(‘get’, src, {})
.progress((received, total) => {
const percentageValue = (received / total) * 100;
roundOffValue = Math.round(percentageValue);

var params = {
contentId: contentId,
source: src,
artistName: artistName,
songName: songName,
progressValue: JSON.stringify(roundOffValue),
};
DeviceEventEmitter.emit(‘downloadProgress’, params);
DeviceEventEmitter.emit(‘downloadProgress’, params);
})
.then(async res => {
let downloadContents = {};
if (Platform.OS === ‘ios’) {
await RNFetchBlob.fs.writeFile(commonConfig.path, res.data, ‘base64’);
offlineMusicPlayerUrl = commonConfig.path;
await startDownloadingTheRestContent(() => {
var params = {
contentId: contentId,
source: src,
artistName: artistName,
songName: songName,
progressValue: JSON.stringify(roundOffValue),
};

DeviceEventEmitter.emit(‘downloadDone’, params);
DeviceEventEmitter.emit(‘downloadProgress’, params);
});
} else {
// for Android
offlineMusicPlayerUrl = res.path();
startDownloadingTheRestContent(() => {
var params = {
contentId: contentId,
source: src,
artistName: artistName,
songName: songName,
progressValue: JSON.stringify(roundOffValue),
};
DeviceEventEmitter.emit(‘downloadDone’, params);
DeviceEventEmitter.emit(‘downloadProgress’, params);
});
}
})

.catch(err => {
callback(‘error’);
DeviceEventEmitter.emit(‘downloadError’, true);
});
}
};

2). Fetch Downloaded Content To User Device🔄

Overview

The fetchDownloadedDataFromLocalDir function is responsible for retrieving downloaded data from local storage in a React Native application. It reads a JSON file containing metadata of downloaded content, parses the data, and sends it to a callback function for further processing.

Parameters:

sendData: A callback function to handle the fetched downloaded data.

Usage Example:

fetchDownloadedDataFromLocalDir(sendData);

Code:

export const fetchDownloadedDataFromLocalDir = async (sendData = () => {}) => {
const trackFolder =
Platform.OS === ‘ios’
? RNFetchBlob.fs.dirs.DocumentDir
: RNFetchBlob.fs.dirs.CacheDir;
const MyPath = RNFetchBlob.fs.dirs.CacheDir + `/.file.json`;
await RNFetchBlob.fs
.ls(trackFolder)
.then(files => {})
.catch(err => {});
try {
let localDownloads = await RNFetchBlob.fs.readFile(MyPath, ‘utf8’);
localDownloads = JSON.parse(localDownloads);
if (Array.isArray(localDownloads)) {
sendData(localDownloads);
}
} catch (e) {}
};

3). Delete Downloaded Content To User Device🗑️

Overview

The deleteContentFromLocalDir function is responsible for deleting specific downloaded content from local storage in a React Native application. It reads a JSON file containing metadata of downloaded content, finds and removes the specified content based on its ID, and then updates the JSON file with the modified data.

Parameters:
downloadedId: The ID of the downloaded item to be deleted.

Usage Example:

deleteContentFromLocalDir(downloadedId);

Code:

export const deleteContentFromLocalDir = async downloadedId => {
let jsonObj = [];
const MyPath = RNFetchBlob.fs.dirs.CacheDir + `/.file.json`;
try {
let localDownloads = await RNFetchBlob.fs.readFile(MyPath, ‘utf8’);
localDownloads = JSON.parse(localDownloads);
if (Array.isArray(localDownloads)) {
jsonObj = localDownloads;
}
} catch (e) {}

let flag = ”;
const contentIdToFind = downloadedId;
jsonObj.map((item, index) => {
if (item.id === contentIdToFind) {
flag = index;
}
});
jsonObj.splice(flag, 1);
await RNFetchBlob.fs
.writeFile(MyPath, JSON.stringify(jsonObj), ‘utf8’)
.then(r => {})
.catch(e => {});
};

4). Delete All Downloaded Content To User Device🧹🧹

Overview

The deleteAllDownloadDataFromLocal function is responsible for clearing all downloaded data from the local storage in a React Native application. It initializes an empty JSON object, converts it to a string, and writes it back to the JSON file, effectively removing all downloaded content records.

Usage Example:
deleteAllDownloadDataFromLocal();

Code:

export const deleteAllDownloadDataFromLocal = async () => {
let jsonObj = [];
const MyPath = RNFetchBlob.fs.dirs.CacheDir + `/.file.json`;
await RNFetchBlob.fs
.writeFile(MyPath, JSON.stringify(jsonObj), ‘utf8’)
.then(r => {})
.catch(e => {});
};

Note:📝📝

Only for iOS: When downloading audio or video content to the user’s device, iOS changes the directory each time for security reasons. To handle this, the downloaded path is dynamically appended when fetching the downloaded content from the user’s device, as iOS generates a new path each time.

For displaying downloaded images, add “file://” before the path in Android, as images may not display correctly in Android without this prefix.

Conclusion🎉🎉

In this documentation, we’ve explored the essential functions for managing downloaded data from local directories in React Native applications. These functions enable crucial operations like fetching, deleting specific content, and clearing all downloaded data from local storage. They address platform-specific considerations, such as dynamic path handling in iOS and path formatting in Android, ensuring seamless management of offline content. By leveraging these functions, developers can enhance user experience and efficiently handle downloaded data within their React Native apps. Experimenting with these functions will help tailor them to specific application requirements. Happy coding!📂📱

After reading the post consider the following:🤔📝

Subscribe to receive newsletters with the latest blog posts
Download the source code for this post from my github

Leave a Reply

Your email address will not be published. Required fields are marked *