[storage] refactor to based and main storage [BasedScreen] store storage context as well (will probably create some main dao in future)

This commit is contained in:
Piotr Domański 2024-08-04 19:20:57 +02:00
parent 1100114040
commit 07c92443ce
16 changed files with 151 additions and 122 deletions

View file

@ -14,10 +14,12 @@ class ApiClient {
ApiClient({
required this.baseUrl,
}) {
() async {
await loadToken();
}();
});
static Future<ApiClient> create({required String baseUrl}) async {
var client = ApiClient(baseUrl: baseUrl);
client.loadToken();
return client;
}
Future<void> loadToken() async {

1
lib/client/based.dart Normal file
View file

@ -0,0 +1 @@
import 'package:fooder/client.dart';

View file

@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:fooder/screens/login.dart';
import 'package:fooder/client.dart';
import 'package:fooder/storage.dart';
import 'package:fooder/theme.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
final Storage storage;
final ApiClient apiClient;
const MyApp({required this.storage, required this.apiClient, super.key});
@override
Widget build(BuildContext context) {
@ -15,14 +19,18 @@ class MyApp extends StatelessWidget {
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
home: LoginScreen(
apiClient: ApiClient(
baseUrl: 'https://fooderapi.domandoman.xyz/api',
),
apiClient: apiClient,
storage: storage,
),
);
}
}
void main() {
runApp(const MyApp());
void main() async {
var storage = await Storage.create();
var apiClient = await ApiClient.create(
baseUrl: 'https://fooderapi.domandoman.xyz/api',
);
runApp(MyApp(storage: storage, apiClient: apiClient));
}

View file

@ -6,6 +6,7 @@ class Product {
final double carb;
final double fat;
final double fiber;
final int usageCountCached;
final String? barcode;
Product(
@ -16,6 +17,7 @@ class Product {
required this.carb,
required this.fat,
required this.fiber,
this.usageCountCached = 0,
this.barcode});
Product.fromJson(Map<String, dynamic> map)
@ -26,6 +28,7 @@ class Product {
carb = map['carb'] as double,
fat = map['fat'] as double,
fiber = map['fiber'] as double,
usageCountCached = map['usage_count_cached'] as int,
barcode = map['barcode'] as String?;
Map<String, Object?> toMap() {
@ -38,6 +41,7 @@ class Product {
'fat': fat,
'fiber': fiber,
'barcode': barcode,
'usage_count_cached': usageCountCached,
};
}
}

View file

@ -9,14 +9,16 @@ import 'package:fooder/components/text.dart';
import 'package:fooder/components/dropdown.dart';
import 'package:fooder/components/floating_action_button.dart';
import 'package:fooder/screens/add_product.dart';
import 'package:fooder/storage/base.dart';
import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
class AddEntryScreen extends BasedScreen {
final Diary diary;
const AddEntryScreen(
{super.key, required super.apiClient, required this.diary});
{super.key,
required super.apiClient,
required super.storage,
required this.diary});
@override
State<AddEntryScreen> createState() => _AddEntryScreen();
@ -27,6 +29,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
final productNameController = TextEditingController();
Meal? meal;
List<Product> products = [];
Diary get diary => widget.diary;
@override
void dispose() {
@ -45,35 +48,18 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
void initState() {
super.initState();
setState(() {
meal = widget.diary.meals[0];
meal = diary.meals[0];
});
_getProducts().then((value) => null);
}
Future<void> _getProducts() async {
if (storage != null) {
var storagePorducts = await storage!.product.list();
if (storagePorducts.length > 5) {
print("Using local storage");
setState(() {
products = storagePorducts;
});
return;
}
} else {
print("No local storage");
}
var productsMap =
await widget.apiClient.getProducts(productNameController.text);
var productsMap = await apiClient.getProducts(productNameController.text);
var parsedProducts = (productsMap['products'] as List<dynamic>)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
await storage!.product.bulkInsert(parsedProducts);
setState(() {
products = parsedProducts;
});
@ -113,7 +99,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
return;
}
await widget.apiClient.addEntry(
await apiClient.addEntry(
grams: grams,
productId: products[0].id,
mealId: meal!.id,
@ -131,7 +117,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
if (res is String) {
try {
var productMap = await widget.apiClient.getProductByBarcode(res);
var productMap = await apiClient.getProductByBarcode(res);
var product = Product.fromJson(productMap);
@ -168,7 +154,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
});
},
items: <DropdownMenuItem<Meal>>[
for (var meal in widget.diary.meals)
for (var meal in diary.meals)
DropdownMenuItem<Meal>(
value: meal,
child: Text(meal.name),
@ -199,7 +185,8 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
context,
MaterialPageRoute(
builder: (context) => AddProductScreen(
apiClient: widget.apiClient,
apiClient: apiClient,
storage: storage,
),
),
).then((product) {

View file

@ -10,7 +10,10 @@ class AddMealScreen extends BasedScreen {
final Diary diary;
const AddMealScreen(
{super.key, required super.apiClient, required this.diary});
{super.key,
required super.apiClient,
required super.storage,
required this.diary});
@override
State<AddMealScreen> createState() => _AddMealScreen();
@ -22,10 +25,10 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
bool nameChanged = false;
List<Preset> presets = [];
Preset? selectedPreset;
Diary get diary => widget.diary;
Future<void> _getPresets() async {
var presetsMap =
await widget.apiClient.getPresets(presetNameController.text);
var presetsMap = await apiClient.getPresets(presetNameController.text);
setState(() {
presets = (presetsMap['presets'] as List<dynamic>)
@ -39,7 +42,7 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
void initState() {
super.initState();
setState(() {
nameController.text = "Meal ${widget.diary.meals.length + 1}";
nameController.text = "Meal ${diary.meals.length + 1}";
});
_getPresets();
}
@ -57,15 +60,15 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
}
Future<void> _addMeal() async {
await widget.apiClient.addMeal(
await apiClient.addMeal(
name: nameController.text,
diaryId: widget.diary.id,
diaryId: diary.id,
);
popMeDaddy();
}
Future<void> _deletePreset(Preset preset) async {
widget.apiClient.deletePreset(preset.id);
apiClient.deletePreset(preset.id);
setState(() {
presets.remove(preset);
});
@ -103,9 +106,9 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
return;
}
await widget.apiClient.addMealFromPreset(
await apiClient.addMealFromPreset(
name: nameChanged ? nameController.text : selectedPreset!.name,
diaryId: widget.diary.id,
diaryId: diary.id,
presetId: selectedPreset!.id,
);
popMeDaddy();

View file

@ -7,7 +7,8 @@ import 'package:fooder/components/text.dart';
import 'package:fooder/components/floating_action_button.dart';
class AddProductScreen extends BasedScreen {
const AddProductScreen({super.key, required super.apiClient});
const AddProductScreen(
{super.key, required super.apiClient, required super.storage});
@override
State<AddProductScreen> createState() => _AddProductScreen();
@ -61,7 +62,7 @@ class _AddProductScreen extends BasedState<AddProductScreen> {
}
try {
var productJson = await widget.apiClient.addProduct(
var productJson = await apiClient.addProduct(
carb: carb,
fat: fat,
protein: protein,

View file

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:fooder/client.dart';
import 'package:fooder/storage.dart';
import 'package:fooder/components/app_bar.dart';
import 'package:fooder/components/navigation_bar.dart';
import 'package:fooder/screens/login.dart';
import 'package:fooder/screens/main.dart';
import 'package:fooder/storage/base.dart';
TextStyle logoStyle(context) {
return Theme.of(context).textTheme.labelLarge!.copyWith(
@ -14,34 +14,27 @@ TextStyle logoStyle(context) {
abstract class BasedScreen extends StatefulWidget {
final ApiClient apiClient;
final Storage? storage;
final Storage storage;
const BasedScreen({super.key, required this.apiClient, this.storage});
const BasedScreen(
{super.key, required this.apiClient, required this.storage});
}
abstract class BasedState<T extends BasedScreen> extends State<T> {
Storage? storage;
ApiClient get apiClient => widget.apiClient;
Storage get storage => widget.storage;
@override
void initState() {
super.initState();
_asyncInitState().then((value) => null);
void logout() async {
await apiClient.logout();
backToLogin();
}
Future<void> _asyncInitState() async {
if (widget.storage != null) {
this.storage = widget.storage;
} else {
this.storage = await Storage.create();
}
}
void _logout() async {
await widget.apiClient.logout();
void backToLogin() {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LoginScreen(apiClient: widget.apiClient),
builder: (context) =>
LoginScreen(apiClient: apiClient, storage: storage),
),
);
}
@ -50,7 +43,8 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MainScreen(apiClient: widget.apiClient),
builder: (context) =>
MainScreen(apiClient: apiClient, storage: storage),
),
);
}
@ -63,7 +57,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
Icons.logout,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: _logout,
onPressed: logout,
),
],
);

View file

@ -13,7 +13,10 @@ class EditEntryScreen extends BasedScreen {
final Entry entry;
const EditEntryScreen(
{super.key, required super.apiClient, required this.entry});
{super.key,
required super.apiClient,
required super.storage,
required this.entry});
@override
State<EditEntryScreen> createState() => _EditEntryScreen();
@ -23,6 +26,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
final gramsController = TextEditingController();
final productNameController = TextEditingController();
List<Product> products = [];
Entry get entry => entry;
@override
void dispose() {
@ -39,15 +43,14 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
void initState() {
super.initState();
setState(() {
gramsController.text = widget.entry.grams.toString();
productNameController.text = widget.entry.product.name;
products = [widget.entry.product];
gramsController.text = entry.grams.toString();
productNameController.text = entry.product.name;
products = [entry.product];
});
}
Future<void> _getProducts() async {
var productsMap =
await widget.apiClient.getProducts(productNameController.text);
var productsMap = await apiClient.getProducts(productNameController.text);
setState(() {
products = (productsMap['products'] as List<dynamic>)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
@ -75,17 +78,17 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
return;
}
await widget.apiClient.updateEntry(
widget.entry.id,
await apiClient.updateEntry(
entry.id,
grams: grams,
productId: products[0].id,
mealId: widget.entry.mealId,
mealId: entry.mealId,
);
popMeDaddy();
}
Future<void> _deleteEntry() async {
await widget.apiClient.deleteEntry(widget.entry.id);
await apiClient.deleteEntry(widget.entry.id);
popMeDaddy();
}
@ -99,7 +102,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
if (res is String) {
try {
var productMap = await widget.apiClient.getProductByBarcode(res);
var productMap = await apiClient.getProductByBarcode(res);
var product = Product.fromJson(productMap);
@ -144,7 +147,8 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
context,
MaterialPageRoute(
builder: (context) => AddProductScreen(
apiClient: widget.apiClient,
apiClient: apiClient,
storage: storage,
),
),
).then((product) {

View file

@ -7,7 +7,8 @@ import 'package:fooder/components/text.dart';
import 'package:fooder/components/button.dart';
class LoginScreen extends BasedScreen {
const LoginScreen({super.key, required super.apiClient});
const LoginScreen(
{super.key, required super.apiClient, required super.storage});
@override
State<LoginScreen> createState() => _LoginScreen();
@ -28,7 +29,8 @@ class _LoginScreen extends BasedState<LoginScreen> {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MainScreen(apiClient: widget.apiClient),
builder: (context) =>
MainScreen(apiClient: apiClient, storage: storage),
),
);
}
@ -36,7 +38,7 @@ class _LoginScreen extends BasedState<LoginScreen> {
// login client when button pressed
Future<void> _login() async {
try {
await widget.apiClient.login(
await apiClient.login(
usernameController.text,
passwordController.text,
);
@ -62,14 +64,14 @@ class _LoginScreen extends BasedState<LoginScreen> {
}
Future<void> _asyncInitState() async {
await widget.apiClient.loadToken();
await apiClient.loadToken();
if (widget.apiClient.refreshToken == null) {
if (apiClient.refreshToken == null) {
return;
}
try {
await widget.apiClient.refresh();
await apiClient.refresh();
showText("Welcome back!");
popMeDaddy();
} on Exception catch (_) {
@ -120,8 +122,10 @@ class _LoginScreen extends BasedState<LoginScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RegisterScreen(apiClient: widget.apiClient),
builder: (context) => RegisterScreen(
apiClient: apiClient,
storage: storage,
),
),
);
},

View file

@ -8,10 +8,10 @@ import 'package:fooder/widgets/meal.dart';
import 'package:fooder/components/sliver.dart';
import 'package:fooder/components/date_picker.dart';
import 'package:fooder/components/floating_action_button.dart';
import 'package:fooder/storage/base.dart';
class MainScreen extends BasedScreen {
const MainScreen({super.key, required super.apiClient});
const MainScreen(
{super.key, required super.apiClient, required super.storage});
@override
State<MainScreen> createState() => _MainScreen();
@ -28,7 +28,7 @@ class _MainScreen extends BasedState<MainScreen> {
}
Future<void> _asyncInitState() async {
var diaryMap = await widget.apiClient.getDiary(date: date);
var diaryMap = await apiClient.getDiary(date: date);
setState(() {
diary = Diary.fromJson(diaryMap);
@ -52,8 +52,8 @@ class _MainScreen extends BasedState<MainScreen> {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AddEntryScreen(apiClient: widget.apiClient, diary: diary!),
builder: (context) => AddEntryScreen(
apiClient: apiClient, diary: diary!, storage: storage),
),
).then((_) => _asyncInitState());
}
@ -67,8 +67,9 @@ class _MainScreen extends BasedState<MainScreen> {
context,
MaterialPageRoute(
builder: (context) => AddMealScreen(
apiClient: widget.apiClient,
apiClient: apiClient,
diary: diary!,
storage: storage,
),
),
).then((_) => _asyncInitState());
@ -90,13 +91,14 @@ class _MainScreen extends BasedState<MainScreen> {
[
SummaryWidget(
diary: diary!,
apiClient: widget.apiClient,
apiClient: apiClient,
refreshParent: _asyncInitState,
),
for (var (i, meal) in diary!.meals.indexed)
MealWidget(
apiClient: apiClient,
storage: storage,
meal: meal,
apiClient: widget.apiClient,
refreshParent: _asyncInitState,
initiallyExpanded: i == 0,
showText: showText,

View file

@ -5,7 +5,8 @@ import 'package:fooder/components/text.dart';
import 'package:fooder/components/button.dart';
class RegisterScreen extends BasedScreen {
const RegisterScreen({super.key, required super.apiClient});
const RegisterScreen(
{super.key, required super.apiClient, required super.storage});
@override
State<RegisterScreen> createState() => _RegisterScreen();
@ -49,7 +50,7 @@ class _RegisterScreen extends BasedState<RegisterScreen> {
}
try {
await widget.apiClient.register(
await apiClient.register(
usernameController.text,
passwordController.text,
);

View file

@ -9,23 +9,23 @@ class Storage {
Database db;
ProductStorage product;
static const String path = "storage.db";
Storage({required this.db, required this.product});
static Future<Storage> create() async {
var db = await database();
return Storage(db: db, product: ProductStorage(db: db));
}
static Future<void> createTables(Database db, int version) async {
await ProductStorage.createTable(db, version);
}
static Future<Database> database({String path = "storage.db"}) async {
WidgetsFlutterBinding.ensureInitialized();
return openDatabase(
var db = await openDatabase(
join(await getDatabasesPath(), path),
onCreate: createTables,
version: 1,
);
return Storage(db: db, product: ProductStorage(db: db));
}
static Future<void> createTables(Database db, int version) async {
var batch = db.batch();
await ProductStorage.createTable(batch);
await batch.commit(noResult: true);
}
}

11
lib/storage/based.dart Normal file
View file

@ -0,0 +1,11 @@
import 'dart:async';
import 'package:sqflite/sqflite.dart';
abstract class StorageBased {
Database db;
StorageBased({required this.db});
static Future<void> createTable(Batch batch) async {}
}

View file

@ -2,29 +2,32 @@ import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:fooder/models/product.dart';
import 'package:fooder/storage/based.dart';
class ProductStorage {
Database db;
class ProductStorage extends StorageBased {
ProductStorage({required super.db});
ProductStorage({required this.db});
static Future<void> createTable(Database db, int version) async {
await db.execute('''
static Future<void> createTable(Batch batch) async {
batch.execute('''
CREATE TABLE product(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
id INTEGER,
name TEXT,
barcode TEXT,
calories NOT NULL,
protein REAL NOT NULL,
carb REAL NOT NULL,
fat REAL NOT NULL,
fiber REAL NOT NULL
calories REAL,
protein REAL,
carb REAL,
fat REAL,
fiber REAL,
usage_count_cached INTEGER
)
''');
}
Future<List<Product>> list() async {
var result = await db.query('product');
Future<List<Product>> list({String? name, String? barcode}) async {
var result = await db.query('product',
where: 'name LIKE ? AND barcode LIKE ?',
whereArgs: ['%$name%', '%$barcode%'],
orderBy: 'usage_count_cached DESC');
return result.map((e) => Product.fromJson(e)).toList();
}

View file

@ -4,6 +4,7 @@ import 'package:fooder/widgets/entry.dart';
import 'package:fooder/widgets/macro.dart';
import 'package:fooder/screens/edit_entry.dart';
import 'package:fooder/client.dart';
import 'package:fooder/storage.dart';
import 'dart:core';
class MealHeader extends StatelessWidget {
@ -38,6 +39,7 @@ class MealWidget extends StatelessWidget {
final Meal meal;
final ApiClient apiClient;
final Storage storage;
final Function() refreshParent;
final Function(String) showText;
final bool initiallyExpanded;
@ -49,6 +51,7 @@ class MealWidget extends StatelessWidget {
required this.refreshParent,
required this.initiallyExpanded,
required this.showText,
required this.storage,
});
Future<void> saveMeal(context) async {
@ -124,6 +127,7 @@ class MealWidget extends StatelessWidget {
builder: (context) => EditEntryScreen(
apiClient: apiClient,
entry: entry,
storage: storage,
),
),
).then((_) => refreshParent());