Mobile App
API Integration
How the Flutter app communicates with the Rust backend
API Integration
Dio HTTP client
The app uses Dio with a base URL and Authorization interceptor.
final dio = Dio(BaseOptions(
baseUrl: 'http://10.0.2.2:8080', // 10.0.2.2 = localhost on Android emulator
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 30),
));
// Auth interceptor β adds Bearer token
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = ref.read(tokenProvider);
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
));Base URL configuration
| Environment | URL |
|---|---|
| Android emulator | http://10.0.2.2:8080 |
| iOS simulator | http://localhost:8080 |
| Physical device (same network) | http://<your-machine-IP>:8080 |
| Production | https://api.cookest.app |
Android emulators cannot reach localhost or 127.0.0.1. Use 10.0.2.2 which the Android virtual network maps to the host machine.
Response shape handling
The API may return either a plain array or a wrapped object depending on the endpoint. All repositories handle both:
final raw = response.data;
final list = raw is List ? raw : (raw['items'] as List? ?? []);Content-Type requirement
All POST requests require Content-Type: application/json. Dio omits this header when data is null. Always pass data: {} for bodyless POSTs:
// β Correct β sends Content-Type: application/json
await dio.post('/api/shopping-list/sync', data: {});
// β Wrong β Dio will omit Content-Type, API returns 400
await dio.post('/api/shopping-list/sync');Token storage
// Access token β stored in memory (Riverpod StateProvider)
// Never written to SharedPreferences
// Refresh token β httpOnly cookie, managed by browser/WebView/Dio cookie jar
// On mobile: use dio_cookie_manager + cookie_jarError handling
try {
final res = await dio.get('/api/recipes');
return (res.data['items'] as List)
.map((j) => Recipe.fromJson(j)).toList();
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
// Token expired β trigger refresh
} else if (e.response?.statusCode == 402) {
// Subscription required β show upgrade prompt
}
rethrow;
}