Merge pull request 'Handle errors when scanning a barcode and show a snackbar in case' (#2) from error-handling into master

Reviewed-on: #2
This commit is contained in:
marco 2024-09-08 13:11:28 +00:00
commit a057115b83
6 changed files with 196 additions and 96 deletions

View File

@ -1,4 +1,4 @@
import 'package:calorimeter/perdate/perdate_widget.dart'; import 'package:calorimeter/utils/enter_food_controller.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';

View File

@ -1,22 +1,44 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
class FoodFactLookupClient { class FoodFactLookupClient {
FoodFactLookupClient(); FoodFactLookupClient();
static const String host = "world.openfoodfacts.org";
static const String url = "https://world.openfoodfacts.org/api/v3/product/"; static const String url = "https://world.openfoodfacts.org/api/v3/product/";
String getProductUrl(String ean) { String getProductUrl(String ean) {
return "$url$ean.json"; return "$url$ean.json";
} }
Future<FoodFactModel> retrieveFoodInfo(String ean) async { Future<FoodFactResponse> retrieveFoodInfo(String ean) async {
HttpClient client = HttpClient(); HttpClient client = HttpClient();
String asString = "";
try {
var request = await client.getUrl(Uri.parse(getProductUrl(ean))); var request = await client.getUrl(Uri.parse(getProductUrl(ean)));
var response = await request.close(); var response = await request.close();
var asString = await response.transform(utf8.decoder).join();
return FoodFactModel.fromJson(jsonDecode(asString)); if (response.statusCode != HttpStatus.ok) {
return FoodFactResponse(
food: null, status: FoodFactResponseStatus.barcodeNotFound);
}
asString = await response.transform(utf8.decoder).join();
} on SocketException {
return FoodFactResponse(
food: null,
status: FoodFactResponseStatus.foodFactServerNotReachable);
} catch (e) {
log(e.toString());
} finally {
client.close();
}
return FoodFactResponse(
food: FoodFactModel.fromJson(jsonDecode(asString)),
status: FoodFactResponseStatus.ok);
} }
} }
@ -32,3 +54,16 @@ class FoodFactModel {
kcalPer100g: json['product']['nutriments']['energy-kcal_100g']); kcalPer100g: json['product']['nutriments']['energy-kcal_100g']);
} }
} }
enum FoodFactResponseStatus {
foodFactServerNotReachable,
barcodeNotFound,
ok,
}
class FoodFactResponse {
final FoodFactModel? food;
final FoodFactResponseStatus status;
FoodFactResponse({required this.food, required this.status});
}

View File

