- Tổng quan kiến trúc
- 1. Cài đặt thư viện cần thiết
- 2. Cấu hình quyền truy cập vị trí
- 3. Xin quyền và lấy vị trí người dùng
- 4. Tính khoảng cách bằng công thức Haversine
- 5. Lọc và sắp xếp danh sách cửa hàng theo khoảng cách
- 6. Truy vấn geospatial ở backend (khuyến nghị cho dữ liệu lớn)
- 7. Hiển thị lên bản đồ với react-native-maps
- 8. Những điểm cần lưu ý khi triển khai thực tế
- Kết luận
Tính năng tìm cửa hàng/chi nhánh gần vị trí người dùng là một trong những tính năng phổ biến nhất trong các app bán lẻ, F&B, giao hàng. Bài viết này sẽ hướng dẫn bạn xây dựng tính năng này từ đầu, bao gồm: lấy vị trí người dùng, tính khoảng cách, gọi API backend, và hiển thị kết quả lên bản đồ.
Tổng quan kiến trúc
Luồng hoạt động cơ bản gồm 4 bước:
- Xin quyền truy cập vị trí (Location Permission)
- Lấy tọa độ GPS hiện tại của người dùng
- Gửi tọa độ lên backend (hoặc tính toán tại client) để tìm các cửa hàng trong bán kính X km
- Hiển thị danh sách/bản đồ các cửa hàng, sắp xếp theo khoảng cách gần nhất
1. Cài đặt thư viện cần thiết
Chúng ta sẽ dùng hai thư viện phổ biến:
npm install react-native-geolocation-service react-native-maps npx pod-install # nếu build iOS
react-native-geolocation-service: lấy tọa độ GPS chính xác hơn API mặc địnhreact-native-maps: hiển thị bản đồ và marker
2. Cấu hình quyền truy cập vị trí
Android
Trong android/app/src/main/AndroidManifest.xml, thêm:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
iOS
Trong ios/YourApp/Info.plist, thêm:
<key>NSLocationWhenInUseUsageDescription</key> <string>Ứng dụng cần quyền truy cập vị trí để tìm cửa hàng gần bạn</string>
3. Xin quyền và lấy vị trí người dùng
Tạo một hàm helper để xin quyền và lấy tọa độ:
import { PermissionsAndroid, Platform } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
export async function requestLocationPermission() {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Quyền truy cập vị trí',
message: 'Ứng dụng cần vị trí của bạn để tìm cửa hàng gần nhất',
buttonPositive: 'Đồng ý',
buttonNegative: 'Từ chối',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
// iOS sẽ tự hiện popup khi gọi Geolocation.requestAuthorization
const auth = await Geolocation.requestAuthorization('whenInUse');
return auth === 'granted';
}
export function getCurrentLocation() {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error) => reject(error),
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
});
}4. Tính khoảng cách bằng công thức Haversine
Nếu bạn chưa có backend hỗ trợ truy vấn địa lý (geospatial query), có thể tính khoảng cách ngay tại client bằng công thức Haversine:
function toRad(value) {
return (value * Math.PI) / 180;
}
export function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // bán kính Trái Đất (km)
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) *
Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // khoảng cách tính bằng km
}Cách này phù hợp khi số lượng cửa hàng không quá lớn (vài trăm điểm). Nếu có hàng nghìn cửa hàng, nên đẩy việc tính toán này về backend với chỉ mục địa lý (xem phần 6).
5. Lọc và sắp xếp danh sách cửa hàng theo khoảng cách
export function getNearbyStores(userLocation, stores, radiusKm = 5) {
return stores
.map((store) => ({
...store,
distance: calculateDistance(
userLocation.latitude,
userLocation.longitude,
store.latitude,
store.longitude
),
}))
.filter((store) => store.distance <= radiusKm)
.sort((a, b) => a.distance - b.distance);
}6. Truy vấn geospatial ở backend (khuyến nghị cho dữ liệu lớn)
Nếu danh sách cửa hàng lớn, bạn nên để backend xử lý lọc bán kính thay vì tải toàn bộ dữ liệu về client. Một số cách phổ biến:
- MongoDB: dùng chỉ mục
2dspherevà toán tử$geoNearhoặc$nearSphere - PostgreSQL + PostGIS: dùng hàm
ST_DWithinđể lọc theo bán kính - Elasticsearch: dùng
geo_distancequery
Ví dụ gọi API backend từ React Native:
export async function fetchNearbyStores(latitude, longitude, radiusKm = 5) {
const response = await fetch(
`https://api.yourapp.com/stores/nearby?lat=${latitude}&lng=${longitude}&radius=${radiusKm}`
);
if (!response.ok) {
throw new Error('Không thể tải danh sách cửa hàng');
}
return response.json();
}Cách này giúp giảm tải cho thiết bị người dùng và tận dụng chỉ mục địa lý để truy vấn nhanh hơn nhiều so với việc tính Haversine cho từng bản ghi.
7. Hiển thị lên bản đồ với react-native-maps
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, ActivityIndicator, Text } from 'react-native';
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
import { requestLocationPermission, getCurrentLocation } from './location';
import { fetchNearbyStores } from './api';
export default function NearbyStoresScreen() {
const [location, setLocation] = useState(null);
const [stores, setStores] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function init() {
try {
const granted = await requestLocationPermission();
if (!granted) {
setError('Bạn cần cấp quyền vị trí để sử dụng tính năng này');
return;
}
const loc = await getCurrentLocation();
setLocation(loc);
const nearby = await fetchNearbyStores(loc.latitude, loc.longitude, 5);
setStores(nearby);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
init();
}, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
</View>
);
}
if (error) {
return (
<View style={styles.center}>
<Text>{error}</Text>
</View>
);
}
return (
<MapView
provider={PROVIDER_GOOGLE}
style={styles.map}
initialRegion={{
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
showsUserLocation
>
{stores.map((store) => (
<Marker
key={store.id}
coordinate={{
latitude: store.latitude,
longitude: store.longitude,
}}
title={store.name}
description={`${store.distance.toFixed(1)} km`}
/>
))}
</MapView>
);
}
const styles = StyleSheet.create({
map: { flex: 1 },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
});8. Những điểm cần lưu ý khi triển khai thực tế
- Xử lý khi người dùng từ chối quyền vị trí: nên có màn hình hướng dẫn người dùng bật lại quyền trong Settings, thay vì để app "đứng hình".
- Cache vị trí: dùng
maximumAgehợp lý để tránh gọi GPS liên tục, tiết kiệm pin. - Debounce khi người dùng di chuyển bản đồ: nếu cho phép tìm kiếm theo vùng bản đồ hiện tại (kiểu "Tìm kiếm khu vực này"), nên debounce việc gọi API để tránh gửi quá nhiều request.
- Đơn vị bán kính: thống nhất km hoặc dặm giữa client và backend để tránh sai lệch kết quả.
- Xử lý khi không có kết quả: hiển thị thông báo rõ ràng và gợi ý tăng bán kính tìm kiếm thay vì màn hình trắng.
- Testing trên thiết bị thật: GPS giả lập trên simulator/emulator thường không chính xác, nên test kỹ trên thiết bị thật trước khi release.
Kết luận
Việc triển khai tính năng tìm cửa hàng gần đây trong React Native không quá phức tạp về mặt kỹ thuật, nhưng cần chú ý đến trải nghiệm người dùng ở các bước xin quyền, xử lý lỗi, và hiệu năng khi dữ liệu lớn. Với dữ liệu nhỏ, tính Haversine tại client là đủ dùng; nhưng khi mở rộng, hãy chuyển việc lọc theo bán kính sang backend với chỉ mục địa lý để đảm bảo tốc độ và khả năng mở rộng.








