Commit f9205db
Changed files (5)
app
lib
screens
subsettings
test
ui
components
health_data_store
lib
src
test
src
repositories
app/lib/screens/subsettings/medicine_manager_screen.dart
@@ -1,4 +1,6 @@
+import 'package:blood_pressure_app/components/consistent_future_builder.dart';
import 'package:blood_pressure_app/components/dialoges/add_medication_dialoge.dart';
+import 'package:blood_pressure_app/components/dialoges/confirm_deletion_dialoge.dart';
import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -9,25 +11,49 @@ import 'package:health_data_store/health_data_store.dart';
///
/// This screen allows adding and removing medication but not modifying them in
/// order to keep the code simple and maintainable.
-class MedicineManagerScreen extends StatefulWidget {
+class MedicineManagerScreen extends StatelessWidget {
/// Create a screen to manage medications in settings.
const MedicineManagerScreen({super.key});
- @override
- State<MedicineManagerScreen> createState() => _MedicineManagerScreenState();
-}
-
-class _MedicineManagerScreenState extends State<MedicineManagerScreen> {
- List<Medicine> medicines = [];
+ Widget _buildMedicine(BuildContext context, Medicine med) => ListTile(
+ leading: med.color == Colors.transparent.value
+ || med.color == null
+ ? null
+ : Container(
+ width: 40.0,
+ height: 40.0,
+ decoration: BoxDecoration(
+ color: Color(med.color!),
+ shape: BoxShape.circle,
+ ),
+ ),
+ title: Text(med.designation),
+ subtitle: med.dosis == null ? null
+ : Text('${AppLocalizations.of(context)!.defaultDosis}: '
+ '${med.dosis!.mg} mg'),
+ trailing: IconButton(
+ icon: const Icon(Icons.delete),
+ onPressed: () async {
+ if (await showConfirmDeletionDialoge(context)) {
+ await RepositoryProvider.of<MedicineRepository>(context).remove(med);
+ }
+ },
+ ),
+ );
- @override
- void initState() {
- super.initState();
- RepositoryProvider.of<MedicineRepository>(context).getAll()
- .then((value) => setState(() => medicines.addAll(value)));
- }
+ Widget _buildAddMed(BuildContext context) => ListTile(
+ leading: const Icon(Icons.add),
+ title: Text(AppLocalizations.of(context)!.addMedication),
+ onTap: () async {
+ final medRepo = RepositoryProvider.of<MedicineRepository>(context);
+ final medicine = await showAddMedicineDialoge(context);
+ if (medicine != null) {
+ await medRepo.add(medicine);
+ }
+ },
+ );
- @override
+ @override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
return Scaffold(
@@ -35,55 +61,20 @@ class _MedicineManagerScreenState extends State<MedicineManagerScreen> {
forceMaterialTransparency: true,
),
body: Center(
- child: ListView.builder(
- itemCount: medicines.length + 1,
- itemBuilder: (context, i) {
- if (i == medicines.length) { // last row
- return ListTile(
- leading: const Icon(Icons.add),
- title: Text(localizations.addMedication),
- onTap: () async {
- final medRepo = RepositoryProvider.of<MedicineRepository>(context);
- final medicine = await showAddMedicineDialoge(context);
- if (medicine != null) {
- setState(() {
- medicines.add(medicine);
- medRepo.add(medicine);
- });
- }
- },
- );
- }
- return ListTile(
- leading: medicines[i].color == Colors.transparent.value
- || medicines[i].color == null
- ? null
- : Container(
- width: 40.0,
- height: 40.0,
- decoration: BoxDecoration(
- color: Color(medicines[i].color!),
- shape: BoxShape.circle,
- ),
- ),
- title: Text(medicines[i].designation),
- // TODO: make localization function
- subtitle: medicines[i].dosis == null ? null
- : Text('${localizations.defaultDosis}: '
- '${medicines[i].dosis!.mg} mg'),
- trailing: IconButton(
- icon: const Icon(Icons.delete),
- onPressed: () async {
- await RepositoryProvider.of<MedicineRepository>(context)
- .remove(medicines[i]);
- setState(() async {
- medicines.removeAt(i);
- // FIXME: somehow no feedback
- });
- },
- ),
- );
- },
+ child: StreamBuilder(
+ stream: RepositoryProvider.of<MedicineRepository>(context).subscribe(),
+ builder: (context, _) => ConsistentFutureBuilder(
+ future: RepositoryProvider.of<MedicineRepository>(context).getAll(),
+ onData: (context, medicines) => ListView.builder(
+ itemCount: medicines.length + 1,
+ itemBuilder: (context, i) {
+ if (i == medicines.length) { // last row
+ return _buildAddMed(context);
+ }
+ return _buildMedicine(context, medicines[i]);
+ },
+ ),
+ ),
),
),
);
app/test/ui/components/util.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:blood_pressure_app/model/storage/storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -132,14 +134,29 @@ class MockMedRepo implements MedicineRepository {
final List<Medicine> _meds = [];
+ final _controller = StreamController.broadcast();
+
@override
- Future<void> add(Medicine medicine) async => _meds.add(medicine);
+ Future<void> add(Medicine medicine) async {
+ _meds.add(medicine);
+ _controller.add(null);
+ }
@override
Future<List<Medicine>> getAll() async=> _meds;
@override
- Future<void> remove(Medicine value) async => _meds.remove(value);
+ Future<void> remove(Medicine value) async {
+ _meds.remove(value);
+ _controller.add(null);
+ }
+
+ @override
+ @Deprecated('Medicines have no date. Use getAll directly')
+ Future<List<Medicine>> get(DateRange range) => getAll();
+
+ @override
+ Stream subscribe() => _controller.stream;
}
final List<Medicine> _meds = [];
health_data_store/lib/src/repositories/medicine_repository.dart
@@ -1,10 +1,12 @@
+import 'package:health_data_store/src/repositories/repository.dart';
import 'package:health_data_store/src/types/medicine.dart';
import 'package:health_data_store/src/types/medicine_intake.dart';
/// Repository for medicines that are taken by the user.
-abstract class MedicineRepository {
+abstract class MedicineRepository extends Repository<Medicine> {
/// Store a [Medicine] in the repository.
+ @override
Future<void> add(Medicine medicine);
/// Get a list of all stored Medicines that haven't been marked as removed.
@@ -15,6 +17,7 @@ abstract class MedicineRepository {
/// Intakes will be deleted as soon as there is no [MedicineIntake]s
/// referencing them. They need to be stored to allow intakes of them to be
/// still displayed correctly.
+ @override
Future<void> remove(Medicine value);
}
health_data_store/lib/src/repositories/medicine_repository_impl.dart
@@ -1,6 +1,9 @@
+import 'dart:async';
+
import 'package:health_data_store/src/database_manager.dart';
import 'package:health_data_store/src/extensions/castable.dart';
import 'package:health_data_store/src/repositories/medicine_repository.dart';
+import 'package:health_data_store/src/types/date_range.dart';
import 'package:health_data_store/src/types/medicine.dart';
import 'package:health_data_store/src/types/units/weight.dart';
import 'package:sqflite_common/sqflite.dart';
@@ -13,10 +16,13 @@ class MedicineRepositoryImpl extends MedicineRepository {
/// The [DatabaseManager] managed database.
final Database _db;
+ final _controller = StreamController.broadcast();
+
@override
Future<void> add(Medicine medicine) => _db.transaction((txn) async {
final idRes = await txn.query('Medicine', columns: ['MAX(medID)']);
final id = (idRes.firstOrNull?['MAX(medID)']?.castOrNull<int>() ?? 0) + 1;
+ _controller.add(null);
await txn.insert('Medicine', {
'medID': id,
'designation': medicine.designation,
@@ -45,25 +51,34 @@ class MedicineRepositoryImpl extends MedicineRepository {
}
@override
- Future<void> remove(Medicine value) => _db.update('Medicine', {
- 'removed': 1,
- },
- where: 'designation = ? AND color '
- + (value.color == null ? 'IS NULL' : '= ?')
- + ' AND defaultDose '
- + (value.dosis == null ? 'IS NULL' : '= ?'),
- whereArgs: [
- value.designation,
- if (value.color != null)
- value.color,
- if (value.dosis != null)
- value.dosis!.mg,
- ],
- );
+ Future<void> remove(Medicine value) async {
+ _controller.add(null);
+ await _db.update('Medicine', {
+ 'removed': 1,
+ },
+ where: 'designation = ? AND color '
+ + (value.color == null ? 'IS NULL' : '= ?')
+ + ' AND defaultDose '
+ + (value.dosis == null ? 'IS NULL' : '= ?'),
+ whereArgs: [
+ value.designation,
+ if (value.color != null)
+ value.color,
+ if (value.dosis != null)
+ value.dosis!.mg,
+ ],
+ );
+ }
Weight? _decode(Object? value) {
if (value is! double) return null;
return Weight.mg(value);
}
+ @override
+ @Deprecated('Medicines have no date. Use getAll directly')
+ Future<List<Medicine>> get(DateRange _) => getAll();
+
+ @override
+ Stream subscribe() => _controller.stream;
}
health_data_store/test/src/repositories/medicine_repository_test.dart
@@ -90,5 +90,20 @@ void main() {
await repo.remove(med3);
expect(await repo.getAll(), isEmpty);
});
+ test('notifies on changes', () async {
+ final db = await mockDBManager();
+ addTearDown(db.close);
+ final repo = MedicineRepositoryImpl(db.db);
+ int calls = 0;
+ repo.subscribe().listen((_) => calls++);
+ final med1 = Medicine(designation: 'med1', color: 0xFF226A,);
+ await repo.add(med1);
+ expect(calls, 1);
+ await repo.add(mockMedicine(designation: 'med2', dosis: 43));
+ await repo.add(Medicine(designation: 'med3'));
+ expect(calls, 3);
+ await repo.remove(med1);
+ expect(calls, 4);
+ });
}