Commit 56cfaca

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-09-09 19:19:33
Add weight view (#428)
* extract action buttons to widget * extract building to full entry builder * add weight setting * reimplement home screen with multi-tab support * implement weight list * test full entry builder * test weight list * test nav buttons * test settings data * test home screen
1 parent 3102bbe
app/lib/data_util/blood_pressure_builder.dart
@@ -1,32 +0,0 @@
-import 'dart:collection';
-
-import 'package:blood_pressure_app/data_util/repository_builder.dart';
-import 'package:blood_pressure_app/model/storage/interval_store.dart';
-import 'package:collection/collection.dart';
-import 'package:flutter/material.dart';
-import 'package:health_data_store/health_data_store.dart';
-
-/// Shorthand class for getting the blood pressure values.
-class BloodPressureBuilder extends StatelessWidget {
-  /// Create a loader for the measurements in the current range.
-  const BloodPressureBuilder({
-    super.key,
-    required this.onData,
-    required this.rangeType,
-  });
-
-  /// The build strategy once the measurement are loaded.
-  final Widget Function(BuildContext context, UnmodifiableListView<BloodPressureRecord> records) onData;
-
-  /// Which measurements to load.
-  final IntervalStoreManagerLocation rangeType;
-  
-  @override
-  Widget build(BuildContext context) =>
-    RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
-      rangeType: rangeType,
-      onData: (context, List<BloodPressureRecord> data) =>
-        onData(context, UnmodifiableListView(data)),
-  );
-  
-}
app/lib/data_util/full_entry_builder.dart
@@ -0,0 +1,44 @@
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+/// Shorthand class for getting a [rangeType]s [FullEntry] values.
+class FullEntryBuilder extends StatelessWidget {
+  /// Create a loader for getting a [rangeType]s [FullEntry] values.
+  ///
+  /// Provide either [onEntries] or [onData].
+  const FullEntryBuilder({
+    super.key,
+    this.onEntries,
+    this.onData,
+    required this.rangeType,
+  }) : assert((onEntries == null) != (onData == null), 'Provide either of the builders.');
+
+  /// Builder using a sorted list of full entries.
+  final Widget Function(BuildContext context, List<FullEntry> entries)? onEntries;
+
+  /// Builder using data from the main categories.
+  final Widget Function(BuildContext context, List<BloodPressureRecord> records, List<MedicineIntake> intakes, List<Note> notes)? onData;
+
+  /// Range type to load entries from.
+  final IntervalStoreManagerLocation rangeType;
+
+  @override
+  Widget build(BuildContext context) => RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
+    rangeType: rangeType,
+    onData: (context, records) => RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
+      rangeType: rangeType,
+      onData: (BuildContext context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
+        rangeType: rangeType,
+        onData: (BuildContext context, List<Note> notes) {
+          if (onData != null) return onData!(context, records, intakes, notes);
+
+          final entries = FullEntryList.merged(records, notes, intakes);
+          entries.sort((a, b) => b.time.compareTo(a.time)); // newest first
+          return onEntries!(context, entries);
+        },
+      ),
+    ),
+  );
+}
app/lib/features/home/navigation_action_buttons.dart
@@ -0,0 +1,60 @@
+import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:blood_pressure_app/screens/settings_screen.dart';
+import 'package:blood_pressure_app/screens/statistics_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+
+/// Column of floating action buttons to navigate to [SettingsPage],
+/// [StatisticsScreen] or [EntryUtils.createEntry]
+class NavigationActionButtons extends StatelessWidget {
+  /// Create main FAB navigation column.
+  const NavigationActionButtons({super.key});
+
+  @override
+  Widget build(BuildContext context) => Consumer<Settings>(
+    builder: (context, settings, _) => Column(
+      verticalDirection: VerticalDirection.up,
+      children: [
+        SizedBox.square(
+          dimension: 75,
+          child: FittedBox(
+            child: FloatingActionButton(
+              heroTag: 'floatingActionAdd',
+              tooltip: AppLocalizations.of(context)!.addMeasurement,
+              autofocus: true,
+              onPressed: context.createEntry,
+              child: const Icon(Icons.add,),
+            ),
+          ),
+        ),
+        const SizedBox(
+          height: 10,
+        ),
+        FloatingActionButton(
+          heroTag: 'floatingActionStatistics',
+          tooltip: AppLocalizations.of(context)!.statistics,
+          backgroundColor: const Color(0xFF6F6F6F),
+          onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(
+            builder: (BuildContext context) => const StatisticsScreen(),
+          )),
+          child: const Icon(Icons.insights, color: Colors.black),
+        ),
+        const SizedBox(
+          height: 10,
+        ),
+        FloatingActionButton(
+          heroTag: 'floatingActionSettings',
+          tooltip: AppLocalizations.of(context)!.settings,
+          backgroundColor: const Color(0xFF6F6F6F),
+          child: const Icon(Icons.settings, color: Colors.black),
+          onPressed: () => Navigator.of(context).push(MaterialPageRoute<void>(
+            builder: (BuildContext context) => const SettingsPage(),
+          )),
+        ),
+      ],
+    ),
+  );
+
+}
\ No newline at end of file
app/lib/features/measurement_list/weight_list.dart
@@ -0,0 +1,42 @@
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+
+/// List of weights recorded in the contexts [BodyweightRepository].
+class WeightList extends StatelessWidget {
+  /// Create a list of weights
+  const WeightList({super.key, required this.rangeType});
+
+  /// The location from which the displayed interval is taken.
+  final IntervalStoreManagerLocation rangeType;
+
+  @override
+  Widget build(BuildContext context) {
+    final format = DateFormat(context.select<Settings, String>((s) => s.dateFormatString));
+    return RepositoryBuilder<BodyweightRecord, BodyweightRepository>(
+      rangeType: rangeType,
+      onData: (context, records) {
+        records.sort((a, b) => b.time.compareTo(a.time));
+        return ListView.builder(
+          itemCount: records.length,
+          itemBuilder: (context, idx) => ListTile(
+            title: Text(_buildWeightText(records[idx].weight)),
+            subtitle: Text(format.format(records[idx].time))
+          ),
+        );
+      },
+    );
+  }
+
+  String _buildWeightText(Weight w) {
+    String weightStr = w.kg.toStringAsFixed(2);
+    if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
+    if (weightStr.endsWith('0')) weightStr = weightStr.substring(0, weightStr.length - 1);
+    if (weightStr.endsWith('.')) weightStr = weightStr.substring(0, weightStr.length - 1);
+    // TODO: preferred weight unit
+    return '$weightStr kg';
+  }
+}
app/lib/l10n/app_en.arb
@@ -516,5 +516,7 @@
   "invalidZip": "Invalid zip file.",
   "@invalidZip": {},
   "errCantCreateArchive": "Can''t create archive. Please report the bug if possible.",
