Files
mileograph_flutter/lib/services/api_service.dart
Pete Gregory 950978b021
All checks were successful
Release / meta (push) Successful in 12s
Release / linux-build (push) Successful in 7m42s
Release / android-build (push) Successful in 16m34s
Release / release-dev (push) Successful in 38s
Release / release-master (push) Successful in 37s
new settings panel for url pickup
2025-12-22 22:45:33 +00:00

153 lines
4.1 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
typedef TokenProvider = String? Function();
typedef UnauthorizedHandler = Future<void> Function();
class ApiService {
String _baseUrl;
final http.Client _client;
final Duration timeout;
TokenProvider? _getToken;
UnauthorizedHandler? _onUnauthorized;
ApiService({
required String baseUrl,
http.Client? client,
this.timeout = const Duration(seconds: 30),
}) : _baseUrl = baseUrl,
_client = client ?? http.Client();
String get baseUrl => _baseUrl;
void setBaseUrl(String url) {
_baseUrl = url;
}
void setTokenProvider(TokenProvider provider) {
_getToken = provider;
}
void setUnauthorizedHandler(UnauthorizedHandler handler) {
_onUnauthorized = handler;
}
void dispose() {
_client.close();
}
Map<String, String> _buildHeaders(Map<String, String>? extra) {
final token = _getToken?.call();
final headers = {'accept': 'application/json', ...?extra};
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
Future<dynamic> get(String endpoint, {Map<String, String>? headers}) async {
final response = await _client
.get(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders(headers),
)
.timeout(timeout);
return _processResponse(response);
}
Future<dynamic> post(
String endpoint,
dynamic data, {
Map<String, String>? headers,
}) async {
final hasBody = data != null;
final response = await _client
.post(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers),
body: hasBody ? jsonEncode(data) : null,
)
.timeout(timeout);
return _processResponse(response);
}
Future<dynamic> postForm(String endpoint, Map<String, String> data) async {
final response = await _client
.post(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
'accept': 'application/json',
}),
body: data, // http package handles form-encoding for Map<String, String>
)
.timeout(timeout);
return _processResponse(response);
}
Future<dynamic> put(
String endpoint,
dynamic data, {
Map<String, String>? headers,
}) async {
final hasBody = data != null;
final response = await _client
.put(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders(hasBody ? _jsonHeaders(headers) : headers),
body: hasBody ? jsonEncode(data) : null,
)
.timeout(timeout);
return _processResponse(response);
}
Future<dynamic> delete(
String endpoint, {
Map<String, String>? headers,
}) async {
final response = await _client
.delete(
Uri.parse('$baseUrl$endpoint'),
headers: _buildHeaders(headers),
)
.timeout(timeout);
return _processResponse(response);
}
Map<String, String> _jsonHeaders(Map<String, String>? extra) {
return {'Content-Type': 'application/json', if (extra != null) ...extra};
}
Future<dynamic> _processResponse(http.Response res) async {
final body = _decodeBody(res);
if (res.statusCode >= 200 && res.statusCode < 300) {
return body;
}
if (res.statusCode == 401 && _onUnauthorized != null) {
await _onUnauthorized!();
}
throw Exception('API error ${res.statusCode}: $body');
}
dynamic _decodeBody(http.Response res) {
if (res.body.isEmpty) return null;
final contentType = res.headers['content-type'] ?? '';
final shouldTryJson = contentType.contains('application/json') ||
contentType.contains('+json') ||
res.body.trimLeft().startsWith('{') ||
res.body.trimLeft().startsWith('[');
if (!shouldTryJson) return res.body;
try {
return jsonDecode(res.body);
} catch (_) {
// Avoid turning a server-side error body into a client-side crash.
return res.body;
}
}
}