handle exceptions by throwing, not with results

This commit is contained in:
2025-10-25 21:02:22 +02:00
parent d855ca4ea4
commit 1bcc5df822
5 changed files with 66 additions and 180 deletions

View File

@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; // For cache storage import 'package:path_provider/path_provider.dart'; // For cache storage
import 'package:sheetless/utility.dart';
import 'sheet.dart'; import 'sheet.dart';
@@ -16,31 +15,22 @@ class ApiClient {
ApiClient({required this.baseUrl, this.token}); ApiClient({required this.baseUrl, this.token});
Future<Result<void>> login(String username, String password) async { Future<void> login(String username, String password) async {
log.info("Logging in..."); log.info("Logging in...");
try { final url = '$baseUrl/login';
final url = '$baseUrl/login'; final response = await http.post(
final response = await http.post( Uri.parse(url),
Uri.parse(url), headers: {'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json'}, body: jsonEncode({'email': username, 'password': password}),
body: jsonEncode({'email': username, 'password': password}), );
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
token = jsonDecode(response.body); token = jsonDecode(response.body);
log.info('Login successful'); log.info('Login successful');
return Result.ok(null); } else {
} else { throw Exception(
log.warning('Login failed: ${response.statusCode}, ${response.body}'); "Failed logging in: Response code ${response.statusCode}\nResponse: ${response.body}",
return Result.error( );
Exception(
"Response code ${response.statusCode}\nResponse: ${response.body}",
),
);
}
} on Exception catch (e) {
log.warning('Error during login', e);
return Result.error(e);
} }
} }
@@ -49,27 +39,26 @@ class ApiClient {
log.info('Logged out successfully.'); log.info('Logged out successfully.');
} }
Future<http.Response?> get(String endpoint, {bool isBinary = false}) async { Future<http.Response> get(
try { String endpoint, {
final url = '$baseUrl$endpoint'; bool isBinary = false,
final headers = { bool throwExceptionIfStatusCodeNot200 = false,
'Authorization': 'Bearer $token', }) async {
if (!isBinary) 'Content-Type': 'application/json', final url = '$baseUrl$endpoint';
}; final headers = {
'Authorization': 'Bearer $token',
if (!isBinary) 'Content-Type': 'application/json',
};
final response = await http.get(Uri.parse(url), headers: headers); final response = await http.get(Uri.parse(url), headers: headers);
if (response.statusCode == 200) { if (!throwExceptionIfStatusCodeNot200 || response.statusCode == 200) {
return response; return response;
} else { } else {
log.warning( throw Exception(
'GET request failed: ${response.statusCode} ${response.body}', 'GET request failed: ${response.statusCode} ${response.body}',
); );
}
} catch (e) {
log.warning('Error during GET request', e);
} }
return null;
} }
Future<http.Response?> post( Future<http.Response?> post(
@@ -130,71 +119,41 @@ class ApiClient {
} }
Future<List<Sheet>> fetchSheets() async { Future<List<Sheet>> fetchSheets() async {
try { final response = await get(
final response = await get("/list/sheets"); "/list/sheets",
throwExceptionIfStatusCodeNot200: true,
);
if (response == null) { final data = jsonDecode(response.body);
return List.empty(); return (data as List<dynamic>)
} .map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
.toList();
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return (data as List<dynamic>)
.map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
.toList();
} else {
log.warning(
'Failed to fetch sheets with status: ${response.statusCode}',
);
log.info('Response: ${response.body}');
}
} catch (e) {
log.warning('Error during fetching sheets', e);
}
return List.empty();
} }
Future<Uint8List?> fetchPdfFileData(String sheetUuid) async { Future<Uint8List> fetchPdfFileData(String sheetUuid) async {
try { final response = await get(
final response = await get('/sheet/pdf/$sheetUuid', isBinary: true); '/sheet/pdf/$sheetUuid',
isBinary: true,
throwExceptionIfStatusCodeNot200: true,
);
if (response != null && response.statusCode == 200) { log.info("PDF downloaded");
log.info("PDF downloaded"); return response.bodyBytes;
return response.bodyBytes;
} else {
log.warning(
"Failed to fetch PDF from API. Status: ${response?.statusCode}",
);
}
} catch (e) {
log.warning("Error fetching PDF", e);
}
return null;
} }
Future<File?> getPdfFileCached(String sheetUuid) async { Future<File> getPdfFileCached(String sheetUuid) async {
try { final cacheDir = await getTemporaryDirectory();
final cacheDir = await getTemporaryDirectory(); final cachedPdfPath = '${cacheDir.path}/$sheetUuid.pdf';
final cachedPdfPath = '${cacheDir.path}/$sheetUuid.pdf';
final cachedFile = File(cachedPdfPath); final cachedFile = File(cachedPdfPath);
if (await cachedFile.exists()) { if (await cachedFile.exists()) {
log.info("PDF found in cache: $cachedPdfPath"); log.info("PDF found in cache: $cachedPdfPath");
return cachedFile;
}
final pdfFileData = await fetchPdfFileData(sheetUuid);
await cachedFile.writeAsBytes(
pdfFileData!,
); // TODO: proper error handling
log.info("PDF cached at: $cachedPdfPath");
return cachedFile; return cachedFile;
} catch (e) {
log.warning("Error fetching PDF", e);
} }
return null; final pdfFileData = await fetchPdfFileData(sheetUuid);
await cachedFile.writeAsBytes(pdfFileData);
log.info("PDF cached at: $cachedPdfPath");
return cachedFile;
} }
} }