-  "@errCantCreateArchive": {}
+  "@errCantCreateArchive": {},
+  "activateWeightFeatures": "Activate weight related features",
+  "@weightInput": {}
 }
\ No newline at end of file
app/lib/model/storage/settings_store.dart
@@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
     List<String>? knownBleDev,
     int? highestMedIndex,
     bool? bleInput,
+    bool? weightInput,
   }) {
     if (accentColor != null) _accentColor = accentColor;
     if (sysColor != null) _sysColor = sysColor;
@@ -76,6 +77,7 @@ class Settings extends ChangeNotifier {
     if (highestMedIndex != null) _highestMedIndex = highestMedIndex;
     if (knownBleDev != null) _knownBleDev = knownBleDev;
     if (bleInput != null) _bleInput = bleInput;
+    if (weightInput != null) _weightInput = weightInput;
     _language = language; // No check here, as null is the default as well.
   }
 
@@ -110,6 +112,7 @@ class Settings extends ChangeNotifier {
       highestMedIndex: ConvertUtil.parseInt(map['highestMedIndex']),
       knownBleDev: ConvertUtil.parseList<String>(map['knownBleDev']),
       bleInput: ConvertUtil.parseBool(map['bleInput']),
+      weightInput: ConvertUtil.parseBool(map['weightInput']),
     );
 
     // update
@@ -157,6 +160,7 @@ class Settings extends ChangeNotifier {
     'preferredPressureUnit': preferredPressureUnit.encode(),
     'knownBleDev': knownBleDev,
     'bleInput': bleInput,
+    'weightInput': weightInput,
   };
 
   /// Serialize the object to a restoreable string.
@@ -192,6 +196,7 @@ class Settings extends ChangeNotifier {
     _medications.clear();
     _medications.addAll(other._medications);
     _highestMedIndex = other._highestMedIndex;
+    _weightInput = other._weightInput;
     notifyListeners();
   }
 
@@ -408,6 +413,14 @@ class Settings extends ChangeNotifier {
     notifyListeners();
   }
 
+  bool _weightInput = false;
+  /// Whether to show weight related features.
+  bool get weightInput => _weightInput;
+  set weightInput(bool value) {
+    _weightInput = value;
+    notifyListeners();
+  }
+
   List<String> _knownBleDev = [];
   /// Bluetooth devices that previously connected.
   ///
app/lib/screens/home_screen.dart
@@ -1,25 +1,19 @@
-import 'dart:collection';
-
-import 'package:blood_pressure_app/data_util/blood_pressure_builder.dart';
 import 'package:blood_pressure_app/data_util/entry_context.dart';
+import 'package:blood_pressure_app/data_util/full_entry_builder.dart';
 import 'package:blood_pressure_app/data_util/interval_picker.dart';
-import 'package:blood_pressure_app/data_util/repository_builder.dart';
+import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
 import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
 import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';
+import 'package:blood_pressure_app/features/measurement_list/weight_list.dart';
 import 'package:blood_pressure_app/features/statistics/value_graph.dart';
 import 'package:blood_pressure_app/model/storage/interval_store.dart';
 import 'package:blood_pressure_app/model/storage/settings_store.dart';
-import 'package:blood_pressure_app/screens/settings_screen.dart';
-import 'package:blood_pressure_app/screens/statistics_screen.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:health_data_store/health_data_store.dart';
 import 'package:provider/provider.dart';
 
-/// Is true during the first [AppHome.build] before creating the widget.
-bool _appStart = true;
+/// 0 when add entry dialoge has not been shown, 1 when dialoge is scheduled, 2 when dialoge was launched.
+int _appStart = 0;
 
 /// Central screen of the app with graph and measurement list that is the center
 /// of navigation.
@@ -28,146 +22,82 @@ class AppHome extends StatelessWidget {
   const AppHome({super.key});
 
   Widget _buildValueGraph(BuildContext context) => Padding(
-    padding: const EdgeInsets.only(right: 8, left: 2, top: 16),
-    child: Column(
-      children: [
-        SizedBox(
-          height: 240,
-          width: MediaQuery.of(context).size.width,
-          // TODO: stop duplicating this complex construct
-          child: RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
-            rangeType: IntervalStoreManagerLocation.mainPage,
-            onData: (context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
-              rangeType: IntervalStoreManagerLocation.mainPage,
-              onData: (context, List<Note> notes) => BloodPressureBuilder(
-                rangeType: IntervalStoreManagerLocation.mainPage,
-                onData: (BuildContext context, UnmodifiableListView<BloodPressureRecord> records) => BloodPressureValueGraph(
-                  records: records,
-                  colors: notes,
-                  intakes: intakes,
-                ),
-              ),
-            ),
-          ),
+    padding: const EdgeInsets.only(right: 8, top: 16),
+    child: SizedBox(
+      height: 240.0,
+      width: MediaQuery.of(context).size.width,
+      child: FullEntryBuilder(
+        rangeType: IntervalStoreManagerLocation.mainPage,
+        onData: (context, records, intakes, notes) => BloodPressureValueGraph(
+          records: records,
+          colors: notes,
+          intakes: intakes,
         ),
-        const IntervalPicker(type: IntervalStoreManagerLocation.mainPage),
-      ],
+      ),
+    ),
+  );
+
+  Widget _buildMeasurementList(BuildContext context) => FullEntryBuilder(
+    rangeType: IntervalStoreManagerLocation.mainPage,
+    onEntries: (context, entries) => Padding(
+      padding: const EdgeInsets.only(top: 12.0),
+      child: (context.select<Settings, bool>((s) => s.compactList))
+        ? CompactMeasurementList(data: entries)
+        : MeasurementList(entries: entries),
     ),
   );
 
   @override
-  Widget build(BuildContext context) {
-    final localizations = AppLocalizations.of(context)!;
-    // direct use of settings possible as no listening is required
-    if (_appStart) {
-      if (Provider.of<Settings>(context, listen: false).startWithAddMeasurementPage) {
-        SchedulerBinding.instance.addPostFrameCallback((_) => context.createEntry());
+  Widget build(BuildContext context) => OrientationBuilder(
+    builder: (BuildContext context, Orientation orientation) {
+      // direct use of settings possible as no listening is required
+      if (_appStart == 0) {
+        if (Provider.of<Settings>(context, listen: false).startWithAddMeasurementPage) {
+          SchedulerBinding.instance.addPostFrameCallback((_) {
+            if (context.mounted) {
+              context.createEntry();
+              _appStart++;
+            } else {
+              _appStart--;
+            }
+
+          });
+        }
+        _appStart++;
       }
-    }
-    _appStart = false;
 
-    return Scaffold(
-      body: OrientationBuilder(
-        builder: (context, orientation) {
-        if (orientation == Orientation.landscape) return _buildValueGraph(context);
-        return Center(
-          child: Padding(
-            padding: const EdgeInsets.only(top: 20),
-            child: Consumer<IntervalStoreManager>(builder: (context, intervalls, child) =>
-              Column(children: [
-                _buildValueGraph(context),
-                Expanded(
-                  child: BloodPressureBuilder(
-                    rangeType: IntervalStoreManagerLocation.mainPage,
-                    onData: (context, records) => RepositoryBuilder<MedicineIntake, MedicineIntakeRepository>(
-                      rangeType: IntervalStoreManagerLocation.mainPage,
-                      onData: (BuildContext context, List<MedicineIntake> intakes) => RepositoryBuilder<Note, NoteRepository>(
-                        rangeType: IntervalStoreManagerLocation.mainPage,
-                        onData: (BuildContext context, List<Note> notes) {
-                          final entries = FullEntryList.merged(records, notes, intakes);
-                          entries.sort((a, b) => b.time.compareTo(a.time)); // newest first
-                          return (context.select<Settings, bool>((s) => s.compactList))
-                            ? CompactMeasurementList(data: entries)
-                            : MeasurementList(entries: entries);
-                        },
-                      ),
-                    ),
-                  ),
-                ),
-              ],),),
-            ),
-        );
-      },
-      ),
-      floatingActionButton: OrientationBuilder(
-        builder: (context, orientation) {
-          if (orientation == Orientation.landscape && MediaQuery.of(context).size.height < 500) {
-            SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
-            return const SizedBox.shrink();
-          }
-          SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
-          return Consumer<Settings>(builder: (context, settings, child) => Column(
-            verticalDirection: VerticalDirection.up,
-            children: [
-              SizedBox.square(
-                dimension: 75,
-                child: FittedBox(
-                  child: FloatingActionButton(
-                    heroTag: 'floatingActionAdd',
-                    tooltip: localizations.addMeasurement,
-                    autofocus: true,
-                    onPressed: context.createEntry,
-                    child: const Icon(Icons.add,),
+      if (orientation == Orientation.landscape) return _buildValueGraph(context);
+      return DefaultTabController(
+        length: 2,
+        child: Scaffold(
+          body: CustomScrollView(
+            slivers: [
+              SliverToBoxAdapter(child: _buildValueGraph(context),),
+              const SliverToBoxAdapter(child: IntervalPicker(type: IntervalStoreManagerLocation.mainPage)),
+              if (!(context.select<Settings, bool>((s) => s.weightInput)))
+                SliverFillRemaining(child: _buildMeasurementList(context)),
+
+              if ((context.select<Settings, bool>((s) => s.weightInput)))
+                const SliverToBoxAdapter(child: TabBar(
+                  tabs: [
+                    Tab(icon: Icon(Icons.monitor_heart)),
+                    Tab(icon: Icon(Icons.scale)),
+                  ],
+                )),
+              if ((context.select<Settings, bool>((s) => s.weightInput)))
+                SliverFillRemaining(
+                  child: TabBarView(
+                      children: [
+                        _buildMeasurementList(context),
+                        const WeightList(rangeType: IntervalStoreManagerLocation.mainPage),
+                      ]
                   ),
-                ),
-              ),
-              const SizedBox(
-                height: 10,
-              ),
-              FloatingActionButton(
-                heroTag: 'floatingActionStatistics',
-                tooltip: localizations.statistics,
-                backgroundColor: const Color(0xFF6F6F6F),
-                onPressed: () {
-                  _buildTransition(context, const StatisticsScreen(), settings.animationSpeed);
-                },
-                child: const Icon(Icons.insights, color: Colors.black),
-              ),
-              const SizedBox(
-                height: 10,
-              ),
-              FloatingActionButton(
-                heroTag: 'floatingActionSettings',
-                tooltip: localizations.settings,
-                backgroundColor: const Color(0xFF6F6F6F),
-                child: const Icon(Icons.settings, color: Colors.black),
-                onPressed: () {
-                  _buildTransition(context, const SettingsPage(), settings.animationSpeed);
-                },
-              ),
+                )
             ],
-          ),);
-        },
-      ),
-    );
-  }
-}
-
-// TODO: consider removing duration override that only occurs in one on home.
-void _buildTransition(BuildContext context, Widget page, int duration) {
-  Navigator.push(context,
-    _TimedMaterialPageRouter(
-      transitionDuration: Duration(milliseconds: duration),
-      builder: (context) => page,
-    ),
+          ),
+          floatingActionButton: const NavigationActionButtons(),
+        ),
+      );
+    },
   );
 }
-
-class _TimedMaterialPageRouter extends MaterialPageRoute {
-  _TimedMaterialPageRouter({
-    required super.builder,
-    required this.transitionDuration,});
-
-  @override
-  final Duration transitionDuration;
-}
\ No newline at end of file
app/lib/screens/settings_screen.dart
@@ -292,6 +292,13 @@ class SettingsPage extends StatelessWidget {
                   if (value != null) settings.preferredPressureUnit = value;
                 },
               ),
+              SwitchListTile(
+                value: settings.weightInput,
+                title: Text(localizations.activateWeightFeatures),
+                secondary: const Icon(Icons.scale),
+                onChanged: (value) {
+                  settings.weightInput = value;
+                },),
             ],),
             TitledColumn(
               title: Text(localizations.data),
app/lib/screens/statistics_screen.dart
@@ -1,11 +1,12 @@
-import 'package:blood_pressure_app/data_util/blood_pressure_builder.dart';
 import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/data_util/repository_builder.dart';
 import 'package:blood_pressure_app/features/statistics/blood_pressure_distribution.dart';
 import 'package:blood_pressure_app/features/statistics/clock_bp_graph.dart';
 import 'package:blood_pressure_app/model/blood_pressure_analyzer.dart';
 import 'package:blood_pressure_app/model/storage/interval_store.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:health_data_store/health_data_store.dart';
 
 /// A page that shows statistics about stored blood pressure values.
 class StatisticsScreen extends StatefulWidget {
@@ -24,7 +25,7 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
       appBar: AppBar(
         title: Text(localizations.statistics),
       ),
-      body: BloodPressureBuilder(
+      body: RepositoryBuilder<BloodPressureRecord, BloodPressureRepository>(
         rangeType: IntervalStoreManagerLocation.statsPage,
         onData: (context, data) {
           final analyzer = BloodPressureAnalyser(data.toList());
app/lib/app.dart
@@ -99,6 +99,7 @@ class _AppState extends State<App> {
     late NoteRepository noteRepo;
     late MedicineRepository medRepo;
     late MedicineIntakeRepository intakeRepo;
+    late BodyweightRepository weightRepo;
 
     try {
       _entryDB = await openDatabase(
@@ -109,6 +110,7 @@ class _AppState extends State<App> {
       noteRepo = db.noteRepo;
       medRepo = db.medRepo;
       intakeRepo = db.intakeRepo;
+      weightRepo = db.weightRepo;
     } catch (e, stack) {
       await ErrorReporting.reportCriticalError('Error loading entry db', '$e\n$stack',);
     }
@@ -180,6 +182,7 @@ class _AppState extends State<App> {
         RepositoryProvider.value(value: noteRepo),
         RepositoryProvider.value(value: medRepo),
         RepositoryProvider.value(value: intakeRepo),
+        RepositoryProvider.value(value: weightRepo),
       ],
       child: MultiProvider(
         providers: [
app/test/data_util/entry_context_test.dart
@@ -14,9 +14,9 @@ void main() {
   testWidgets('fully deletes entries', (tester) async {
     final entry = mockEntry(time: DateTime.now(), sys: 123, note: 'test', intake: mockIntake(mockMedicine()));
 
-    final BloodPressureRepository bpRepo = _MockBp() as BloodPressureRepository;
-    final NoteRepository noteRepo = _MockNote() as NoteRepository;
-    final MedicineIntakeRepository intakeRepo = _MockIntake() as MedicineIntakeRepository;
+    final BloodPressureRepository bpRepo = MockBloodPressureRepository() as BloodPressureRepository;
+    final NoteRepository noteRepo = MockNoteRepository() as NoteRepository;
+    final MedicineIntakeRepository intakeRepo = MockMedicineIntakeRepository() as MedicineIntakeRepository;
     await bpRepo.add(entry.$1);
     await noteRepo.add(entry.$2);
     await intakeRepo.add(entry.$3.first);
@@ -50,32 +50,3 @@ void main() {
     expect(await intakeRepo.get(DateRange.all()), isEmpty);
   });
 }
-
-class _MockRepo<T> extends Repository<T> {
-  List<T> data = [];
-
-  @override
-  Future<void> add(T value) async => data.add(value);
-
-  @override
-  Future<List<T>> get(DateRange range) async => data;
-
-  @override
-  Future<void> remove(T value) async => data.remove(value);
-
-  @override
-  Stream subscribe() => Stream.empty();
-}
-
-class _MockBp extends _MockRepo<BloodPressureRecord> implements BloodPressureRepository {
-  @override
-  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
-class _MockNote extends _MockRepo<Note> implements NoteRepository {
-  @override
-  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
-class _MockIntake extends _MockRepo<MedicineIntake> implements MedicineIntakeRepository {
-  @override
-  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
-}
app/test/data_util/full_entry_builder_test.dart
@@ -0,0 +1,58 @@
+import 'package:blood_pressure_app/data_util/full_entry_builder.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+import '../features/measurement_list/measurement_list_entry_test.dart';
+import '../model/analyzer_test.dart';
+import '../util.dart';
+
+void main() {
+  testWidgets('loads expected data', (tester) async {
+    final records = [mockRecord(sys: 123)];
+    final notes = [Note(time: DateTime(2000), note: 'test1'), Note(time: DateTime(2001), note: 'test2')];
+    final intakes = [mockIntake(mockMedicine()), mockIntake(mockMedicine()), mockIntake(mockMedicine())];
+
+    final mainIntervalls = IntervalStorage();
+    mainIntervalls.changeStepSize(TimeStep.lifetime);
+
+    await tester.pumpWidget(await appBaseWithData(FullEntryBuilder(
+      rangeType: IntervalStoreManagerLocation.mainPage,
+      onData: (context, foundRecords, foundIntakes, foundNotes) {
+        expect(foundRecords, records);
+        expect(foundIntakes, intakes);
+        expect(foundNotes, notes);
+        return const Text('ok');
+      },
+    ), records: records, intakes: intakes, notes: notes, intervallStoreManager: IntervalStoreManager(mainIntervalls, IntervalStorage(), IntervalStorage())));
+
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+    expect(find.text(localizations.loading), findsOneWidget);
+    await tester.pumpAndSettle();
+    expect(find.text('ok'), findsOneWidget);
+  });
+  testWidgets('loads sorted entries', (tester) async {
+    final notes = [Note(time: DateTime(2003), note: 'test0'), Note(time: DateTime(2000), note: 'test1'), Note(time: DateTime(2001), note: 'test2')];
+
+    final exportPageIntervalls = IntervalStorage();
+    exportPageIntervalls.changeStepSize(TimeStep.lifetime);
+
+    await tester.pumpWidget(await appBaseWithData(FullEntryBuilder(
+      rangeType: IntervalStoreManagerLocation.exportPage,
+      onEntries: (context, entries) {
+        expect(entries, hasLength(3));
+        expect(entries[0].time.year, 2003);
+        expect(entries[1].time.year, 2001);
+        expect(entries[2].time.year, 2000);
+        return const Text('ok');
+      },
+    ), notes: notes, intervallStoreManager: IntervalStoreManager(exportPageIntervalls, IntervalStorage(), IntervalStorage())));
+
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+    expect(find.text(localizations.loading), findsOneWidget);
+    await tester.pumpAndSettle();
+    expect(find.text('ok'), findsOneWidget);
+  });
+}
app/test/features/home/navigation_action_buttons.dart
@@ -0,0 +1,16 @@
+import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../util.dart';
+
+void main() {
+  testWidgets('shows all buttons', (tester) async {
+    await tester.pumpWidget(materialApp(const NavigationActionButtons()));
+    
+    expect(find.byType(FloatingActionButton), findsNWidgets(3));
+    expect(find.byIcon(Icons.add), findsOneWidget);
+    expect(find.byIcon(Icons.settings), findsOneWidget);
+    expect(find.byIcon(Icons.insights), findsOneWidget);
+  });
+}
app/test/features/measurement_list/weight_list_test.dart
@@ -0,0 +1,45 @@
+import 'package:blood_pressure_app/features/measurement_list/weight_list.dart';
+import 'package:blood_pressure_app/model/storage/interval_store.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:health_data_store/health_data_store.dart';
+
+import '../../util.dart';
+
+void main() {
+  testWidgets('shows all elements in time range in order', (tester) async {
+    final interval = IntervalStorage();
+    interval.changeStepSize(TimeStep.lifetime);
+
+    await tester.pumpWidget(await appBaseWithData(
+      weights: [
+        BodyweightRecord(time: DateTime(2001), weight: Weight.kg(123.0)),
+        BodyweightRecord(time: DateTime(2003), weight: Weight.kg(122.1)),
+        BodyweightRecord(time: DateTime(2000), weight: Weight.kg(70.0)),
+        BodyweightRecord(time: DateTime(2002), weight: Weight.kg(7000.12345)),
+      ],
+      intervallStoreManager: IntervalStoreManager(interval, IntervalStorage(), IntervalStorage()),
+      const WeightList(rangeType: IntervalStoreManagerLocation.mainPage),
+    ));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(ListTile), findsNWidgets(4));
+    expect(find.text('123 kg'), findsOneWidget);
+    expect(find.text('122.1 kg'), findsOneWidget);
+    expect(find.text('70 kg'), findsOneWidget);
+    expect(find.text('7000.12 kg'), findsOneWidget);
+
+    expect(
+      tester.getCenter(find.textContaining('2003')).dy,
+      lessThan(tester.getCenter(find.textContaining('2002')).dy),
+    );
+    expect(
+      tester.getCenter(find.textContaining('2002')).dy,
+      lessThan(tester.getCenter(find.textContaining('2001')).dy),
+    );
+    expect(
+      tester.getCenter(find.textContaining('2001')).dy,
+      lessThan(tester.getCenter(find.textContaining('2000')).dy),
+    );
+  });
+}
app/test/model/json_serialization_test.dart
@@ -96,6 +96,7 @@ void main() {
         bottomAppBars: true,
         knownBleDev: ['a', 'b'],
         bleInput: false,
+        weightInput: true,
       );
       final fromJson = Settings.fromJson(initial.toJson());
 
@@ -124,6 +125,7 @@ void main() {
       expect(initial.bottomAppBars, fromJson.bottomAppBars);
       expect(initial.knownBleDev, fromJson.knownBleDev);
       expect(initial.bleInput, fromJson.bleInput);
+      expect(initial.weightInput, fromJson.weightInput);
 
       expect(initial.toJson(), fromJson.toJson());
     });
app/test/screens/home_screen_test.dart
@@ -0,0 +1,70 @@
+import 'dart:ui';
+
+import 'package:blood_pressure_app/data_util/interval_picker.dart';
+import 'package:blood_pressure_app/features/home/navigation_action_buttons.dart';
+import 'package:blood_pressure_app/features/measurement_list/compact_measurement_list.dart';
+import 'package:blood_pressure_app/features/measurement_list/measurement_list.dart';
+import 'package:blood_pressure_app/features/statistics/value_graph.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:blood_pressure_app/screens/home_screen.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../model/analyzer_test.dart';
+import '../util.dart';
+
+void main() {
+  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
+
+  testWidgets('shows graph above list in phone mode', (tester) async {
+    await binding.setSurfaceSize(const Size(400, 800));
+
+    await tester.pumpWidget(await appBaseWithData(const AppHome()));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(NavigationActionButtons), findsOneWidget);
+
+    expect(find.byType(BloodPressureValueGraph), findsOneWidget);
+    expect(find.byType(IntervalPicker), findsOneWidget);
+    expect(find.byType(MeasurementList), findsOneWidget);
+
+    expect(
+      tester.getCenter(find.byType(BloodPressureValueGraph)).dy,
+      lessThan(tester.getCenter(find.byType(IntervalPicker)).dy)
+    );
+    expect(
+        tester.getCenter(find.byType(IntervalPicker)).dy,
+        lessThan(tester.getCenter(find.byType(MeasurementList)).dy)
+    );
+  });
+
+  testWidgets('only shows graph in landscape more', (tester) async {
+    await binding.setSurfaceSize(const Size(800, 400));
+
+    await tester.pumpWidget(await appBaseWithData(const AppHome(),
+      records: [mockRecord(sys: 123)],
+    ));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(BloodPressureValueGraph), findsOneWidget);
+    expect(find.byType(NavigationActionButtons), findsNothing);
+    expect(find.byType(IntervalPicker), findsNothing);
+    expect(find.byType(MeasurementList), findsNothing);
+  });
+
+  testWidgets('respects compact list setting', (tester) async {
+    await binding.setSurfaceSize(const Size(400, 800));
+
+    final s = Settings(useLegacyList: false);
+    await tester.pumpWidget(await appBaseWithData(const AppHome(), settings: s));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(MeasurementList), findsOneWidget);
+    expect(find.byType(CompactMeasurementList), findsNothing);
+
+    s.compactList = true;
+    await tester.pump();
+
+    expect(find.byType(MeasurementList), findsNothing);
+    expect(find.byType(CompactMeasurementList), findsOneWidget);
+  });
+}
app/test/util.dart
@@ -7,7 +7,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:health_data_store/health_data_store.dart';
 import 'package:provider/provider.dart';
-import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
 /// Create a root material widget with localizations.
 Widget materialApp(Widget child, {
@@ -39,7 +38,7 @@ Widget materialApp(Widget child, {
 }
 
 /// Creates a the same App as the main method.
-Future<Widget> appBase(Widget child,  {
+Widget appBase(Widget child,  {
   Settings? settings,
   ExportSettings? exportSettings,
   CsvExportSettings? csvExportSettings,
@@ -47,11 +46,18 @@ Future<Widget> appBase(Widget child,  {
   IntervalStoreManager? intervallStoreManager,
   BloodPressureRepository? bpRepo,
   MedicineRepository? medRepo,
+  NoteRepository? noteRepo,
   MedicineIntakeRepository? intakeRepo,
-}) async {
+  BodyweightRepository? weightRepo,
+}) {
   HealthDataStore? db;
-  if (bpRepo == null || medRepo == null || intakeRepo == null) {
-    db = await _getHealthDateStore();
+  if (bpRepo == null
+    || medRepo == null
+    || intakeRepo == null
+    || noteRepo == null
+    || weightRepo == null
+  ) {
+    db = MockHealthDataSore();
   }
 
   return MultiRepositoryProvider(
@@ -59,6 +65,8 @@ Future<Widget> appBase(Widget child,  {
       RepositoryProvider(create: (context) => bpRepo ?? db!.bpRepo),
       RepositoryProvider(create: (context) => medRepo ?? db!.medRepo),
       RepositoryProvider(create: (context) => intakeRepo ?? db!.intakeRepo),
+      RepositoryProvider(create: (context) => noteRepo ?? db!.noteRepo),
+      RepositoryProvider(create: (context) => weightRepo ?? db!.weightRepo),
     ],
     child: materialApp(child,
       settings: settings,
@@ -79,15 +87,31 @@ Future<Widget> appBaseWithData(Widget child,  {
   IntervalStoreManager? intervallStoreManager,
   List<BloodPressureRecord>? records,
   List<Medicine>? meds,
+  List<Note>? notes,
   List<MedicineIntake>? intakes,
+  List<BodyweightRecord>? weights,
 }) async {
-  final db = await _getHealthDateStore();
+  final db = MockHealthDataSore();
   final bpRepo = db.bpRepo;
   for (final r in records ?? []) {
     await bpRepo.add(r);
   }
   final medRepo = db.medRepo;
+  for (final m in meds ?? []) {
+    await medRepo.add(m);
+  }
   final intakeRepo = db.intakeRepo;
+  for (final i in intakes ?? []) {
+    await intakeRepo.add(i);
+  }
+  final noteRepo = db.noteRepo;
+  for (final n in notes ?? []) {
+    await noteRepo.add(n);
+  }
+  final weightRepo = db.weightRepo;
+  for (final w in weights ?? []) {
+    await weightRepo.add(w);
+  }
 
   return appBase(
     child,
@@ -98,7 +122,9 @@ Future<Widget> appBaseWithData(Widget child,  {
     intervallStoreManager: intervallStoreManager,
     bpRepo: bpRepo,
     medRepo: medRepo,
+    noteRepo: noteRepo,
     intakeRepo: intakeRepo,
+    weightRepo: weightRepo,
   );
 }
 
@@ -185,12 +211,44 @@ Medicine mockMedicine({
   return med;
 }
 
-/// Don't use this, use [_getHealthDateStore] to obtain.
-HealthDataStore? _db;
-Future<HealthDataStore> _getHealthDateStore() async {
-  TestWidgetsFlutterBinding.ensureInitialized();
-  sqfliteFfiInit();
-  final db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
-  _db ??= await HealthDataStore.load(db);
-  return _db!;
+class MockHealthDataSore implements HealthDataStore {
+  @override
+  BloodPressureRepository bpRepo = MockBloodPressureRepository();
+
+  @override
+  MedicineIntakeRepository intakeRepo = MockMedicineIntakeRepository();
+
+  @override
+  MedicineRepository medRepo = MockMedicineRepository();
+
+  @override
+  NoteRepository noteRepo = MockNoteRepository();
+
+  @override
+  BodyweightRepository weightRepo = MockBodyweightRepository();
+}
+
+class _MockRepo<T> extends Repository<T> {
+  List<T> data = [];
+
+  @override
+  Future<void> add(T value) async => data.add(value);
+
+  @override
+  Future<List<T>> get(DateRange range) async => data;
+
+  @override
+  Future<void> remove(T value) async => data.remove(value);
+
+  @override
+  Stream subscribe() => const Stream.empty(); // FIXME
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => throw Exception('unexpected call: $invocation');
 }
+
+class MockBloodPressureRepository extends _MockRepo<BloodPressureRecord> implements BloodPressureRepository {}
+class MockMedicineIntakeRepository extends _MockRepo<MedicineIntake> implements MedicineIntakeRepository {}
+class MockMedicineRepository extends _MockRepo<Medicine> implements MedicineRepository {}
+class MockNoteRepository extends _MockRepo<Note> implements NoteRepository {}
+class MockBodyweightRepository extends _MockRepo<BodyweightRecord> implements BodyweightRepository {}
app/pubspec.lock
@@ -500,18 +500,18 @@ packages:
     dependency: transitive
     description:
       name: leak_tracker
-      sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
+      sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
       url: "https://pub.dev"
     source: hosted
-    version: "10.0.7"
+    version: "10.0.5"
   leak_tracker_flutter_testing:
     dependency: transitive
     description:
       name: leak_tracker_flutter_testing
-      sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
+      sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.8"
+    version: "3.0.5"
   leak_tracker_testing:
     dependency: transitive
     description:
@@ -852,7 +852,7 @@ packages:
     dependency: transitive
     description: flutter
     source: sdk
-    version: "0.0.0"
+    version: "0.0.99"
   source_gen:
     dependency: transitive
     description:
@@ -1097,10 +1097,10 @@ packages:
     dependency: transitive
     description:
       name: vm_service
-      sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+      sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
       url: "https://pub.dev"
     source: hosted
-    version: "14.2.5"
+    version: "14.2.4"
   watcher:
     dependency: transitive
     description: