Make persistence work

This commit is contained in:
Marco 2024-06-09 19:06:10 +02:00
parent 2298c12704
commit d59f09eed4
11 changed files with 295 additions and 81 deletions

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:kalodings/calendar.dart';
class AppDrawer extends StatelessWidget { class AppDrawer extends StatelessWidget {
const AppDrawer({ const AppDrawer({
@ -27,7 +26,7 @@ class AppDrawer extends StatelessWidget {
title: const Text('Menü')), title: const Text('Menü')),
), ),
ListTile( ListTile(
trailing: const Icon(Icons.calendar_month), trailing: const Icon(Icons.today),
title: const Text('Kalender'), title: const Text('Kalender'),
onTap: () { onTap: () {
context.goNamed('calendar'); context.goNamed('calendar');

View File

@ -6,9 +6,9 @@ import 'package:kalodings/food_entry_bloc.dart';
import 'package:kalodings/row_with_spacers_widget.dart'; import 'package:kalodings/row_with_spacers_widget.dart';
class EnterFoodWidget extends StatefulWidget { class EnterFoodWidget extends StatefulWidget {
const EnterFoodWidget({ final Function(BuildContext context, FoodEntry entry) onAdd;
super.key,
}); const EnterFoodWidget({super.key, required this.onAdd});
@override @override
State<EnterFoodWidget> createState() => _EnterFoodWidgetState(); State<EnterFoodWidget> createState() => _EnterFoodWidgetState();
@ -80,22 +80,16 @@ class _EnterFoodWidgetState extends State<EnterFoodWidget> {
mass: massAsNumber, mass: massAsNumber,
kcalPerMass: kcalPerMassAsNumber); kcalPerMass: kcalPerMassAsNumber);
log('Entry id: ${entry.id}'); widget.onAdd(context, entry);
context.read<FoodEntryBloc>().add(FoodEntryEvent(entry: entry));
}, },
child: const Icon(Icons.add)); child: const Icon(Icons.add));
return Column( return RowWidgetWithSpacers(
children: [ nameWidget,
RowWidgetWithSpacers( massWidget,
nameWidget, kcalPerMassWidget,
massWidget, null,
kcalPerMassWidget, enterButton,
null,
enterButton,
),
],
); );
} }
} }

View File

