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:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
final PageState initialState;
class FoodEntryBloc extends Bloc<FoodEvent, GlobalEntryState> {
final GlobalEntryState initialState;
final FoodStorage storage;
final DateTime forDate;
FoodEntryBloc(
{required this.initialState,
required this.forDate,
required this.storage})
FoodEntryBloc({required this.initialState, required this.storage})
: super(initialState) {
on<PageBeingInitialized>(handlePageBeingInitialized);
on<FoodEntryEvent>(handleFoodEntryEvent);
on<FoodChangedEvent>(handleFoodChangedEvent);
on<FoodDeletionEvent>(handleDeleteFoodEvent);
on<BarcodeScanned>(handleBarcodeScannedEvent);
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(
FoodEntryEvent event, Emitter<PageState> emit) async {
PageState newState = PageState.from(state);
newState.addEntry(event.entry);
FoodEntryEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
entriesForDate ??= [];
await storage.writeEntriesForDate(forDate, newState.foodEntries);
entriesForDate.add(event.entry);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
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(
FoodChangedEvent event, Emitter<PageState> emit) async {
var entries = state.foodEntries;
var index = entries.indexWhere((entry) {
FoodChangedEvent event, Emitter<GlobalEntryState> emit) async {
var entriesForDate = state.foodEntries[event.forDate];
if (entriesForDate == null) return;
var index = entriesForDate.indexWhere((entry) {
return entry.id == event.newEntry.id;
});
entries.removeAt(index);
entries.insert(index, event.newEntry);
entriesForDate.removeAt(index);
entriesForDate.insert(index, event.newEntry);
await storage.writeEntriesForDate(forDate, entries);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
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(
FoodDeletionEvent event, Emitter<PageState> emit) async {
state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
FoodDeletionEvent event, Emitter<GlobalEntryState> emit) async {
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(
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 scanResult = await event.scanResultFuture;
@ -66,14 +101,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
return;
}
if (scanResult.type == ResultType.Error) {
emit(PageState(
emit(GlobalEntryState(
foodEntries: state.foodEntries,
errorString: "Fehler beim Scannen des Barcodes"));
return;
}
var responseFuture = client.retrieveFoodInfo(scanResult.rawContent);
List<FoodEntryState> newList = List.from(state.foodEntries);
var newEntryWaiting = FoodEntryState(
kcalPer100: 0,
name: "",
@ -81,11 +115,13 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
waitingForNetwork: true,
isSelected: false,
);
newList.add(newEntryWaiting);
emit(PageState(foodEntries: newList));
entriesForDate.add(newEntryWaiting);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(GlobalEntryState(foodEntries: newFoodEntries));
await responseFuture.then((response) async {
var index = newList
var index = entriesForDate
.indexWhere((entryState) => entryState.id == newEntryWaiting.id);
// element not found (was deleted previously)
@ -94,25 +130,23 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
}
if (response.status == FoodFactResponseStatus.barcodeNotFound) {
List<FoodEntryState> listWithEntryRemoved =
List.from(state.foodEntries);
listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(PageState(
foodEntries: listWithEntryRemoved,
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "Barcode konnte nicht gefunden werden."));
return;
}
if (response.status ==
FoodFactResponseStatus.foodFactServerNotReachable) {
List<FoodEntryState> listWithEntryRemoved =
List.from(state.foodEntries);
listWithEntryRemoved
.removeWhere((entry) => entry.id == newEntryWaiting.id);
entriesForDate.removeWhere((entry) => entry.id == newEntryWaiting.id);
var newFoodEntries = state.foodEntries;
newFoodEntries.addAll({event.forDate: entriesForDate});
emit(PageState(
foodEntries: listWithEntryRemoved,
emit(GlobalEntryState(
foodEntries: newFoodEntries,
errorString: "OpenFoodFacts-Server konnte nicht erreicht werden."));
return;
}
@ -125,91 +159,114 @@ class FoodEntryBloc extends Bloc<FoodEvent, PageState> {
isSelected: false,
);
newList.removeAt(index);
newList.insert(index, newEntryFinishedWaiting);
entriesForDate.removeAt(index);
entriesForDate.insert(index, newEntryFinishedWaiting);
await storage.writeEntriesForDate(forDate, newList);
await storage.writeEntriesForDate(event.forDate, entriesForDate);
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(
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;
for (var entry in state.foodEntries) {
for (var entry in entriesForDate) {
entry.isSelected = false;
}
var selectedEntry = state.foodEntries.firstWhere((entry) {
var selectedEntry = entriesForDate.firstWhere((entry) {
return entry.id == event.entry.id;
});
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 {
final FoodEntryState entry;
FoodEntryEvent({required this.entry});
FoodEntryEvent({required this.entry, required super.forDate});
}
class FoodChangedEvent extends FoodEvent {
final FoodEntryState newEntry;
FoodChangedEvent({required this.newEntry});
FoodChangedEvent({required this.newEntry, required super.forDate});
}
class FoodDeletionEvent extends FoodEvent {
final String entryID;
FoodDeletionEvent({required this.entryID});
FoodDeletionEvent({required this.entryID, required super.forDate});
}
class BarcodeScanned extends FoodEvent {
final Future<ScanResult> scanResultFuture;
BarcodeScanned({required this.scanResultFuture});
BarcodeScanned({required this.scanResultFuture, required super.forDate});
}
class FoodEntryTapped extends FoodEvent {
final FoodEntryState entry;
FoodEntryTapped({required this.entry});
FoodEntryTapped({required this.entry, required super.forDate});
}
/// This is the state for one date/page
class PageState {
final List<FoodEntryState> foodEntries;
class GlobalEntryState {
final Map<DateTime, List<FoodEntryState>> foodEntries;
final String? errorString;
PageState({required this.foodEntries, this.errorString});
GlobalEntryState({required this.foodEntries, this.errorString});
factory PageState.init() {
return PageState(foodEntries: []);
factory GlobalEntryState.init() {
return GlobalEntryState(foodEntries: {});
}
static from(PageState state) {
return PageState(foodEntries: state.foodEntries);
static from(GlobalEntryState state) {
return GlobalEntryState(foodEntries: state.foodEntries);
}
void addEntry(FoodEntryState entry) {
foodEntries.add(entry);
bool addEntry(FoodEntryState entry, DateTime date) {
var list = foodEntries[date];
if (list == null) {
return false;
}
list.add(entry);
return true;
}
}
class FoodEntryState {
final String id;
final String name;
final int mass;
final int kcalPer100;
final String id;
final bool waitingForNetwork;
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/storage/storage.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:go_router/go_router.dart';
List<FoodEntryState> entriesForToday = [];
DateTime timeNow = DateTime.now();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -14,6 +18,16 @@ void main() async {
var storage = await FoodStorage.create();
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 brightness = await storage.readBrightness();
@ -42,6 +56,13 @@ class MainApp extends StatelessWidget {
return SafeArea(
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => FoodEntryBloc(
storage: storage,
initialState:
GlobalEntryState(foodEntries: {timeNow: entriesForToday}),
),
),
BlocProvider(
create: (context) => SettingsDataBloc(
SettingsState(kcalLimit: kcalLimit),

View File

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

View File

@ -61,7 +61,7 @@ class _PerDatePageviewState extends State<PerDatePageview> {
return PerDateWidget(
key: ValueKey(dateToBuildWidgetFor.toString()),
date: dateToBuildWidgetFor,
date: dateToBuildWidgetFor.copyWith(isUtc: true),
onDateSelected: (dateSelected) {
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_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class PerDateWidget extends StatefulWidget {
final DateTime date;
@ -26,16 +25,13 @@ class PerDateWidget extends StatefulWidget {
class _PerDateWidgetState extends State<PerDateWidget>
with AutomaticKeepAliveClientMixin<PerDateWidget> {
late FoodStorage storage;
late Future<List<FoodEntryState>> entriesFuture;
List<FoodEntryState> entries = [];
@override
void initState() {
storage = FoodStorage.getInstance();
entriesFuture = storage.getEntriesForDate(widget.date);
entriesFuture.then((val) {
entries = val;
});
context
.read<FoodEntryBloc>()
.add(PageBeingInitialized(forDate: widget.date));
super.initState();
}
@ -48,63 +44,51 @@ class _PerDateWidgetState extends State<PerDateWidget>
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
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) {
if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!);
}
},
builder: (context, pageState) {
return Scaffold(
appBar: AppBar(
title: Text(
DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()],
),
body: FoodEntryList(entries: pageState.foodEntries),
bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary,
child: SumWidget(
foodEntries: pageState.foodEntries)),
drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [
ScanFoodFloatingButton(
onPressed: () {
var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add(
BarcodeScanned(scanResultFuture: result));
},
),
const SizedBox(width: 8),
CalendarFloatingButton(
startFromDate: widget.date,
onDateSelected: (dateSelected) {
widget.onDateSelected(dateSelected);
},
),
]),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked);
},
),
);
});
return BlocConsumer<FoodEntryBloc, GlobalEntryState>(
listener: (context, pageState) {
if (pageState.errorString != null) {
showNewSnackbarWith(context, pageState.errorString!);
}
},
builder: (context, globalState) {
return Scaffold(
appBar: AppBar(
title: Text(DateFormat.yMMMMd('de').format(widget.date)),
actions: const [ThemeSwitcherButton()],
),
body: FoodEntryList(
entries: globalState.foodEntries[widget.date] ?? [],
date: widget.date),
bottomNavigationBar: BottomAppBar(
shape: const RectangularNotchShape(),
color: Theme.of(context).colorScheme.secondary,
child: SumWidget(
foodEntries: globalState.foodEntries[widget.date] ?? [])),
drawer: const AppDrawer(),
floatingActionButton: OverflowBar(children: [
ScanFoodFloatingButton(
onPressed: () {
var result = BarcodeScanner.scan();
context.read<FoodEntryBloc>().add(
BarcodeScanned(
scanResultFuture: result,
forDate: widget.date,
),
);
},
),
const SizedBox(width: 8),
CalendarFloatingButton(
startFromDate: widget.date,
onDateSelected: (dateSelected) {
widget.onDateSelected(dateSelected);
},
),
]),
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked);
},
);
}
void showNewSnackbarWith(BuildContext context, String text) {