![]()
Ở bài trước, chúng ta đã học cách "gõ cửa" hệ điều hành để xin quyền truy cập. Hôm nay, khi cánh cửa đã mở, chúng ta sẽ tiến thẳng vào một trong những phần cứng thú vị nhất của chiếc điện thoại: Camera và Gallery.
Thử nghĩ xem, có mạng xã hội nào tồn tại mà không cho phép người dùng thay đổi Avatar? Có ứng dụng bán hàng nào không cần người dùng chụp ảnh sản phẩm để đánh giá? Chụp và chọn ảnh là tính năng "bắt buộc phải biết" của mọi lập trình viên Mobile.
Trong bài viết này, chúng ta sẽ sử dụng 2 "vũ khí" chính chủ từ hệ sinh thái Expo: expo-image-picker (để chọn ảnh có sẵn) và expo-camera (để tự tay xây dựng một máy ảnh thu nhỏ).
1. Chọn ảnh từ Thư viện với expo-image-picker
Khởi động với tính năng đơn giản và được sử dụng nhiều nhất: Chọn một bức ảnh đã có sẵn trong máy.

Bước 1: Cài đặt expo-image-picker
Mở Terminal và chạy lệnh sau:
npx expo install expo-image-picker
Bước 2: Viết code gọi Thư viện ảnh
Thư viện này cung cấp hàm launchImageLibraryAsync giúp mở thẳng ứng dụng Thư viện ảnh (Photos) của điện thoại. Bạn có thể cho phép cắt cúp ảnh (crop) ngay sau khi chọn!
import React, { useState } from 'react'
import { Button, Image, View, StyleSheet, Text, TouchableOpacity } from 'react-native'
import * as ImagePicker from 'expo-image-picker' // 1. Import thư viện
export default function ImagePickerScreen() {
const [imageUri, setImageUri] = useState(null)
const pickImage = async () => {
// 2. Mở thư viện ảnh (Hàm này tự động xin quyền đọc ảnh trên hầu hết thiết bị)
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Chỉ chọn ảnh, không chọn video
allowsEditing: true, // Cho phép người dùng crop ảnh
aspect: [1, 1], // Ép khung crop hình vuông (tỷ lệ 1:1, phù hợp làm Avatar)
quality: 0.8, // Giảm chất lượng xuống 80% để nhẹ máy, tải lên mạng nhanh hơn
})
// 3. Nếu người dùng không bấm "Hủy" (Cancel)
if (!result.canceled) {
// result.assets[0].uri chứa đường dẫn tới bức ảnh vừa chọn
setImageUri(result.assets[0].uri)
}
}
return (
<View style={styles.container}>
<Text style={styles.title}>Cập Nhật Ảnh Đại Diện</Text>
{/* Hiển thị ảnh nếu đã chọn, nếu chưa thì hiển thị khung màu xám */}
{imageUri ? <Image source={{ uri: imageUri }} style={styles.avatar} /> : <View style={styles.placeholder} />}
<TouchableOpacity style={styles.button} onPress={pickImage}>
<Text style={styles.buttonText}>📁 Chọn Ảnh Từ Máy</Text>
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20 },
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
avatar: { width: 150, height: 150, borderRadius: 75, marginBottom: 20 },
placeholder: { width: 150, height: 150, borderRadius: 75, backgroundColor: '#dfe6e9', marginBottom: 20 },
button: { backgroundColor: '#0984e3', padding: 12, borderRadius: 8 },
buttonText: { color: 'white', fontWeight: 'bold', fontSize: 16 },
})
2. Nâng cao: Tự xây dựng Máy ảnh với expo-camera
Nếu image-picker cũng có tính năng mở camera mặc định của máy, thì tại sao chúng ta lại cần expo-camera?
Lý do là: image-picker văng ra khỏi ứng dụng của bạn để mở app Camera gốc. Còn với expo-camera, bạn có thể nhúng trực tiếp khung ngắm camera vào trong giao diện app của bạn. Bạn có thể đặt các bộ lọc, nút bấm tùy chỉnh đè lên trên khung ngắm đó (giống hệt cách TikTok hay Instagram làm).
(Lưu ý: Code dưới đây sử dụng component <CameraView> hiện đại nhất của Expo SDK 50/51, thay thế cho thẻ <Camera> cũ đã lỗi thời).

