react-nativefluttermobileapideveloper-guide

Integrating Body Measurement APIs in React Native and Flutter

· 7 min read · Martin Hejda

Mobile is the primary surface for sizing features in fashion, fitness, and health applications. The integration pattern differs from web in one critical way: you cannot put your body measurement API key in the mobile app binary. App binaries can be decompiled. A key in a React Native bundle or a Flutter binary is effectively public.

The correct architecture has a backend service — an API route on your server — that holds the prediction API key and proxies requests from the app.


The architecture

Mobile App (RN / Flutter)

    │ POST /api/sizing/predict
    │ Authorization: Bearer <user_token>
    │ Body: { gender, height_cm, weight_kg, region }

Your Backend (Node.js / FastAPI / Rails / etc.)
    │ Validates user token
    │ Calls prediction API with server-side key

Prediction API (holds your API key server-side)

The mobile app never knows the prediction API key. It only knows your own backend’s URL.


Backend proxy endpoint (Node.js / Express)

// server/routes/sizing.js
import express from 'express';
import fetch from 'node-fetch';

const router = express.Router();

router.post('/predict', async (req, res) => {
  const { gender, height_cm, weight_kg, region = 'GLOBAL' } = req.body;
  
  // Validate inputs server-side
  if (!gender || !height_cm || !weight_kg) {
    return res.status(400).json({ error: 'gender, height_cm, and weight_kg are required' });
  }
  if (!['male', 'female'].includes(gender)) {
    return res.status(400).json({ error: 'gender must be male or female' });
  }
  if (height_cm < 100 || height_cm > 250) {
    return res.status(400).json({ error: 'height_cm must be between 100 and 250' });
  }
  if (weight_kg < 20 || weight_kg > 300) {
    return res.status(400).json({ error: 'weight_kg must be between 20 and 300' });
  }
  
  try {
    const response = await fetch(
      'https://dimensionspot-bodysize-engine.p.rapidapi.com/v1/predict',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-RapidAPI-Key': process.env.PREDICTION_API_KEY, // Server-side only
          'X-RapidAPI-Host': 'dimensionspot-bodysize-engine.p.rapidapi.com'
        },
        body: JSON.stringify({
          input_data: {
            input_unit_system: 'metric',
            subject: { gender, input_origin_region: region },
            anchors: {
              body_height: Math.round(height_cm * 10), // cm → mm
              body_mass: weight_kg
            }
          },
          output_settings: {
            calculation: { target_region: region, body_build_type: 'CIVILIAN' },
            requested_dimensions: { bundle: 'TORSO' },
            output_format: { include_range_95: true, confidence_score_threshold: 60 }
          }
        })
      }
    );
    
    const data = await response.json();
    res.json(data);
  } catch (error) {
    res.status(502).json({ error: 'Prediction service unavailable' });
  }
});

export default router;

React Native implementation

