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:
commit
a057115b83
@ -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';
|
||||||
|
@ -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});
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showNewSnackbarWith(BuildContext context, String text) {
|
||||||
|
var snackbar =
|
||||||
|
ErrorSnackbar(colorScheme: Theme.of(context).colorScheme, text: text);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(snackbar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EnterFoodController extends ChangeNotifier {
|
class ErrorSnackbar extends SnackBar {
|
||||||
String name = "";
|
final String text;
|
||||||
String kcalPer100g = "";
|
final ColorScheme colorScheme;
|
||||||
|
ErrorSnackbar({
|
||||||
void set(String newName, String newKcal) {
|
required this.text,
|
||||||
name = newName;
|
required this.colorScheme,
|
||||||
kcalPer100g = newKcal;
|
super.key,
|
||||||
notifyListeners();
|
}) : super(
|
||||||
}
|
content: Text(text, style: TextStyle(color: colorScheme.onError)),
|
||||||
|
backgroundColor: colorScheme.error);
|
||||||
}
|
}
|
||||||
|
12
lib/utils/enter_food_controller.dart
Normal file
12
lib/utils/enter_food_controller.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user