@ -1,34 +1,61 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kalodings/storage/storage.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> { class FoodEntryBloc extends Bloc<FoodEvent, FoodEntryState> {
FoodEntryBloc(super.initialState) { final FoodStorage storage;
on<FoodEntryEvent>(addFoodEntryToState);
FoodEntryBloc(super.initialState, {required this.storage}) {
on<FoodEntryEvent>(addFoodEntry);
on<FoodDeletionEvent>(deleteFood); on<FoodDeletionEvent>(deleteFood);
on<PageChangedEvent>(updateEntries);
} }
void addFoodEntryToState(FoodEntryEvent event, Emitter<FoodEntryState> emit) { void addFoodEntry(FoodEntryEvent event, Emitter<FoodEntryState> emit) async {
FoodEntryState newState = FoodEntryState.from(state); FoodEntryState newState = FoodEntryState.from(state);
newState.addEntry(event.entry); newState.addEntry(event.entry);
await storage.writeEntriesForDate(event.date, newState.foodEntries);
emit(newState); emit(newState);
} }
void deleteFood(FoodDeletionEvent event, Emitter<FoodEntryState> emit) { void deleteFood(FoodDeletionEvent event, Emitter<FoodEntryState> emit) async {
state.foodEntries.removeWhere((entry) => entry.id == event.entryID); state.foodEntries.removeWhere((entry) => entry.id == event.entryID);
await storage.writeEntriesForDate(event.date, state.foodEntries);
emit(FoodEntryState.from(state)); emit(FoodEntryState.from(state));
} }
void updateEntries(
PageChangedEvent event, Emitter<FoodEntryState> emit) async {
var entries = await storage.getEntriesForDate(event.changedToDate);
var newState = FoodEntryState(foodEntries: entries);
emit(newState);
}
} }
class FoodEvent {} class FoodEvent {}
class FoodEntryEvent extends FoodEvent { class FoodEntryEvent extends FoodEvent {
final FoodEntry entry; final FoodEntry entry;
FoodEntryEvent({required this.entry}); final DateTime date;
FoodEntryEvent({required this.entry, required this.date});
} }
class FoodDeletionEvent extends FoodEvent { class FoodDeletionEvent extends FoodEvent {
final String entryID; final String entryID;
FoodDeletionEvent({required this.entryID}); final DateTime date;
FoodDeletionEvent({required this.entryID, required this.date});
}
class PageChangedEvent extends FoodEvent {
final DateTime changedToDate;
PageChangedEvent({required this.changedToDate});
} }
class FoodEntryState { class FoodEntryState {
@ -61,4 +88,9 @@ class FoodEntry {
required this.mass, required this.mass,
required this.kcalPerMass, required this.kcalPerMass,
}) : id = const Uuid().v1(); }) : id = const Uuid().v1();
@override
String toString() {
return '$id,$name,$mass,$kcalPerMass';
}
} }

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kalodings/food_entry_bloc.dart'; import 'package:kalodings/food_entry_bloc.dart';
import 'package:kalodings/row_with_spacers_widget.dart'; import 'package:kalodings/row_with_spacers_widget.dart';
class FoodEntryWidget extends StatelessWidget { class FoodEntryWidget extends StatelessWidget {
final FoodEntry entry; final FoodEntry entry;
final Function(BuildContext context) onDelete;
const FoodEntryWidget({super.key, required this.entry}); const FoodEntryWidget(
{super.key, required this.entry, required this.onDelete});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -16,13 +17,11 @@ class FoodEntryWidget extends StatelessWidget {
Text(entry.mass.toString()), Text(entry.mass.toString()),
Text(entry.kcalPerMass.toString()), Text(entry.kcalPerMass.toString()),
Text((entry.mass * entry.kcalPerMass / 100).toString()), Text((entry.mass * entry.kcalPerMass / 100).toString()),
IconButton( ElevatedButton(
onPressed: () { onPressed: () {
context onDelete(context);
.read<FoodEntryBloc>()
.add(FoodDeletionEvent(entryID: entry.id));
}, },
icon: const Icon(Icons.delete_forever_rounded)), child: const Icon(Icons.delete_forever_rounded)),
), ),
); );
} }

View File

@ -1,24 +1,26 @@
// ignore_for_file: prefer_const_constructors
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:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:kalodings/calendar.dart'; import 'package:kalodings/calendar.dart';
import 'package:kalodings/food_entry_bloc.dart'; import 'package:kalodings/food_entry_bloc.dart';
import 'package:kalodings/perday_widget.dart'; import 'package:kalodings/perdate_widget.dart';
import 'package:kalodings/storage/storage.dart';
void main() { void main() async {
runApp(const MainApp()); var storage = await FoodStorage.create();
runApp(MainApp(storage: storage));
} }
class MainApp extends StatelessWidget { class MainApp extends StatelessWidget {
const MainApp({super.key}); final FoodStorage storage;
const MainApp({required this.storage, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (BuildContext context) { create: (BuildContext context) {
return FoodEntryBloc(FoodEntryState.init()); return FoodEntryBloc(FoodEntryState.init(), storage: storage);
}, },
child: MaterialApp.router( child: MaterialApp.router(
theme: ThemeData.dark(), theme: ThemeData.dark(),
@ -33,7 +35,7 @@ final router = GoRouter(routes: [
path: '/', path: '/',
name: 'perDayToday', name: 'perDayToday',
builder: (context, state) { builder: (context, state) {
return PerDay(DateTime.now()); return PerDateWidget(DateTime.now());
}), }),
GoRoute( GoRoute(
path: '/day', path: '/day',
@ -46,12 +48,12 @@ final router = GoRouter(routes: [
day = state.extra as DateTime; day = state.extra as DateTime;
} }
return PerDay(day); return PerDateWidget(day);
}), }),
GoRoute( GoRoute(
path: '/calendar', path: '/calendar',
name: 'calendar', name: 'calendar',
builder: (context, state) { builder: (context, state) {
return CalendarWidget(); return const CalendarWidget();
}), }),
]); ]);