// screens/SizingScreen.tsx
import React, { useState } from 'react';
import {
  View, Text, TextInput, TouchableOpacity,
  ActivityIndicator, StyleSheet, Alert
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface MeasurementProfile {
  gender: 'male' | 'female';
  height_cm: number;
  weight_kg: number;
  region: string;
  saved_at: string;
}

interface MeasurementDetail {
  value: number;
  confidence_score: number;
  range_95?: [number, number];
  unit: string;
  label: string;
}

const PROFILE_KEY = '@sizing_profile';
const API_BASE = 'https://api.yourapp.com';

export default function SizingScreen() {
  const [gender, setGender] = useState<'male' | 'female'>('female');
  const [heightCm, setHeightCm] = useState('');
  const [weightKg, setWeightKg] = useState('');
  const [loading, setLoading] = useState(false);
  const [dimensions, setDimensions] = useState<[string, MeasurementDetail][]>([]);

  const saveProfile = async (profile: MeasurementProfile) => {
    await AsyncStorage.setItem(PROFILE_KEY, JSON.stringify(profile));
  };

  const loadProfile = async () => {
    const stored = await AsyncStorage.getItem(PROFILE_KEY);
    if (stored) {
      const profile: MeasurementProfile = JSON.parse(stored);
      setGender(profile.gender);
      setHeightCm(String(profile.height_cm));
      setWeightKg(String(profile.weight_kg));
    }
  };

  React.useEffect(() => {
    loadProfile();
  }, []);

  const handlePredict = async () => {
    const h = parseFloat(heightCm);
    const w = parseFloat(weightKg);

    if (!h || !w || h < 100 || h > 250 || w < 20 || w > 300) {
      Alert.alert('Invalid input', 'Please enter valid height (100–250cm) and weight (20–300kg).');
      return;
    }

    setLoading(true);
    try {
      const response = await fetch(`${API_BASE}/api/sizing/predict`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${await getAuthToken()}` // Your auth token
        },
        body: JSON.stringify({ gender, height_cm: h, weight_kg: w, region: 'GLOBAL' })
      });

      if (!response.ok) {
        throw new Error(`Server error ${response.status}`);
      }

      const data = await response.json();
      setDimensions(Object.entries(data.body_dimensions || {}));

      // Cache the profile locally
      await saveProfile({
        gender, height_cm: h, weight_kg: w,
        region: 'GLOBAL',
        saved_at: new Date().toISOString()
      });
    } catch (error) {
      Alert.alert('Error', 'Could not get size prediction. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Body proportions</Text>
      <View style={styles.genderRow}>
        {(['female', 'male'] as const).map(g => (
          <TouchableOpacity
            key={g}
            style={[styles.genderBtn, gender === g && styles.genderBtnActive]}
            onPress={() => setGender(g)}
          >
            <Text style={gender === g ? styles.genderTextActive : styles.genderText}>
              {g === 'female' ? 'Feminine' : 'Masculine'}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

      <Text style={styles.label}>Height (cm)</Text>
      <TextInput
        style={styles.input}
        keyboardType="decimal-pad"
        value={heightCm}
        onChangeText={setHeightCm}
        placeholder="e.g. 170"
      />

      <Text style={styles.label}>Weight (kg)</Text>
      <TextInput
        style={styles.input}
        keyboardType="decimal-pad"
        value={weightKg}
        onChangeText={setWeightKg}
        placeholder="e.g. 65"
      />

      <TouchableOpacity
        style={styles.button}
        onPress={handlePredict}
        disabled={loading}
      >
        {loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Get My Size</Text>}
      </TouchableOpacity>

      {dimensions.map(([dimId, dim]) => (
        <View key={dimId} style={styles.dimRow}>
          <Text style={styles.dimName}>{dimId.replace(/_/g, ' ')}</Text>
          <Text style={styles.dimValue}>{(dim.value / 10).toFixed(1)} cm</Text>
        </View>
      ))}
    </View>
  );
}

async function getAuthToken(): Promise<string> {
  // Return your app's current user auth token
  return AsyncStorage.getItem('@auth_token') || '';
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, backgroundColor: '#fff' },
  label: { fontSize: 14, fontWeight: '600', marginTop: 16, marginBottom: 6, color: '#333' },
  input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, fontSize: 16 },
  genderRow: { flexDirection: 'row', gap: 12, marginBottom: 4 },
  genderBtn: { flex: 1, padding: 12, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, alignItems: 'center' },
  genderBtnActive: { backgroundColor: '#1a1a2e', borderColor: '#1a1a2e' },
  genderText: { color: '#333' },
  genderTextActive: { color: '#fff', fontWeight: '600' },
  button: { backgroundColor: '#1a1a2e', padding: 16, borderRadius: 8, alignItems: 'center', marginTop: 24 },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  dimRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' },
  dimName: { color: '#555', textTransform: 'capitalize' },
  dimValue: { fontWeight: '600', color: '#1a1a2e' },
});

Flutter implementation

// lib/screens/sizing_screen.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class SizingScreen extends StatefulWidget {
  const SizingScreen({super.key});

  @override
  State<SizingScreen> createState() => _SizingScreenState();
}

class _SizingScreenState extends State<SizingScreen> {
  String gender = 'female';
  final heightController = TextEditingController();
  final weightController = TextEditingController();
  bool loading = false;
  List<Map<String, dynamic>> dimensions = [];

  @override
  void initState() {
    super.initState();
    _loadStoredProfile();
  }

  Future<void> _loadStoredProfile() async {
    final prefs = await SharedPreferences.getInstance();
    final stored = prefs.getString('sizing_profile');
    if (stored != null) {
      final profile = jsonDecode(stored) as Map<String, dynamic>;
      setState(() {
        gender = profile['gender'] ?? 'female';
        heightController.text = profile['height_cm']?.toString() ?? '';
        weightController.text = profile['weight_kg']?.toString() ?? '';
      });
    }
  }

  Future<void> _saveProfile() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('sizing_profile', jsonEncode({
      'gender': gender,
      'height_cm': double.tryParse(heightController.text),
      'weight_kg': double.tryParse(weightController.text),
      'saved_at': DateTime.now().toIso8601String(),
    }));
  }

  Future<void> _predict() async {
    final h = double.tryParse(heightController.text);
    final w = double.tryParse(weightController.text);

    if (h == null || w == null || h < 100 || h > 250 || w < 20 || w > 300) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Enter valid height (100–250cm) and weight (20–300kg)')),
      );
      return;
    }

    setState(() => loading = true);

    try {
      final response = await http.post(
        Uri.parse('https://api.yourapp.com/api/sizing/predict'),
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ${await _getAuthToken()}',
        },
        body: jsonEncode({
          'gender': gender,
          'height_cm': h,
          'weight_kg': w,
          'region': 'GLOBAL',
        }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body) as Map<String, dynamic>;
        final rawDims = (data['body_dimensions'] as Map<String, dynamic>?) ?? {};
        setState(() {
          dimensions = rawDims.entries
              .map((e) => {'dim_id': e.key, ...Map<String, dynamic>.from(e.value as Map)})
              .toList();
        });
        await _saveProfile();
      } else {
        throw Exception('Server error ${response.statusCode}');
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Could not get prediction. Please try again.')),
      );
    } finally {
      setState(() => loading = false);
    }
  }

  Future<String> _getAuthToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString('auth_token') ?? '';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Find Your Size')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('Body proportions', style: TextStyle(fontWeight: FontWeight.w600)),
            const SizedBox(height: 8),
            SegmentedButton<String>(
              segments: const [
                ButtonSegment(value: 'female', label: Text('Feminine')),
                ButtonSegment(value: 'male', label: Text('Masculine')),
              ],
              selected: {gender},
              onSelectionChanged: (s) => setState(() => gender = s.first),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: heightController,
              keyboardType: const TextInputType.numberWithOptions(decimal: true),
              decoration: const InputDecoration(
                labelText: 'Height (cm)',
                hintText: 'e.g. 170',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: weightController,
              keyboardType: const TextInputType.numberWithOptions(decimal: true),
              decoration: const InputDecoration(
                labelText: 'Weight (kg)',
                hintText: 'e.g. 65',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: FilledButton(
                onPressed: loading ? null : _predict,
                child: loading
                  ? const SizedBox(
                      height: 20, width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                    )
                  : const Text('Get My Size'),
              ),
            ),
            if (dimensions.isNotEmpty) ...[
              const SizedBox(height: 24),
              const Text('Your measurements', style: TextStyle(fontWeight: FontWeight.w600)),
              const SizedBox(height: 8),
              ...dimensions.map((dim) => Padding(
                padding: const EdgeInsets.symmetric(vertical: 4),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      (dim['dim_id'] as String).replaceAll('_', ' '),
                      style: const TextStyle(color: Colors.grey),
                    ),
                    Text(
                      '${((dim['value'] as num) / 10).toStringAsFixed(1)} cm',
                      style: const TextStyle(fontWeight: FontWeight.w600),
                    ),
                  ],
                ),
              )),
            ],
          ],
        ),
      ),
    );
  }
}

Offline handling

Body dimension predictions can be cached locally so users can still see their last size recommendation when offline:

// React Native: serve from cache when offline
const handlePredictWithOfflineFallback = async () => {
  try {
    await handlePredict(); // Try live prediction first
  } catch (networkError) {
    // Fall back to cached profile
    const cached = await AsyncStorage.getItem(PROFILE_KEY);
    if (cached) {
      const profile = JSON.parse(cached);
      Alert.alert(
        'Offline mode',
        `Showing size estimate based on your saved measurements from ${
          new Date(profile.saved_at).toLocaleDateString()
        }. Connect to the internet for an updated prediction.`
      );
      // Display cached dimensions if stored
      const cachedDims = await AsyncStorage.getItem('@cached_dimensions');
      if (cachedDims) {
        setDimensions(JSON.parse(cachedDims));
      }
    } else {
      Alert.alert('No connection', 'Please connect to the internet to get your size recommendation.');
    }
  }
};

Platform-specific considerations

iOS: HealthKit integration can auto-populate height and weight if the user grants permission. Request only the specific quantity types you need (HKQuantityTypeIdentifierHeight, HKQuantityTypeIdentifierBodyMass).

Android: Health Connect provides similar capability via the Compose Health SDK. Available on Android 14+ natively; earlier versions require the Health Connect companion app.

Both platforms: App Store and Play Store review guidelines flag apps that collect health data. If your app’s primary purpose includes health or fitness, declare this in your app’s privacy nutrition label. Even if you proxy all API calls through your server and store nothing locally, the user is providing health data — be transparent about the data flow in your privacy policy.


The server-side proxy pattern adds one round trip (client → your server → prediction API) but eliminates the API key exposure risk entirely. For mobile specifically, this is non-negotiable — treat any secret in a mobile binary as compromised.

Try DimensionsPot

Free tier — 100 requests/month, no credit card required.

Get API on RapidAPI