Files
MABEA/flutter_app/lib/api/api_client.dart
T
patrickandClaude Sonnet 5 0909dacf2b
CI / backend-tests (push) Successful in 1m5s
CI / frontend-build (push) Successful in 18s
Flutter-Tests: ApiClient, Offline-Queue, Login-/Objektliste-Widgets
ApiClient nimmt jetzt einen injizierbaren http.Client entgegen (Testbarkeit,
kein Verhaltensunterschied im Betrieb). Tests mocken ihn mit mocktail statt
echter Netzwerkaufrufe. offline_queue_test.dart nutzt sqflite_common_ffi
(reines Dart) für die Queue-Logik ohne Gerät/Emulator: Erfolg entfernt
Eintrag, Netzwerkfehler bleibt "wartet", 4xx setzt "fehler" ohne Auto-Retry.
Dazu Widget-Tests für Login-Flow und Objektliste-Suchfilter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-04 18:38:17 +02:00

103 lines
3.0 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
/// Gleiche Basis-Idee wie im React-Client (frontend/src/api/client.ts):
/// gemeinsamer JSON-Wrapper, Token aus persistentem Speicher, ApiException
/// mit HTTP-Status, damit aufrufender Code gezielt reagieren kann (z.B. 409
/// bei Objekt-Sperre).
class ApiException implements Exception {
final int status;
final dynamic detail;
ApiException(this.status, this.detail);
@override
String toString() => 'ApiException($status, $detail)';
}
class ApiClient {
ApiClient({required this.baseUrl, http.Client? httpClient}) : _http = httpClient ?? http.Client();
final String baseUrl;
final http.Client _http;
String? _token;
Future<void> ladeToken() async {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('mabea_token');
}
Future<void> setzeToken(String? token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
if (token == null) {
await prefs.remove('mabea_token');
} else {
await prefs.setString('mabea_token', token);
}
}
bool get istEingeloggt => _token != null;
Future<String> login(String username, String password) async {
final response = await _http.post(
Uri.parse('$baseUrl/auth/login'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {'username': username, 'password': password},
);
if (response.statusCode != 200) {
throw ApiException(response.statusCode, _tryDecode(response.body));
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final token = data['access_token'] as String;
await setzeToken(token);
return token;
}
Future<dynamic> request(
String path, {
String method = 'GET',
Object? body,
}) async {
final uri = Uri.parse('$baseUrl$path');
final headers = {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
final encodedBody = body != null ? jsonEncode(body) : null;
late http.Response response;
switch (method) {
case 'POST':
response = await _http.post(uri, headers: headers, body: encodedBody);
break;
case 'PUT':
response = await _http.put(uri, headers: headers, body: encodedBody);
break;
case 'PATCH':
response = await _http.patch(uri, headers: headers, body: encodedBody);
break;
case 'DELETE':
response = await _http.delete(uri, headers: headers);
break;
default:
response = await _http.get(uri, headers: headers);
}
if (response.statusCode >= 400) {
throw ApiException(response.statusCode, _tryDecode(response.body));
}
if (response.body.isEmpty) return null;
return jsonDecode(response.body);
}
dynamic _tryDecode(String body) {
try {
return jsonDecode(body);
} catch (_) {
return null;
}
}
}