Всем привет! 👋 Если вы когда-нибудь пытались создать функцию для здоровья, фитнеса или другой утилиты в кроссплатформенном приложении на React Native, вы знаете эту боль: вы пишете не один набор функций, а боретесь с двумя совершенно разными тяжёлыми нативными экосистемами.
1. Начало
На iOS есть зрелый API Apple HealthKit. На Android — Health Connect, единый API-фреймворк Google, заменивший устаревший Google Fit SDK. Когда я проектировал архитектуру отслеживания воды для нового приложения WaterPaw, мне нужно было полностью модульное решение.
Я не хотел, чтобы условные проверки платформы (Platform.OS === 'ios') засоряли слой интерфейса. Мне нужен был один абстрактный пользовательский React-хук, который отвечает за доступность, разрешения и получение данных. Вот чистый шаблон проектирования, с помощью которого я их объединил; нативные настройки при этом автоматически применяются через Expo Config Plugins.
Сначала я определил конкретный контракт (интерфейс), чтобы фронтенду было всё равно, на какой нативной платформе он работает:
export type HydrationData = {
value: number; // in milliliters
date: string;
};
export interface HealthBridge {
isAvailable(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
getHydrationData(date: Date): Promise<HydrationData[]>;
}2. Конкретные мосты
Затем я вынес конкретные детали реализации в изолированные нативные обёртки.
- Обёртка для iOS: Работает на
react-native-healthдля проверки статуса инициализации и передачи стандартных матриц разрешений HealthKit. - Обёртка для Android: Работает на
react-native-health-connectс обязательным двухшаговым процессом (проверка инициализации, затем отдельный контракт запроса разрешений).
3. Хук: динамический экспорт по платформе
Вся магия происходит при динамическом экспорте моста в зависимости от платформы: компоненты вызывают один-единственный пользовательский хук:
import { Platform } from 'react-native';
import { iOSHealthBridge } from './iOSHealthBridge';
import { AndroidHealthBridge } from './AndroidHealthBridge';
const HealthAPI = Platform.OS === 'ios' ? iOSHealthBridge : AndroidHealthBridge;
export function useHydrationSync() {
const syncData = async () => {
const available = await HealthAPI.isAvailable();
if (!available) return;
const hasPermission = await HealthAPI.requestPermissions();
if (hasPermission) {
const records = await HealthAPI.getHydrationData(new Date());
// Commit directly to a local-first store (e.g. AsyncStorage or Zustand)
}
};
return { syncData };
}4. Главная ловушка Expo 💡
Оба нативных пакета используют сильно настроенный нативный код (Objective-C и Kotlin), поэтому они не скомпилируются в обычном Expo Go. Чтобы аккуратно обойти это без ручной работы в Xcode или Android Studio, нужно объявить config plugins в app.json. Обратите внимание, что Health Connect явно требует минимальную версию SDK 26:
{
"expo": {
"plugins": [
["react-native-health", { "NSHealthShareUsageDescription": "Sync your hydration records." }],
["expo-health-connect"],
[
"expo-build-properties",
{ "android": { "minSdkVersion": 26 } }
]
]
}
}После этого запуск npx expo prebuild --clean автоматически выполняет нативную генерацию.
Зачем столько усилий?
При создании минималистичных утилит важны производительность и отсутствие трения. Благодаря такой архитектуре интерфейс остаётся плавным на 60 FPS, обновления мгновенны, а поскольку подход local-first, приложение полностью уважает приватность данных пользователя. Никаких сторонних серверов и обязательной регистрации.
Эту структурную логику я использовал для умных напоминаний и системы развития талисмана в WaterPaw, которое я только что запустил!