76
lib/perdate_widget.dart Normal file
View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:kalodings/app_drawer.dart';
import 'package:kalodings/enter_food_widget.dart';
import 'package:kalodings/food_entry_bloc.dart';
import 'package:kalodings/food_entry_widget.dart';
import 'package:kalodings/sum_widget.dart';
class PerDateWidget extends StatefulWidget {
final DateTime date;
const PerDateWidget(this.date, {super.key});
@override
State<PerDateWidget> createState() => _PerDateWidgetState();
}
class _PerDateWidgetState extends State<PerDateWidget> {
@override
void initState() {
super.initState();
context
.read<FoodEntryBloc>()
.add(PageChangedEvent(changedToDate: widget.date));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.date.toString()),
),
drawer: const AppDrawer(),
body: BlocBuilder<FoodEntryBloc, FoodEntryState>(
builder: (context, state) {
return ListView.builder(
itemCount: state.foodEntries.length + 2,
itemBuilder: (BuildContext itemBuilderContext, int index) {
if (index == state.foodEntries.length) {
return const SumWidget();
}
if (index == state.foodEntries.length + 1) {
return EnterFoodWidget(
onAdd: (context, entry) {
context
.read<FoodEntryBloc>()
.add(FoodEntryEvent(entry: entry, date: widget.date));
},
);
}
return FoodEntryWidget(
entry: state.foodEntries[index],
onDelete: (callbackContext) {
callbackContext.read<FoodEntryBloc>().add(
FoodDeletionEvent(
entryID: state.foodEntries[index].id,
date: widget.date),
);
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.goNamed('calendar');
},
child: const Icon(Icons.today)),
);
}
void deleteCallback(BuildContext context, String idToDelete, DateTime date) {}
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kalodings/app_drawer.dart';
import 'package:kalodings/enter_food_widget.dart';
import 'package:kalodings/food_entry_bloc.dart';
import 'package:kalodings/food_entry_widget.dart';
import 'package:kalodings/sum_widget.dart';
class PerDay extends StatelessWidget {
final DateTime day;
const PerDay(this.day, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(day.toString()),
),
drawer: const AppDrawer(),
body: BlocBuilder<FoodEntryBloc, FoodEntryState>(
builder: (context, state) {
return ListView.builder(
itemCount: state.foodEntries.length + 2,
itemBuilder: (BuildContext itemBuilderContext, int index) {
if (index == state.foodEntries.length) {
return const SumWidget();
}
if (index == state.foodEntries.length + 1) {
return const EnterFoodWidget();
}
return FoodEntryWidget(entry: state.foodEntries[index]);
},
);
},
));
}
}

59
lib/storage/storage.dart Normal file
View File

@ -0,0 +1,59 @@
import 'dart:io';
import 'package:kalodings/food_entry_bloc.dart';
import 'package:path_provider/path_provider.dart';
class FoodStorage {
late String path;
FoodStorage._create();
static Future<FoodStorage> create() async {
var storage = FoodStorage._create();
var directory = await getApplicationCacheDirectory();
storage.path = directory.path;
return storage;
}
Future<List<FoodEntry>> getEntriesForDate(DateTime date) async {
List<FoodEntry> entries = [];
var filePath = '$path/${date.year}/${date.month}/${date.day}';
var file = File(filePath);
var exists = await file.exists();
if (!exists) return [];
var lines = await file.readAsLines();
for (var line in lines) {
var fields = line.split(',');
var entry = FoodEntry(
name: fields[1],
mass: double.parse(fields[2]),
kcalPerMass: double.parse(fields[3]));
entries.add(entry);
}
return entries;
}
Future<void> writeEntriesForDate(
DateTime date, List<FoodEntry> foodEntries) async {
var filePath = '$path/${date.year}/${date.month}/${date.day}';
var file = File(filePath);
var exists = await file.exists();
if (!exists) {
await file.create(recursive: true);
}
String fullString = '';
for (var entry in foodEntries) {
fullString += '${entry.toString()}\n';
}
await file.writeAsString(fullString);
}
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
} }

View File

@ -65,6 +65,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -192,6 +200,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
url: "https://pub.dev"
source: hosted
version: "2.2.5"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -301,6 +373,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.2" version: "14.2.2"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sdks: sdks:
dart: ">=3.3.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.22.0"

View File

@ -11,6 +11,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_bloc: ^8.1.5 flutter_bloc: ^8.1.5
go_router: ^14.1.4 go_router: ^14.1.4
path_provider: ^2.1.3
quiver: ^3.2.1 quiver: ^3.2.1
uuid: ^4.4.0 uuid: ^4.4.0