Bước 1: Cài đặt expo-camera
npx expo install expo-camera
Bước 2: Xây dựng màn hình Camera
Với Camera, BẮT BUỘC phải xin quyền rành mạch trước khi hiển thị khung ngắm. Chúng ta cũng cần Hook useRef để ra lệnh cho máy ảnh chụp hình.
import React, { useState, useRef } from 'react'
import { StyleSheet, Text, TouchableOpacity, View, Image } from 'react-native'
import { CameraView, useCameraPermissions } from 'expo-camera'
export default function CustomCameraScreen() {
// 1. Quản lý quyền
const [permission, requestPermission] = useCameraPermissions()
// 2. State quản lý hướng camera (trước/sau) và ảnh đã chụp
const [facing, setFacing] = useState('back')
const [photo, setPhoto] = useState(null)
// 3. Tham chiếu (Ref) để điều khiển CameraView
const cameraRef = useRef(null)
// Màn hình chờ nếu chưa kiểm tra xong quyền
if (!permission) return <View />
// Nếu chưa cấp quyền, hiển thị nút xin quyền
if (!permission.granted) {
return (
<View style={styles.container}>
<Text style={{ textAlign: 'center' }}>Cần cấp quyền để sử dụng Camera</Text>
<Button onPress={requestPermission} title="Cấp quyền ngay" />
</View>
)
}
// Hàm lật camera trước/sau
const toggleCameraFacing = () => {
setFacing((current) => (current === 'back' ? 'front' : 'back'))
}
// Hàm chụp ảnh
const takePic = async () => {
// Nếu ref đã được gắn vào CameraView
if (cameraRef.current) {
const options = { quality: 0.8, base64: true } // Cấu hình chất lượng
const newPhoto = await cameraRef.current.takePictureAsync(options)
setPhoto(newPhoto.uri) // Lưu đường dẫn ảnh vừa chụp vào state
}
}
// Nếu ĐÃ CHỤP ẢNH xong, hiển thị ảnh chụp để xem trước (Preview)
if (photo) {
return (
<View style={styles.container}>
<Image source={{ uri: photo }} style={{ flex: 1 }} />
<TouchableOpacity style={styles.retakeButton} onPress={() => setPhoto(null)}>
<Text style={styles.text}>Chụp lại</Text>
</TouchableOpacity>
</View>
)
}
// Giao diện LÚC ĐANG NGẮM CHỤP
return (
<View style={styles.container}>
<CameraView style={styles.camera} facing={facing} ref={cameraRef}>
<View style={styles.buttonContainer}>
{/* Nút lật Camera */}
<TouchableOpacity style={styles.flipButton} onPress={toggleCameraFacing}>
<Text style={styles.text}>Lật Cam</Text>
</TouchableOpacity>
{/* Nút Bấm Chụp */}
<TouchableOpacity style={styles.captureButton} onPress={takePic}>
<View style={styles.innerCapture} />
</TouchableOpacity>
</View>
</CameraView>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', backgroundColor: '#000' },
camera: { flex: 1 },
buttonContainer: {
flex: 1,
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'space-around',
alignItems: 'flex-end',
paddingBottom: 40,
},
flipButton: { alignSelf: 'flex-end', padding: 15, backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 10 },
text: { fontSize: 18, fontWeight: 'bold', color: 'white' },
captureButton: {
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: 'rgba(0,0,0,0.2)',
},
innerCapture: { width: 54, height: 54, borderRadius: 27, backgroundColor: 'white' },
retakeButton: {
position: 'absolute',
bottom: 40,
alignSelf: 'center',
backgroundColor: '#d63031',
padding: 15,
borderRadius: 10,
},
})
3. Kiến thức thực chiến: Upload ảnh lên Server thế nào?
Cả hai cách trên đều trả về cho bạn một biến uri (Ví dụ: file:///data/user/0/.../image.jpg).
Nhưng đây chỉ là đường dẫn nằm trong ổ cứng của điện thoại. Nếu bạn mang biến uri này lưu vào cơ sở dữ liệu trên Server, những người dùng khác sẽ không thể xem được ảnh của bạn!
Để đưa bức ảnh này lên mạng (Upload), bạn không thể dùng JSON.stringify() như gửi Text thông thường. Bức ảnh là một file nhị phân. Bạn bắt buộc phải dùng công nghệ FormData.
Đây là đoạn code "vàng" giúp bạn biến file từ điện thoại thành dữ liệu gửi đi được qua API:
const uploadImageToServer = async (localUri) => {
// 1. Lấy tên file và đuôi mở rộng từ đường dẫn
const filename = localUri.split('/').pop()
const match = /\.(\w+)$/.exec(filename)
const type = match ? `image/${match[1]}` : `image`
// 2. Tạo một túi hồ sơ FormData
const formData = new FormData()
// 3. Bỏ bức ảnh vào túi (Lưu ý phải truyền đủ uri, name, type)
formData.append('photo', { uri: localUri, name: filename, type })
// Bạn có thể bỏ thêm dữ liệu khác (như ID người dùng)
formData.append('userId', '12345')
// 4. Gửi bằng Fetch API
try {
const response = await fetch('https://api.domain-cua-ban.com/upload', {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data', // Bắt buộc khi dùng FormData
},
})
const result = await response.json()
console.log('Upload thành công! Link ảnh trên mạng là:', result.imageUrl)
} catch (error) {
console.error('Lỗi upload:', error)
}
}
Kết luận: Bạn đã nắm trong tay đầy quyền năng
Bạn đã nắm trong tay cách điều khiển thị giác của thiết bị di động:
- Dùng
expo-image-pickercho 90% nhu cầu thông thường: Chọn Avatar, gửi ảnh có sẵn, mở camera cơ bản. - Dùng
expo-camerakhi bạn muốn thiết kế các app chuyên về nhiếp ảnh, quét mã QR, hoặc làm khung ngắm tùy chỉnh như Instagram Story. - Luôn nhớ dùng
FormDatađể đẩy file ảnh thực sự lên Server.
Nếu Camera là con mắt, thì ở bài sau, chúng ta sẽ cho ứng dụng một khả năng định hướng tuyệt vời. Hãy sẵn sàng để bước đi tiếp nào!