@ -1,5 +1,6 @@
import 'dart:developer'; import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
import 'package:calorimeter/utils/enter_food_controller.dart';
import 'package:calorimeter/utils/scan_food_floating_button.dart'; import 'package:calorimeter/utils/scan_food_floating_button.dart';
import 'package:calorimeter/utils/app_drawer.dart'; import 'package:calorimeter/utils/app_drawer.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
@ -29,7 +30,6 @@ class _PerDateWidgetState extends State<PerDateWidget> {
@override @override
void initState() { void initState() {
log("PerDateWidgetState's initState()");
storage = FoodStorage.getInstance(); storage = FoodStorage.getInstance();
entriesFuture = storage.getEntriesForDate(widget.date); entriesFuture = storage.getEntriesForDate(widget.date);
entriesFuture.then((val) { entriesFuture.then((val) {
@ -40,20 +40,23 @@ class _PerDateWidgetState extends State<PerDateWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
log("PerDateWidgetState's build()");
return FutureBuilder( return FutureBuilder(
future: entriesFuture, future: entriesFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) { return snapshot.connectionState != ConnectionState.done
return const Center(child: CircularProgressIndicator()); ? const Center(child: CircularProgressIndicator())
} else { : MultiProvider(
return ChangeNotifierProvider( providers: [
create: (context) => EnterFoodController(), ChangeNotifierProvider(
child: BlocProvider( create: (context) => EnterFoodController()),
BlocProvider(
create: (context) => FoodEntryBloc( create: (context) => FoodEntryBloc(
initialState: FoodEntryState(foodEntries: entries), initialState: FoodEntryState(foodEntries: entries),
storage: storage, storage: storage,
forDate: widget.date), forDate: widget.date,
),
)
],
child: BlocBuilder<FoodEntryBloc, FoodEntryState>( child: BlocBuilder<FoodEntryBloc, FoodEntryState>(
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
@ -69,7 +72,48 @@ class _PerDateWidgetState extends State<PerDateWidget> {
child: SumWidget(foodEntries: state.foodEntries)), child: SumWidget(foodEntries: state.foodEntries)),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [ floatingActionButton: OverflowBar(children: [
const ScanFoodFloatingButton(), ScanFoodFloatingButton(
onPressed: () async {
var client = FoodFactLookupClient();
var scanResult = await BarcodeScanner.scan();
if (scanResult.type == ResultType.Cancelled) {
return;
}
if (!context.mounted) return;
if (scanResult.type == ResultType.Error) {
showNewSnackbarWith(context,
"Fehler beim Scannen des Barcodes.");
}
var response = await client
.retrieveFoodInfo(scanResult.rawContent);
if (!context.mounted) return;
if (response.status ==
FoodFactResponseStatus.barcodeNotFound) {
showNewSnackbarWith(context,
"Barcode konnte nicht gefunden werden.");
return;
}
if (response.status ==
FoodFactResponseStatus
.foodFactServerNotReachable) {
showNewSnackbarWith(context,
"OpenFoodFacts-Server konnte nicht erreicht werden.");
return;
}
context.read<EnterFoodController>().set(
response.food!.name,
response.food!.kcalPer100g.toString(),
);
},
),
const SizedBox(width: 8), const SizedBox(width: 8),
CalendarFloatingButton( CalendarFloatingButton(
startFromDate: widget.date, startFromDate: widget.date,
@ -87,20 +131,27 @@ class _PerDateWidgetState extends State<PerDateWidget> {
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked); FloatingActionButtonLocation.endDocked);
}), }),
),
); );
}
}); });
} }
}
class EnterFoodController extends ChangeNotifier { void showNewSnackbarWith(BuildContext context, String text) {
String name = ""; var snackbar =
String kcalPer100g = ""; ErrorSnackbar(colorScheme: Theme.of(context).colorScheme, text: text);
void set(String newName, String newKcal) { ScaffoldMessenger.of(context).clearSnackBars();
name = newName; ScaffoldMessenger.of(context).showSnackBar(snackbar);
kcalPer100g = newKcal;
notifyListeners();
} }
} }
class ErrorSnackbar extends SnackBar {
final String text;
final ColorScheme colorScheme;
ErrorSnackbar({
required this.text,
required this.colorScheme,
super.key,
}) : super(
content: Text(text, style: TextStyle(color: colorScheme.onError)),
backgroundColor: colorScheme.error);
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class EnterFoodController extends ChangeNotifier {
String name = "";
String kcalPer100g = "";
void set(String newName, String newKcal) {
name = newName;
kcalPer100g = newKcal;
notifyListeners();
}
}

View File

@ -1,30 +1,16 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:calorimeter/food_scan/food_fact_lookup.dart';
import 'package:calorimeter/perdate/perdate_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ScanFoodFloatingButton extends StatelessWidget { class ScanFoodFloatingButton extends StatelessWidget {
const ScanFoodFloatingButton({ final Function() onPressed;
super.key, const ScanFoodFloatingButton({super.key, required this.onPressed});
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FloatingActionButton( return FloatingActionButton(
heroTag: "scanFoodFAB", heroTag: "scanFoodFAB",
child: const Icon(Icons.barcode_reader), child: const Icon(Icons.barcode_reader),
onPressed: () async { onPressed: () {
var client = FoodFactLookupClient(); onPressed();
var scanResult = await BarcodeScanner.scan();
var food = await client.retrieveFoodInfo(scanResult.rawContent);
if (!context.mounted) return;
context
.read<EnterFoodController>()
.set(food.name, food.kcalPer100g.toString());
}, },
); );
} }

View File

@ -2,37 +2,53 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calorimeter/food_entry/food_entry_bloc.dart'; import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
import 'package:calorimeter/utils/row_with_spacers_widget.dart';
class SumWidget extends StatefulWidget { class SumWidget extends StatelessWidget {
final List<FoodEntry> foodEntries; final List<FoodEntry> foodEntries;
const SumWidget({required this.foodEntries, super.key}); const SumWidget({required this.foodEntries, super.key});
@override
State<SumWidget> createState() => _SumWidgetState();
}
class _SumWidgetState extends State<SumWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SettingsDataBloc, SettingsState>( return BlocBuilder<SettingsDataBloc, SettingsState>(
builder: (context, state) { builder: (context, state) {
var sum = 0.0; var sum = 0.0;
for (var entry in widget.foodEntries) { for (var entry in foodEntries) {
sum += entry.kcalPerMass / 100 * entry.mass; sum += entry.kcalPerMass / 100 * entry.mass;
} }
var diff = state.kcalLimit - sum;
var diffLimit = state.kcalLimit ~/ 4;
return RowWidget( var textColor = Theme.of(context).colorScheme.onSecondary;
Text( var newTextColor = textColor;
var brightness = Theme.of(context).brightness;
switch (brightness) {
case Brightness.dark:
if (diff < 0) {
newTextColor = Colors.red[900]!;
} else if (diff < diffLimit) {
newTextColor = Colors.orange[900]!;
}
break;
case Brightness.light:
if (diff < 0) {
newTextColor = Colors.redAccent;
} else if (diff < diffLimit) {
newTextColor = Colors.orangeAccent;
}
break;
}
return Align(
alignment: Alignment.centerLeft,
child: Text(
'kcal heute: ${sum.ceil().toString()}/${state.kcalLimit.ceil()}', 'kcal heute: ${sum.ceil().toString()}/${state.kcalLimit.ceil()}',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyMedium! .bodyLarge!
.copyWith(color: Theme.of(context).colorScheme.onPrimary), .copyWith(color: newTextColor),
), ),
null,
null,
null,
); );
}, },
); );