Bloc is now global and responsible for all dates

This commit is contained in:
Marco 2024-10-08 00:58:56 +02:00
parent cb18e1d1f0
commit ce373404ad
5 changed files with 204 additions and 140 deletions

View File

@ -4,61 +4,96 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, PageState> { class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
final PageState initialState; final GlobalEntryState initialState;
final FoodStorage storage; final FoodStorage storage;
final DateTime forDate;
FoodEntryBloc( FoodEntryBloc({required this.initialState, required this.storage})
{required this.initialState,
required this.forDate,
required this.storage})
: super(initialState) { : super(initialState) {
on<PageBeingInitialized>(handlePageBeingInitialized);
on<FoodEntryEvent>(handleFoodEntryEvent); on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent); on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent); on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent); on<BarcodeScanned>(handleBarcodeScannedEvent);
on<FoodEntryTapped>(handleFoodEntryTapped); on<FoodEntryTapped>(handleFoodEntryTapped);
} }
void handlePageBeingInitialized(
PageBeingInitialized event, Emitter<GlobalEntryState> emit) async {
var newList = await storage.getEntriesForDate(event.forDate);
state.foodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: state.foodEntries));
}
void handleFoodEntryEvent( void handleFoodEntryEvent(
FoodEntryEvent event, Emitter<PageState> emit) async { FoodEntryEvent event, Emitter<GlobalEntryState> emit) async {
PageState newState = PageState.from(state); var entriesForDate = state.foodEntries[event.forDate];
newState.addEntry(event.entry); entriesForDate ??= [];
await storage.writeEntriesForDate(forDate, newState.foodEntries); entriesForDate.add(event.entry);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.entry); storage.addFoodEntryToLookupDatabase(event.entry);
emit(newState); // this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleFoodChangedEvent( void handleFoodChangedEvent(
FoodChangedEvent event, Emitter<PageState> emit) async { FoodChangedEvent event, Emitter<GlobalEntryState> emit) async {
var entries = state.foodEntries; var entriesForDate = state.foodEntries[event.forDate];
var index = entries.indexWhere((entry) { if (entriesForDate == null) return;
var index = entriesForDate.indexWhere((entry) {
return entry.id == event.newEntry.id; return entry.id == event.newEntry.id;
}); });
entries.removeAt(index); entriesForDate.removeAt(index);
entries.insert(index, event.newEntry); entriesForDate.insert(index, event.newEntry);
await storage.writeEntriesForDate(forDate, entries); await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(event.newEntry); storage.addFoodEntryToLookupDatabase(event.newEntry);
emit(PageState(foodEntries: entries)); // this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleDeleteFoodEvent( void handleDeleteFoodEvent(
FoodDeletionEvent event, Emitter<PageState> emit) async { FoodDeletionEvent event, Emitter<GlobalEntryState> emit) async {
state.foodEntries.removeWhere((entry) => entry.id == event.entryID); var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
await storage.writeEntriesForDate(forDate, state.foodEntries); entriesForDate.removeWhere((entry) => entry.id == event.entryID);
emit(PageState.from(state)); await storage.writeEntriesForDate(event.forDate, entriesForDate);
// this is just checking if writing to the database worked
// can be optimized out by just emitting newState
var newList = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: newList});
emit(GlobalEntryState(foodEntries: newFoodEntries));
} }
void handleBarcodeScannedEvent( void handleBarcodeScannedEvent(
BarcodeScanned event, Emitter<PageState> emit) async { BarcodeScanned event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var client = FoodFactLookupClient(); var client = FoodFactLookupClient();
var scanResult = await event.scanResultFuture; var scanResult = await event.scanResultFuture;
@ -66,14 +101,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
return; return;
} }
if (scanResult.type == ResultType.Error) { if (scanResult.type == ResultType.Error) {
emit(PageState( emit(GlobalEntryState(
foodEntries: state.foodEntries, foodEntries: state.foodEntries,
errorString: "Fehler beim Scannen des Barcodes")); errorString: "Fehler beim Scannen des Barcodes"));
return; return;
} }
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent); var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
List<FoodEntryState> newList = List.from(state.foodEntries);
var newEntryWaiting = FoodEntryState( var newEntryWaiting = FoodEntryState(
kcalPer100: 0, kcalPer100: 0,
name: "", name: "",
@ -81,11 +115,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
waitingForNetwork: true, waitingForNetwork: true,
isSelected: false, isSelected: false,
); );
newList.add(newEntryWaiting); entriesForDate.add(newEntryWaiting);
emit(PageState(foodEntries: newList)); var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async { await responseFuture.then((response) async {
var index = newList var index = entriesForDate
.indexWhere((entryState) => entryState.id == newEntryWaiting.id); .indexWhere((entryState) => entryState.id == newEntryWaiting.id);
// element not found (was deleted previously) // element not found (was deleted previously)
@ -94,25 +130,23 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
} }
if (response.status == FoodFactResponseStatus.barcodeNotFound) { if (response.status == FoodFactResponseStatus.barcodeNotFound) {
List<FoodEntryState> listWithEntryRemoved = entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
List.from(state.foodEntries); var newFoodEntries = state.foodEntries;
listWithEntryRemoved newFoodEntries.addAll({event.forDate: entriesForDate});
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(PageState( emit(GlobalEntryState(
foodEntries: listWithEntryRemoved, foodEntries: newFoodEntries,
errorString: "Barcode konnte nicht gefunden werden.")); errorString: "Barcode konnte nicht gefunden werden."));
return; return;
} }
if (response.status == if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) { FoodFactResponseStatus.foodFactServerNotReachable) {
List<FoodEntryState> listWithEntryRemoved = entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
List.from(state.foodEntries); var newFoodEntries = state.foodEntries;
listWithEntryRemoved newFoodEntries.addAll({event.forDate: entriesForDate});
.removeWhere((entry) => entry.id == newEntryWaiting.id);
emit(PageState( emit(GlobalEntryState(
foodEntries: listWithEntryRemoved, foodEntries: newFoodEntries,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden.")); errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
return; return;
} }
@ -125,91 +159,114 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
isSelected: false, isSelected: false,
); );
newList.removeAt(index); entriesForDate.removeAt(index);
newList.insert(index, newEntryFinishedWaiting); entriesForDate.insert(index, newEntryFinishedWaiting);
await storage.writeEntriesForDate(forDate, newList); await storage.writeEntriesForDate(event.forDate, entriesForDate);
storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting); storage.addFoodEntryToLookupDatabase(newEntryFinishedWaiting);
emit(PageState(foodEntries: newList)); var entriesFromStorage = await storage.getEntriesForDate(event.forDate);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesFromStorage});
emit(GlobalEntryState(foodEntries: newFoodEntries));
}); });
} }
void handleFoodEntryTapped( void handleFoodEntryTapped(
FoodEntryTapped event, Emitter<PageState> emit) async { FoodEntryTapped event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var oldStateOfTappedEntry = event.entry.isSelected; var oldStateOfTappedEntry = event.entry.isSelected;
for (var entry in state.foodEntries) { for (var entry in entriesForDate) {
entry.isSelected = false; entry.isSelected = false;
} }
var selectedEntry = state.foodEntries.firstWhere((entry) { var selectedEntry = entriesForDate.firstWhere((entry) {
return entry.id == event.entry.id; return entry.id == event.entry.id;
}); });
selectedEntry.isSelected = !oldStateOfTappedEntry; selectedEntry.isSelected = !oldStateOfTappedEntry;
emit(PageState(foodEntries: state.foodEntries)); emit(GlobalEntryState(foodEntries: state.foodEntries));
} }
} }
class FoodEvent {} class FoodEvent {
final DateTime forDate;
FoodEvent({required this.forDate});
}
class PageBeingInitialized extends FoodEvent {
PageBeingInitialized({required super.forDate});
}
class FoodEntryEvent extends FoodEvent { class FoodEntryEvent extends FoodEvent {
final FoodEntryState entry; final FoodEntryState entry;
FoodEntryEvent({required this.entry}); FoodEntryEvent({required this.entry, required super.forDate});
} }
class FoodChangedEvent extends FoodEvent { class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry; final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry}); FoodChangedEvent({required this.newEntry, required super.forDate});
} }
class FoodDeletionEvent extends FoodEvent { class FoodDeletionEvent extends FoodEvent {
final String entryID; final String entryID;
FoodDeletionEvent({required this.entryID}); FoodDeletionEvent({required this.entryID, required super.forDate});
} }
class BarcodeScanned extends FoodEvent { class BarcodeScanned extends FoodEvent {
final Future<ScanResult> scanResultFuture; final Future<ScanResult> scanResultFuture;
BarcodeScanned({required this.scanResultFuture}); BarcodeScanned({required this.scanResultFuture, required super.forDate});
} }
class FoodEntryTapped extends FoodEvent { class FoodEntryTapped extends FoodEvent {
final FoodEntryState entry; final FoodEntryState entry;
FoodEntryTapped({required this.entry}); FoodEntryTapped({required this.entry, required super.forDate});
} }
/// This is the state for one date/page /// This is the state for one date/page
class PageState { class GlobalEntryState {
final List<FoodEntryState> foodEntries; final Map<DateTime, List<FoodEntryState>> foodEntries;
final String? errorString; final String? errorString;
PageState({required this.foodEntries, this.errorString}); GlobalEntryState({required this.foodEntries, this.errorString});
factory PageState.init() { factory GlobalEntryState.init() {
return PageState(foodEntries: []); return GlobalEntryState(foodEntries: {});
} }
static from(PageState state) { static from(GlobalEntryState state) {
return PageState(foodEntries: state.foodEntries); return GlobalEntryState(foodEntries: state.foodEntries);
} }
void addEntry(FoodEntryState entry) { bool addEntry(FoodEntryState entry, DateTime date) {
foodEntries.add(entry); var list = foodEntries[date];
if (list == null) {
return false;
}
list.add(entry);
return true;
} }
} }
class FoodEntryState { class FoodEntryState {
final String id;
final String name; final String name;
final int mass; final int mass;
final int kcalPer100; final int kcalPer100;
final String id;
final bool waitingForNetwork; final bool waitingForNetwork;
bool isSelected; bool isSelected;

View File

@ -1,3 +1,4 @@
import 'package:calorimeter/food_entry/food_entry_bloc.dart';
import 'package:calorimeter/perdate/perdate_pageview.dart'; import 'package:calorimeter/perdate/perdate_pageview.dart';
import 'package:calorimeter/storage/storage.dart'; import 'package:calorimeter/storage/storage.dart';
import 'package:calorimeter/utils/settings_bloc.dart'; import 'package:calorimeter/utils/settings_bloc.dart';
@ -7,6 +8,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -14,6 +18,16 @@ void main() async {
var storage = await FoodStorage.create(); var storage = await FoodStorage.create();
await storage.buildFoodLookupDatabase(); await storage.buildFoodLookupDatabase();
timeNow = DateTime.now().copyWith(
hour: 0,
isUtc: true,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0);
entriesForToday = await storage.getEntriesForDate(timeNow);
var kcalLimit = await storage.readLimit(); var kcalLimit = await storage.readLimit();
var brightness = await storage.readBrightness(); var brightness = await storage.readBrightness();
@ -42,6 +56,13 @@ class MainApp extends StatelessWidget {
return SafeArea( return SafeArea(
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
storage: storage,
initialState:
GlobalEntryState(foodEntries: {timeNow: entriesForToday}),
),
),
BlocProvider( BlocProvider(
create: (context) => SettingsDataBloc( create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit), SettingsState(kcalLimit: kcalLimit),

View File

@ -6,9 +6,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class FoodEntryList extends StatelessWidget { class FoodEntryList extends StatelessWidget {
final List<FoodEntryState> entries; final List<FoodEntryState> entries;
final DateTime date;
const FoodEntryList({ const FoodEntryList({
required this.entries, required this.entries,
required this.date,
super.key, super.key,
}); });
@ -25,7 +27,7 @@ class FoodEntryList extends StatelessWidget {
onAdd: (context, entry) { onAdd: (context, entry) {
context context
.read<FoodEntryBloc>() .read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry)); .add(FoodEntryEvent(entry: entry, forDate: date));
}, },
), ),
const SizedBox(height: 75), const SizedBox(height: 75),
@ -40,19 +42,19 @@ class FoodEntryList extends StatelessWidget {
key: ValueKey(entries[entryIndex].id), key: ValueKey(entries[entryIndex].id),
entry: entries[entryIndex], entry: entries[entryIndex],
onDelete: (_, id) { onDelete: (_, id) {
context.read<FoodEntryBloc>().add(FoodDeletionEvent( context
entryID: id, .read<FoodEntryBloc>()
)); .add(FoodDeletionEvent(entryID: id, forDate: date));
}, },
onChange: (_, changedEntry) { onChange: (_, changedEntry) {
context context.read<FoodEntryBloc>().add(
.read<FoodEntryBloc>() FoodChangedEvent(newEntry: changedEntry, forDate: date),
.add(FoodChangedEvent(newEntry: changedEntry)); );
}, },
onTap: (_, tappedEntry) { onTap: (_, tappedEntry) {
context context.read<FoodEntryBloc>().add(
.read<FoodEntryBloc>() FoodEntryTapped(entry: tappedEntry, forDate: date),
.add(FoodEntryTapped(entry: tappedEntry)); );
}, },
), ),
const Divider(), const Divider(),

View File

@ -61,7 +61,7 @@ class _PerDatePageviewState extends State<PerDatePageview> {
return PerDateWidget( return PerDateWidget(
key: ValueKey(dateToBuildWidgetFor.toString()), key: ValueKey(dateToBuildWidgetFor.toString()),
date: dateToBuildWidgetFor, date: dateToBuildWidgetFor.copyWith(isUtc: true),
onDateSelected: (dateSelected) { onDateSelected: (dateSelected) {
if (dateSelected == null) return; if (dateSelected == null) return;

View File

@ -11,7 +11,6 @@ import 'package:calorimeter/utils/theme_switcher_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class PerDateWidget extends StatefulWidget { class PerDateWidget extends StatefulWidget {
final DateTime date; final DateTime date;
@ -26,16 +25,13 @@ class PerDateWidget extends StatefulWidget {
class _PerDateWidgetState extends State<PerDateWidget> class _PerDateWidgetState extends State<PerDateWidget>
with AutomaticKeepAliveClientMixin<PerDateWidget> { with AutomaticKeepAliveClientMixin<PerDateWidget> {
late FoodStorage storage; late FoodStorage storage;
late Future<List<FoodEntryState>> entriesFuture;
List<FoodEntryState> entries = []; List<FoodEntryState> entries = [];
@override @override
void initState() { void initState() {
storage = FoodStorage.getInstance(); context
entriesFuture = storage.getEntriesForDate(widget.date); .read<FoodEntryBloc>()
entriesFuture.then((val) { .add(PageBeingInitialized(forDate: widget.date));
entries = val;
});
super.initState(); super.initState();
} }
@ -48,47 +44,37 @@ class _PerDateWidgetState extends State<PerDateWidget>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return FutureBuilder( return BlocConsumer<FoodEntryBloc, GlobalEntryState>(
future: entriesFuture,
builder: (context, snapshot) {
return snapshot.connectionState != ConnectionState.done
? const Center(child: CircularProgressIndicator())
: MultiProvider(
providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
initialState: PageState(foodEntries: entries),
storage: storage,
forDate: widget.date,
),
)
],
child: BlocConsumer<FoodEntryBloc, PageState>(
listener: (context, pageState) { listener: (context, pageState) {
if (pageState.errorString != null) { if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!); showNewSnackbarWith(context, pageState.errorString!);
} }
}, },
builder: (context, pageState) { builder: (context, globalState) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(DateFormat.yMMMMd('de').format(widget.date)),
DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()], actions: const [ThemeSwitcherButton()],
), ),
body: FoodEntryList(entries: pageState.foodEntries), body: FoodEntryList(
entries: globalState.foodEntries[widget.date] ?? [],
date: widget.date),
bottomNavigationBar: BottomAppBar( bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(), shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
child: SumWidget( child: SumWidget(
foodEntries: pageState.foodEntries)), foodEntries: globalState.foodEntries[widget.date] ?? [])),
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [ floatingActionButton: OverflowBar(children: [
ScanFoodFloatingButton( ScanFoodFloatingButton(
onPressed: () { onPressed: () {
var result = BarcodeScanner.scan(); var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add( context.read<FoodEntryBloc>().add(
BarcodeScanned(scanResultFuture: result)); BarcodeScanned(
scanResultFuture: result,
forDate: widget.date,
),
);
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -102,9 +88,7 @@ class _PerDateWidgetState extends State<PerDateWidget>
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked); FloatingActionButtonLocation.endDocked);
}, },
),
); );
});
} }
void showNewSnackbarWith(BuildContext context, String text) { void showNewSnackbarWith(BuildContext context, String text) {