Share one note form between capture and the detail screen
Some checks are pending
file-length / file length <= 250 lines (push) Waiting to run
release-apk / release (APK) (push) Waiting to run

Editing an existing note looked nothing like capturing a new one: it
always showed the template dropdown, the View/Guided/Raw pills and the
priority/status row. The cause was one missing argument -- both screens
already embedded the same NoteEditor, but NoteDetailScreen never passed
advancedMode, which defaults to true in the widget while the app-wide
AppSettings.advancedMode defaults to false.

Extract the shared form as three single-purpose components rather than
one widget owning several jobs:

- note_meta_row.dart renders the priority/status row (was
  capture_meta_row.dart; drops the private _MetaDropdown duplicate the
  detail screen carried, which used DropdownButtonFormField where the
  shared one uses DropdownButton).
- note_form_chrome_gate.dart owns chrome visibility and nothing else,
  replacing the _chromeVisible flag both screens copy-pasted.
- note_form.dart composes those two with NoteEditor.

appSettings is now a required parameter through NotesListScreen and
NoteDetailScreen. Required, not defaulted: a silent default is exactly
what caused this, so it must not be reintroducible.

Also in the capture app bar: drop the decorative Icons.list_alt_outlined
beside the note count, since the adjacent Icons.list button already names
what is counted, and move the count to the right of that button. The
finalize-and-clear action becomes Icons.check instead of Icons.add -- it
never saved anything (every keystroke is already persisted), so a plus
sign misdescribed it.

314 tests, 100% line coverage. Verified in a running web build that edit
and capture render identically with advanced mode both off and on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-14 17:48:39 +02:00
parent 4e66de5467
commit 85a45daaee
13 changed files with 299 additions and 181 deletions

View File

@ -68,30 +68,10 @@ class CaptureAppBar extends StatelessWidget implements PreferredSizeWidget {
),
),
// coverage:ignore-end
// Live count of stored notes, proving local persistence.
StreamBuilder<int>(
stream: repository.watchCount(),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Padding(
padding: const EdgeInsets.only(right: 4),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.list_alt_outlined, size: 18),
const SizedBox(width: 4),
Text('$count'),
],
),
),
);
},
),
IconButton(
tooltip: 'New note',
onPressed: onNewNote,
icon: const Icon(Icons.add),
icon: const Icon(Icons.check),
),
if (advanced)
IconButton(
@ -110,6 +90,19 @@ class CaptureAppBar extends StatelessWidget implements PreferredSizeWidget {
onPressed: onOpenList,
icon: const Icon(Icons.list),
),
// Live count of stored notes, proving local persistence. Sits directly
// right of the Notes button it belongs to, and carries no icon of its
// own -- that button's Icons.list already names what is being counted.
StreamBuilder<int>(
stream: repository.watchCount(),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Padding(
padding: const EdgeInsets.only(right: 4),
child: Center(child: Text('$count')),
);
},
),
IconButton(
tooltip: 'Settings',
onPressed: onOpenSettings,

View File

@ -13,11 +13,10 @@ import 'package:todo/sync/local_backup.dart';
import 'package:todo/sync/sync_settings.dart';
import 'package:todo/ui/capture_app_bar.dart';
import 'package:todo/ui/capture_draft.dart';
import 'package:todo/ui/capture_meta_row.dart';
import 'package:todo/ui/capture_status.dart';
import 'package:todo/ui/capture_status_footer.dart';
import 'package:todo/ui/capture_sync_runner.dart';
import 'package:todo/ui/note_editor.dart';
import 'package:todo/ui/note_form.dart';
import 'package:todo/ui/notes_list_screen.dart';
import 'package:todo/ui/settings_screen.dart';
@ -75,10 +74,6 @@ class _CaptureScreenState extends State<CaptureScreen>
/// hammer GitHub with a request per focus change.
Timer? _autoSyncDebounce;
/// Hides the Priority/Status row while the editor's own bare-guided chrome
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
bool _chromeVisible = true;
/// When this session started, for the duration on the `app_close` event.
final DateTime _sessionStart = DateTime.now();
@ -232,12 +227,6 @@ class _CaptureScreenState extends State<CaptureScreen>
}
}
/// Mirrors the editor's chrome visibility so this screen's own
/// priority/status row hides and shows in step with it.
void _setChromeVisible(bool visible) {
setState(() => _chromeVisible = visible);
}
/// setState, reachable from this screen's `part` files.
///
/// setState is protected to State subclasses, so an extension in a part

View File