View File

@@ -106,6 +106,7 @@ class _MyHomePageState extends State<MyHomePage> {
}, },
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
log.warning("Error loading sheets:", snapshot.error);
return Center( return Center(
child: Text( child: Text(
style: Theme.of( style: Theme.of(

View File

@@ -63,15 +63,16 @@ class _LoginPageState extends State<LoginPage> {
_error = null; _error = null;
}); });
final apiClient = ApiClient(baseUrl: "$serverUrl/api"); final apiClient = ApiClient(baseUrl: "$serverUrl/api");
final loginResult = await apiClient.login(email, password); try {
if (loginResult.isOk()) { await apiClient.login(email, password);
await _storageHelper.writeSecure(SecureStorageKey.url, serverUrl); await _storageHelper.writeSecure(SecureStorageKey.url, serverUrl);
await _storageHelper.writeSecure(SecureStorageKey.jwt, apiClient.token!); await _storageHelper.writeSecure(SecureStorageKey.jwt, apiClient.token!);
await _storageHelper.writeSecure(SecureStorageKey.email, email); await _storageHelper.writeSecure(SecureStorageKey.email, email);
await _navigateToMainPage(); await _navigateToMainPage();
} else { } catch (e) {
setState(() { setState(() {
_error = "Login failed.\n${loginResult.error()}"; _error = "Login failed.\n$e";
}); });
} }
} }

View File

@@ -60,17 +60,11 @@ class _SheetViewerPageState extends State<SheetViewerPage>
Future<bool> loadPdf() async { Future<bool> loadPdf() async {
if (kIsWeb) { if (kIsWeb) {
var data = await widget.apiClient.fetchPdfFileData(widget.sheet.uuid); final data = await widget.apiClient.fetchPdfFileData(widget.sheet.uuid);
if (data == null) {
throw Exception("Failed fetching pdf file");
}
document = await PdfDocument.openData(data); document = await PdfDocument.openData(data);
} else { } else {
var file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid); final file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid);
if (file == null) {
throw Exception("Failed fetching pdf file");
}
document = await PdfDocument.openFile(file.path); document = await PdfDocument.openFile(file.path);
} }
@@ -181,6 +175,7 @@ class _SheetViewerPageState extends State<SheetViewerPage>
], ],
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
log.warning("Error loading pdf:", snapshot.error);
return Center( return Center(
child: Text( child: Text(
style: Theme.of( style: Theme.of(

View File

@@ -1,70 +0,0 @@
/// Utility class that simplifies handling errors.
///
/// Return a [Result] from a function to indicate success or failure.
///
/// A [Result] is either an [Ok] with a value of type [T]
/// or an [Err] with an [Exception].
///
/// Use [Result.ok] to create a successful result with a value of type [T].
/// Use [Result.error] to create an error result with an [Exception].
///
/// Evaluate the result using a switch statement:
/// ```dart
/// switch (result) {
/// case Ok(): {
/// print(result.value);
/// }
/// case Error(): {
/// print(result.error);
/// }
/// }
/// ```
sealed class Result<T> {
const Result();
/// Creates a successful [Result], completed with the specified [value].
const factory Result.ok(T value) = Ok._;
/// Creates an error [Result], completed with the specified [error].
const factory Result.error(Exception error) = Err._;
bool isOk() {
return this is Ok;
}
T value() {
Ok<T> ok = this as Ok<T>;
return ok._value;
}
bool isErr() {
return this is Err;
}
Exception error() {
Err<T> err = this as Err<T>;
return err._error;
}
}
/// A successful [Result] with a returned [value].
final class Ok<T> extends Result<T> {
const Ok._(this._value);
/// The returned value of this result.
final T _value;
@override
String toString() => 'Result<$T>.ok($_value)';
}
/// An error [Result] with a resulting [error].
final class Err<T> extends Result<T> {
const Err._(this._error);
/// The resulting error of this result.
final Exception _error;
@override
String toString() => 'Result<$T>.error($_error)';
}