Lewati ke konten utama

Dukungan React Native

Dropup bekerja di React Native dengan adapter native.

Instalasi

npm install @samithahansaka/dropup

# Untuk image picking
npx expo install expo-image-picker

# Untuk document picking
npx expo install expo-document-picker

# Untuk manipulasi gambar (opsional)
npx expo install expo-image-manipulator

Setup Dasar

import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import { View, Text, Button, Image, FlatList } from 'react-native';

function NativeUploader() {
const { files, actions, state } = useDropup({
// Gunakan adapter native
adapter: NativeAdapter,

upload: {
url: 'https://your-api.com/upload',
},
});

const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
quality: 0.8,
});

if (!result.canceled) {
// Konversi ke objek mirip File
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'photo.jpg',
type: asset.mimeType || 'image/jpeg',
}));

actions.addFiles(files);
}
};

return (
<View style={styles.container}>
<Button title="Pilih Gambar" onPress={pickImage} />

<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.preview} />
)}
<Text>{item.name}</Text>
<Text>{item.status}</Text>
{item.status === 'uploading' && (
<Text>{item.progress}%</Text>
)}
</View>
)}
/>

<Button
title="Upload Semua"
onPress={() => actions.upload()}
disabled={state.isUploading}
/>
</View>
);
}

const styles = {
container: {
flex: 1,
padding: 20,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
preview: {
width: 50,
height: 50,
marginRight: 10,
borderRadius: 4,
},
};

Integrasi Image Picker

Pilih dari Galeri

import * as ImagePicker from 'expo-image-picker';

const pickFromGallery = async () => {
// Minta izin
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Izin diperlukan');
return;
}

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
quality: 0.8,
selectionLimit: 10,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
size: asset.fileSize,
}));

actions.addFiles(files);
}
};

Ambil Foto

const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('Izin kamera diperlukan');
return;
}

const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
allowsEditing: true,
aspect: [4, 3],
});

if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
}]);
}
};

Document Picker

import * as DocumentPicker from 'expo-document-picker';

const pickDocument = async () => {
const result = await DocumentPicker.getDocumentAsync({
type: ['application/pdf', 'application/msword'],
multiple: true,
});

if (result.type === 'success') {
actions.addFiles([{
uri: result.uri,
name: result.name,
type: result.mimeType,
size: result.size,
}]);
}
};

Kompresi Gambar (Native)

import * as ImageManipulator from 'expo-image-manipulator';

async function compressNativeImage(uri: string): Promise<string> {
const result = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1200 } }],
{ compress: 0.7, format: ImageManipulator.SaveFormat.JPEG }
);

return result.uri;
}

// Gunakan dengan Dropup
const { actions } = useDropup({
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
if (file.type.startsWith('image/')) {
const compressedUri = await compressNativeImage(file.uri);
actions.updateFileMeta(file.id, { compressedUri });
}
}
},
});

Contoh Native Lengkap

import React from 'react';
import {
View,
Text,
TouchableOpacity,
Image,
FlatList,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import * as DocumentPicker from 'expo-document-picker';

export default function FileUploader() {
const { files, actions, state } = useDropup({
adapter: NativeAdapter,
maxFiles: 10,
upload: {
url: 'https://your-api.com/upload',
headers: {
'Authorization': 'Bearer token',
},
},
onUploadComplete: (file) => {
console.log('Terupload:', file.uploadedUrl);
},
onUploadError: (file, error) => {
console.error('Gagal:', error.message);
},
});

const pickImages = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') return;

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'image.jpg',
type: asset.mimeType || 'image/jpeg',
}));
actions.addFiles(files);
}
};

const pickDocuments = async () => {
const result = await DocumentPicker.getDocumentAsync({
multiple: true,
});

if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.name,
type: asset.mimeType,
size: asset.size,
}));
actions.addFiles(files);
}
};

const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') return;

const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
});

if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: 'image/jpeg',
}]);
}
};

const renderFile = ({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.thumbnail} />
)}
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.fileStatus}>
{item.status === 'uploading'
? `Mengupload ${item.progress}%`
: item.status}
</Text>
</View>
<TouchableOpacity
onPress={() => actions.remove(item.id)}
style={styles.removeBtn}
>
<Text style={styles.removeText}>×</Text>
</TouchableOpacity>
</View>
);

return (
<View style={styles.container}>
{/* Tombol Aksi */}
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={pickImages}>
<Text style={styles.buttonText}>Galeri</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={takePhoto}>
<Text style={styles.buttonText}>Kamera</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={pickDocuments}>
<Text style={styles.buttonText}>File</Text>
</TouchableOpacity>
</View>

{/* Daftar File */}
<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={renderFile}
style={styles.list}
ListEmptyComponent={
<Text style={styles.emptyText}>Tidak ada file dipilih</Text>
}
/>

{/* Tombol Upload */}
<TouchableOpacity
style={[styles.uploadBtn, state.isUploading && styles.uploadBtnDisabled]}
onPress={() => actions.upload()}
disabled={state.isUploading || files.length === 0}
>
{state.isUploading ? (
<View style={styles.uploadingRow}>
<ActivityIndicator color="white" />
<Text style={styles.uploadBtnText}>
Mengupload {state.progress}%
</Text>
</View>
) : (
<Text style={styles.uploadBtnText}>
Upload {files.length} file
</Text>
)}
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 16,
},
button: {
flex: 1,
backgroundColor: '#f0f0f0',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
fontWeight: '600',
},
list: {
flex: 1,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
thumbnail: {
width: 50,
height: 50,
borderRadius: 4,
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontWeight: '500',
},
fileStatus: {
color: '#666',
fontSize: 12,
marginTop: 2,
},
removeBtn: {
padding: 8,
},
removeText: {
fontSize: 24,
color: '#999',
},
emptyText: {
textAlign: 'center',
color: '#999',
marginTop: 40,
},
uploadBtn: {
backgroundColor: '#2196f3',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 16,
},
uploadBtnDisabled: {
backgroundColor: '#ccc',
},
uploadBtnText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
uploadingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
});

Perbedaan Platform

FiturWebReact Native
Seret & lepasYaTidak
File pickerNativeexpo-image-picker
URL PreviewObject URLURI File
Akses fileObjek FileBerbasis URI

Expo vs Bare React Native

Expo (Managed)

Semua contoh di atas berfungsi dengan workflow managed Expo.

Bare React Native

Untuk bare React Native, install modul native:

# Sebagai gantinya paket expo
npm install react-native-image-picker
npm install react-native-document-picker

# Link jika diperlukan (versi RN lama)
npx react-native link

Lalu gunakan API yang sesuai dari paket-paket tersebut.