@ -54,10 +54,11 @@ extension _CaptureSyncBehaviour on _CaptureScreenState {
void _openList() {
_logEvent('screen_view', params: {'screen': 'notes_list'});
unawaited(
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => NotesListScreen(repository: widget.repository),
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => NotesListScreen(
repository: widget.repository,
appSettings: widget.appSettings,
),
),
);
@ -93,36 +94,22 @@ extension _CaptureSyncBehaviour on _CaptureScreenState {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Hidden together with the editor's own chrome while the
// bare guided stepper or its entry wizard is up, so the top
// of the screen stays free of noise. Also hidden outright
// when advanced mode is off.
if (advanced && _chromeVisible) ...[
CaptureMetaRow(
Expanded(
// Bumping this key is how "new note" gets a fresh editor: it
// remounts NoteForm, which also resets the chrome gate to
// visible. Keep it here rather than inside NoteForm — a key
// below the conditional metadata row can't be matched after
// advancedMode toggles that row in or out, so Flutter tears
// the editor down and remounts it with an empty draft,
// discarding whatever the user was typing (fixed in ce2ea5b).
key: ValueKey(_editorGeneration),
child: NoteForm(
advancedMode: advanced,
initialTemplate: NoteTemplate.defaultTemplate,
priority: _draft.priority,
status: _draft.status,
onPriorityChanged: _setPriority,
onStatusChanged: _setStatus,
),
const SizedBox(height: 12),
],
Expanded(
// The key lives on this Expanded, not on NoteEditor itself:
// the Row above is conditionally present, which shifts
// this Expanded's position in the Column whenever
// advancedMode changes at runtime. A key one level too
// deep (previously on NoteEditor) doesn't survive that
// reposition — Flutter can't match it at the new index,
// so it tears down and remounts NoteEditor with an empty
// draft, discarding whatever the user was typing.
key: ValueKey(_editorGeneration),
child: NoteEditor(
initialTemplate: NoteTemplate.defaultTemplate,
initialMode: NoteEditorMode.raw,
advancedMode: advanced,
priority: _draft.priority,
onPriorityChanged: _setPriority,
onChromeVisibleChanged: _setChromeVisible,
autofocus: true,
onChanged: _onChanged,
),
@ -172,9 +159,10 @@ extension _CaptureSyncBehaviour on _CaptureScreenState {
void _saveAndReset() {
_logEvent('new_note_reset');
_rebuild(() {
// Bumping the generation remounts NoteForm, which resets the chrome
// gate to visible for free -- no separate flag to keep in step.
_editorGeneration++; // recreate the editor with a fresh template
_draft.reset();
_chromeVisible = true;
});
}

View File

@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/data/note_template.dart';
import 'package:todo/ui/confirm.dart';
import 'package:todo/ui/note_editor.dart';
import 'package:todo/ui/note_form.dart';
/// Full-screen view of a single note: read it in full, edit its body through
/// the guided [NoteEditor], change its priority/status, or delete it.
/// the shared [NoteForm], change its priority/status, or delete it.
///
/// Edits persist immediately (matching the capture screen's autosave), so
/// there is no explicit save button. The template is detected from the note's
@ -17,6 +18,7 @@ class NoteDetailScreen extends StatefulWidget {
const NoteDetailScreen({
required this.note,
required this.repository,
required this.appSettings,
super.key,
});
@ -26,6 +28,11 @@ class NoteDetailScreen extends StatefulWidget {
/// The store edits persist to.
final NoteRepository repository;
/// Drives whether the metadata row and editor chrome are shown. Required,
/// not defaulted: this screen silently defaulting it to "advanced" is
/// exactly what made editing look different from capture.
final ValueNotifier<AppSettings> appSettings;
@override
State<NoteDetailScreen> createState() => _NoteDetailScreenState();
}
@ -33,10 +40,6 @@ class NoteDetailScreen extends StatefulWidget {
class _NoteDetailScreenState extends State<NoteDetailScreen> {
late Note _note = widget.note;
/// Hides the Priority/Status row while the editor's own bare-guided chrome
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
bool _chromeVisible = true;
Future<void> _persist(Note next) async {
setState(() => _note = next);
await widget.repository.upsert(next);
@ -72,92 +75,24 @@ class _NoteDetailScreenState extends State<NoteDetailScreen> {
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_chromeVisible) ...[
Row(
children: [
Expanded(
child: _MetaDropdown<Priority>(
label: 'Priority',
value: _note.priority,
values: Priority.values,
labelOf: (p) => p.label,
onChanged: (p) => _persist(
_note.copyWith(priority: p, updatedAt: DateTime.now()),
),
),
),
const SizedBox(width: 12),
Expanded(
child: _MetaDropdown<Status>(
label: 'Status',
value: _note.status,
values: Status.values,
labelOf: (s) => s.label,
onChanged: (s) => _persist(
_note.copyWith(status: s, updatedAt: DateTime.now()),
),
),
),
],
),
const SizedBox(height: 12),
],
Expanded(
child: NoteEditor(
initialText: _note.text,
initialMode: NoteEditorMode.raw,
priority: _note.priority,
onPriorityChanged: (p) => _persist(
_note.copyWith(priority: p, updatedAt: DateTime.now()),
),
onChromeVisibleChanged: (visible) =>
setState(() => _chromeVisible = visible),
onChanged: _onTextChanged,
),
// The same form capture renders, gated on the same setting, so an
// edited note and a fresh one cannot look different.
child: ValueListenableBuilder<AppSettings>(
valueListenable: widget.appSettings,
builder: (context, settings, _) => NoteForm(
advancedMode: settings.advancedMode,
initialText: _note.text,
priority: _note.priority,
status: _note.status,
onPriorityChanged: (p) => _persist(
_note.copyWith(priority: p, updatedAt: DateTime.now()),
),
],
onStatusChanged: (s) =>
_persist(_note.copyWith(status: s, updatedAt: DateTime.now())),
onChanged: _onTextChanged,
),
),
),
);
}
}
/// A compact labelled dropdown for picking an enum value (priority/status).
class _MetaDropdown<T> extends StatelessWidget {
const _MetaDropdown({
required this.label,
required this.value,
required this.values,
required this.labelOf,
required this.onChanged,
});
final String label;
final T value;
final List<T> values;
final String Function(T) labelOf;
final ValueChanged<T> onChanged;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<T>(
initialValue: value,
decoration: InputDecoration(
labelText: label,
isDense: true,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: [
for (final v in values)
DropdownMenuItem<T>(value: v, child: Text(labelOf(v))),
],
onChanged: (v) {
if (v != null) onChanged(v);
},
);
}
}

99
lib/ui/note_form.dart Normal file
View File

@ -0,0 +1,99 @@
/// Composes the note-editing form shared by capture and the detail screen.
///
/// Composition only, no logic of its own: [NoteFormChromeGate] decides
/// visibility, [NoteMetaRow] renders the priority/status row, and [NoteEditor]
/// owns the text. Both surfaces render this so an edited note and a fresh
/// capture cannot look different -- the divergence this replaced was the
/// detail screen defaulting `advancedMode` to true while the app-wide setting
/// defaults to false.
library;
import 'package:flutter/material.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_template.dart';
import 'package:todo/ui/note_editor.dart';
import 'package:todo/ui/note_form_chrome_gate.dart';
import 'package:todo/ui/note_meta_row.dart';
/// The metadata row above a [NoteEditor], gated on advanced mode.
class NoteForm extends StatelessWidget {
/// Creates the form for the given note values.
const NoteForm({
required this.advancedMode,
required this.priority,
required this.status,
required this.onPriorityChanged,
required this.onStatusChanged,
required this.onChanged,
this.initialText = '',
this.initialTemplate,
this.autofocus = false,
super.key,
});
/// Whether the metadata row and the editor's own chrome may be shown.
final bool advancedMode;
/// The note's current priority.
final Priority priority;
/// The note's current status.
final Status status;
/// Called with a newly picked priority.
final ValueChanged<Priority> onPriorityChanged;
/// Called with a newly picked status.
final ValueChanged<Status> onStatusChanged;
/// Called with the freshly assembled note text on every edit.
final ValueChanged<String> onChanged;
/// Existing note text to load. Empty for a fresh draft.
final String initialText;
/// Template to author with. Null detects it from [initialText], which is
/// what opening an existing note wants.
final NoteTemplate? initialTemplate;
/// Autofocus the editor, so a fresh capture needs zero taps before typing.
final bool autofocus;
@override
Widget build(BuildContext context) {
return NoteFormChromeGate(
advancedMode: advancedMode,
builder:
(context, {required chromeVisible, required onChromeVisibleChanged}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (chromeVisible) ...[
NoteMetaRow(
priority: priority,
status: status,
onPriorityChanged: onPriorityChanged,
onStatusChanged: onStatusChanged,
),
const SizedBox(height: 12),
],
Expanded(
child: NoteEditor(
initialText: initialText,
initialTemplate: initialTemplate,
initialMode: NoteEditorMode.raw,
advancedMode: advancedMode,
priority: priority,
onPriorityChanged: onPriorityChanged,
onChromeVisibleChanged: onChromeVisibleChanged,
autofocus: autofocus,
onChanged: onChanged,
),
),
],
);
},
);
}
}

View File

@ -0,0 +1,58 @@
/// Decides whether a note form's chrome (the priority/status row) is visible.
///
/// This is the one piece of state capture and the detail screen used to keep
/// separately, in copy-pasted form -- which is how the two drifted apart. It
/// lives here so both surfaces resolve visibility by the same rule.
library;
import 'package:flutter/material.dart';
/// Holds the editor's chrome-visibility signal and resolves it against
/// advanced mode.
///
/// Decides visibility and nothing else: it neither builds the metadata row nor
/// the editor. [builder] receives the resolved answer plus the callback to
/// hand to `NoteEditor.onChromeVisibleChanged`.
class NoteFormChromeGate extends StatefulWidget {
/// Creates a gate resolving visibility against [advancedMode].
const NoteFormChromeGate({
required this.advancedMode,
required this.builder,
super.key,
});
/// When false the chrome is never shown, whatever the editor reports --
/// casual capture needs neither the metadata row nor the mode toggle.
final bool advancedMode;
/// Built with the resolved visibility and the editor's visibility callback.
final Widget Function(
BuildContext context, {
required bool chromeVisible,
required ValueChanged<bool> onChromeVisibleChanged,
})
builder;
@override
State<NoteFormChromeGate> createState() => _NoteFormChromeGateState();
}
class _NoteFormChromeGateState extends State<NoteFormChromeGate> {
/// What the editor last reported. Starts true because a freshly mounted
/// editor shows its chrome until Guided (wizard or bare stepper) takes over.
bool _editorChromeVisible = true;
void _setEditorChromeVisible(bool visible) {
if (visible == _editorChromeVisible) return;
setState(() => _editorChromeVisible = visible);
}
@override
Widget build(BuildContext context) {
return widget.builder(
context,
chromeVisible: widget.advancedMode && _editorChromeVisible,
onChromeVisibleChanged: _setEditorChromeVisible,
);
}
}

View File

@ -1,8 +1,8 @@
/// The capture screen's metadata controls: the priority/status row and the
/// generic dropdown behind it.
/// A note's metadata controls: the priority/status row and the generic
/// dropdown behind it.
///
/// Split out of `capture_status.dart` for file size. Public only because Dart
/// privacy is library-scoped.
/// Shared by both editing surfaces -- capture and the detail screen -- so the
/// two cannot drift apart. Public only because Dart privacy is library-scoped.
library;
import 'package:flutter/material.dart';
@ -66,14 +66,13 @@ class MetaDropdown<T> extends StatelessWidget {
}
}
/// The draft's priority and status pickers, side by side.
/// A note's priority and status pickers, side by side.
///
/// Hidden together with the editor's own chrome while the bare guided
/// stepper or its entry wizard is up, so the top of the screen stays free of
/// noise -- the screen decides that; this only lays the row out.
class CaptureMetaRow extends StatelessWidget {
/// Creates the metadata row for the current draft values.
const CaptureMetaRow({
/// Lays the row out and nothing else: whether it is visible at all is
/// [NoteFormChromeGate]'s job, not this widget's.
class NoteMetaRow extends StatelessWidget {
/// Creates the metadata row for the current values.
const NoteMetaRow({
required this.priority,
required this.status,
required this.onPriorityChanged,
@ -81,10 +80,10 @@ class CaptureMetaRow extends StatelessWidget {
super.key,
});
/// The draft's current priority.
/// The note's current priority.
final Priority priority;
/// The draft's current status.
/// The note's current status.
final Status status;
/// Called with a newly picked priority.

View File

@ -7,6 +7,7 @@ library;
import 'package:flutter/material.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/ui/note_actions_sheet.dart';
@ -17,10 +18,15 @@ Future<void> openNote(
BuildContext context,
Note note,
NoteRepository repository,
ValueNotifier<AppSettings> appSettings,
) async {
await Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (_) => NoteDetailScreen(note: note, repository: repository),
builder: (_) => NoteDetailScreen(
note: note,
repository: repository,
appSettings: appSettings,
),
),
);
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/ui/note_tile.dart';
@ -21,11 +22,19 @@ const Set<Status> kDefaultStatuses = {Status.todo, Status.inProgress};
/// rebuild (e.g. a search keystroke) does not churn a new DB subscription.
class NotesListScreen extends StatefulWidget {
/// Creates a [NotesListScreen] backed by [repository].
const NotesListScreen({required this.repository, super.key});
const NotesListScreen({
required this.repository,
required this.appSettings,
super.key,
});
/// The store this screen searches/filters/sorts.
final NoteRepository repository;
/// Passed through to the detail screen so an opened note is edited with the
/// same chrome capture uses.
final ValueNotifier<AppSettings> appSettings;
@override
State<NotesListScreen> createState() => _NotesListScreenState();
}
@ -207,8 +216,12 @@ class _NotesListScreenState extends State<NotesListScreen> {
itemBuilder: (context, i) => NoteTile(
key: ValueKey(notes[i].id),
note: notes[i],
onTap: () =>
openNote(context, notes[i], widget.repository),
onTap: () => openNote(
context,
notes[i],
widget.repository,
widget.appSettings,
),
onActions: () =>
openNoteActions(context, notes[i], widget.repository),
),

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.21+1
version: 1.0.22+1
environment:
sdk: ^3.12.2

View File

@ -175,10 +175,10 @@ void main() {
);
await tester.pump();
// The Row above the editor is conditionally present, which shifts the
// editor's position in the Column whenever advancedMode changes at
// runtime — a key one level too shallow doesn't survive that
// reposition and Flutter remounts the editor with an empty draft.
// NoteForm's metadata row is conditionally present, so toggling
// advancedMode changes the editor's position beneath it — a key one
// level too shallow doesn't survive that reposition and Flutter
// remounts the editor with an empty draft, losing the typing.
appSettings.value = const AppSettings(advancedMode: false);
await tester.pump();

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note.dart';
import 'package:todo/ui/note_detail_screen.dart';
@ -15,7 +16,11 @@ void main() {
updatedAt: DateTime(2026, 6, 15, 9),
);
Future<FakeNoteRepository> pumpDetail(WidgetTester tester, Note note) async {
Future<FakeNoteRepository> pumpDetail(
WidgetTester tester,
Note note, {
bool advancedMode = true,
}) async {
tester.view.physicalSize = const Size(1200, 2400);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
@ -23,9 +28,15 @@ void main() {
final repo = FakeNoteRepository([note]);
addTearDown(repo.close);
final settings = ValueNotifier(AppSettings(advancedMode: advancedMode));
addTearDown(settings.dispose);
await tester.pumpWidget(
MaterialApp(
home: NoteDetailScreen(note: note, repository: repo),
home: NoteDetailScreen(
note: note,
repository: repo,
appSettings: settings,
),
),
);
await tester.pump();
@ -46,7 +57,7 @@ void main() {
final repo = await pumpDetail(tester, seedNote('# T'));
await tester.tap(
find.byWidgetPredicate((w) => w is DropdownButtonFormField<Priority>),
find.byWidgetPredicate((w) => w is DropdownButton<Priority>),
);
await tester.pumpAndSettle();
await tester.tap(find.text('High').last);
@ -59,7 +70,7 @@ void main() {
final repo = await pumpDetail(tester, seedNote('# T'));
await tester.tap(
find.byWidgetPredicate((w) => w is DropdownButtonFormField<Status>),
find.byWidgetPredicate((w) => w is DropdownButton<Status>),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Done').last);
@ -109,6 +120,28 @@ void main() {
},
);
// The regression this screen was built to stop: it used to let
// NoteEditor.advancedMode default to true, so editing an existing note
// showed the template dropdown and the View/Guided/Raw pills even though
// capture -- with the same app-wide setting off -- showed a bare box.
testWidgets('advanced mode off hides every chrome control, as capture does', (
tester,
) async {
await pumpDetail(
tester,
seedNote('# T\n\n## what\nbody'),
advancedMode: false,
);
expect(find.text('Template'), findsNothing);
expect(find.text('Guided'), findsNothing);
expect(find.text('Raw'), findsNothing);
expect(find.text('Priority'), findsNothing);
expect(find.text('Status'), findsNothing);
// The body itself is still editable — only the chrome is gone.
expect(find.byType(TextField), findsOneWidget);
});
testWidgets('the delete action removes the note and pops', (tester) async {
final repo = await pumpDetail(tester, seedNote('# Bye'));

View File

@ -7,8 +7,8 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/app_settings.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
import 'package:todo/ui/notes_list_screen.dart';
import 'fake_note_repository.dart';
@ -33,11 +33,16 @@ Note note(
Future<FakeNoteRepository> pumpList(
WidgetTester tester, {
List<Note> seed = const [],
bool advancedMode = true,
}) async {
final repo = FakeNoteRepository(seed);
addTearDown(repo.close);
final settings = ValueNotifier(AppSettings(advancedMode: advancedMode));
addTearDown(settings.dispose);
await tester.pumpWidget(
MaterialApp(home: NotesListScreen(repository: repo)),
MaterialApp(
home: NotesListScreen(repository: repo, appSettings: settings),
),
);
await tester.pump(); // flush the initial stream emit
return repo;