rn-apk-patcher
v2.1.0
Published
The simplest APK delta-update library for React Native. One function call — that's it.
Maintainers
Readme
rn-apk-patcher
The simplest way to deliver APK updates in React Native.
Instead of making your users download a full APK every time, you ship a tiny patch file (just the bytes that changed). The library downloads it, builds the new APK, and opens the installer — all from a single function call.
old.apk + patch.bsdiff → new.apk → 📲 InstallAndroid only.
Install
npm install rn-apk-patcher react-native-fsThat's the only install step. The library uses React Native's auto-linking, so no manual Gradle or Java changes are needed.
One manual step: Copy
bspatch.c,bspatch.h, and thebzip2/folder from github.com/mendsley/bsdiff intonode_modules/rn-apk-patcher/android/src/main/jniLibs/. This is a one-time thing and is needed because the bspatch source can't be bundled directly due to its license.
Then add two things to your AndroidManifest.xml:
<!-- Inside <manifest> -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Inside <application> -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.rnapkpatcher.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/rnapkpatcher_paths" />
</provider>Create android/app/src/main/res/xml/rnapkpatcher_paths.xml:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="internal" path="." />
<cache-path name="cache" path="." />
</paths>That's the entire setup.
Usage
import { updateApp } from 'rn-apk-patcher';
// Somewhere in your app, when an update is available:
const result = await updateApp('https://your-server.com/update.bsdiff');
if (result.success) {
// The system install dialog is now open
}With a progress bar
import { updateApp } from 'rn-apk-patcher';
const result = await updateApp(
'https://your-server.com/update.bsdiff',
(progress) => {
console.log(`${progress.percent}% — ${progress.message}`);
// progress.stage: 'downloading' | 'patching' | 'installing' | 'done' | 'error'
}
);With checksum verification (recommended)
const result = await updateApp(
'https://your-server.com/update.bsdiff',
(p) => setPercent(p.percent),
'a1b2c3d4...' // SHA-256 of the new APK, from your server
);Full example component
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { updateApp, type UpdateProgress } from 'rn-apk-patcher';
export default function UpdateButton() {
const [progress, setProgress] = useState<UpdateProgress | null>(null);
const [loading, setLoading] = useState(false);
const handleUpdate = async () => {
setLoading(true);
const result = await updateApp(
'https://your-server.com/update.bsdiff',
setProgress,
'optional-sha256-checksum'
);
setLoading(false);
if (!result.success) {
console.error('Update failed:', result.error);
}
};
return (
<View style={styles.container}>
{progress && (
<View style={styles.progressBar}>
<View style={[styles.fill, { width: `${progress.percent}%` }]} />
</View>
)}
<Text style={styles.status}>
{progress ? `${progress.percent}% — ${progress.message}` : 'Update available'}
</Text>
<TouchableOpacity
style={styles.button}
onPress={handleUpdate}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Updating...' : 'Install Update'}
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20 },
progressBar: { height: 6, backgroundColor: '#eee', borderRadius: 3, marginBottom: 10 },
fill: { height: 6, backgroundColor: '#007AFF', borderRadius: 3 },
status: { color: '#555', marginBottom: 16 },
button: { backgroundColor: '#007AFF', padding: 14, borderRadius: 10, alignItems: 'center' },
buttonText: { color: '#fff', fontWeight: '700' },
});API
updateApp(patchUrl, onProgress?, checksum?)
The only function you need to call.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| patchUrl | string | ✅ | URL to your .bsdiff patch file |
| onProgress | (p: UpdateProgress) => void | ❌ | Called as the update progresses |
| checksum | string | ❌ | SHA-256 of the new APK to verify the download |
Returns a Promise<UpdateResult>:
{
success: boolean,
error?: string, // present only on failure
durationMs?: number // how long it took in milliseconds
}UpdateProgress
{
stage: 'downloading' | 'patching' | 'installing' | 'done' | 'error',
percent: number, // 0–100
message: string, // e.g. "Downloading update..."
bytesDownloaded?: number,
totalBytes?: number,
}resetBase()
Forces the library to re-save the current APK as the baseline. You shouldn't need this in normal use.
import { resetBase } from 'rn-apk-patcher';
await resetBase();clearCache()
Deletes all cached APK files to free up disk space.
import { clearCache } from 'rn-apk-patcher';
await clearCache();Server side: generating patch files
On your build server, install bsdiff and run:
# Install
sudo apt-get install bsdiff # Linux
brew install bsdiff # macOS
# Generate the patch
bsdiff old.apk new.apk update.bsdiff
# Generate the checksum (to pass as the 3rd argument on the client)
sha256sum new.apkA typical update API response might look like:
{
"hasUpdate": true,
"patchUrl": "https://cdn.yourapp.com/patches/v2.bsdiff",
"checksum": "a1b2c3d4e5f6...",
"version": "2.0.0"
}How it works
- On the first call, the library silently saves your currently installed APK as a baseline (
base.apk). - When you call
updateApp(), it downloads the patch file from your URL. - It runs
bspatchnatively (C library, compiled with NDK) to reconstruct the full new APK frombase.apk + patch. - It opens the Android system installer with the new APK.
- The new APK automatically becomes the baseline for the next update.
The patch file is typically 5–15× smaller than a full APK download, which means faster updates and less data usage for your users.
FAQ
Do I need to call anything on app launch?
No. The first time updateApp() runs, it automatically saves the current APK as the baseline. Nothing to set up.
What if the user cancels the install dialog?
The new APK stays on disk. You can call updateApp() again and it will go straight to the install step — no re-download needed.
What Android versions are supported?
Android 5.0 (API 21) and above. FileProvider is handled automatically on Android 7+.
Does it work on iOS?
No. APK files are Android-only. For iOS, use TestFlight or the App Store.
License
MIT
