Add local reminder controls#2
Conversation
📝 WalkthroughWalkthroughThis PR implements complete local reminder management for the Flutter app, adding the ability to create, edit, delete, and toggle enabled/disabled state for reminders. The core ReminderSchedule model gains a copyWith helper, RemindersScreen is refactored from a read-only list into an interactive CRUD interface with modal forms, and comprehensive widget tests validate the full user flow. ChangesLocal Reminder CRUD Management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds basic local reminder management to the Flutter app so the Reminders tab is usable without notifications/BLE/cloud sync, while keeping schedules persisted in the existing Drift repository.
Changes:
- Implemented local reminder add/edit/delete and enable/disable UI in the Reminders tab (bottom sheet + list tiles).
- Added
ReminderSchedule.copyWithto support updates (e.g., toggling enabled state). - Added a widget test covering the reminder add/edit/toggle/delete flow and updated docs/READMEs to reflect the new reminder controls and mobile test checklist.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates top-level project summary to mention local reminder controls. |
| mobile_app/README.md | Notes local reminder add/edit/delete + enabled state in the mobile overview. |
| mobile_app/dosey_app/README.md | Expands app README to reflect local reminder controls and enabled state. |
| mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart | Adds reminder list tiles, bottom-sheet editor, enable/disable toggle, and delete flow. |
| mobile_app/dosey_app/lib/core/reminders/reminder_schedule.dart | Adds copyWith for schedule updates. |
| mobile_app/dosey_app/lib/app/dosey_app_scope.dart | Marks owned database close as intentionally unawaited during disposal. |
| mobile_app/dosey_app/test/widget_test.dart | Adds widget test for reminder add/edit/toggle/delete flow using an in-memory database. |
| docs/test_plan.md | Adds a “Current mobile checks” section with expected Flutter commands and widget-test coverage. |
| docs/mobile_stack.md | Updates mobile stack doc to mention local reminder controls. |
| docs/decisions.md | Records decision to keep reminder CRUD + enabled state local until notifications are wired behind the reminder interface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await reminders.upsertSchedule( | ||
| schedule.copyWith(isEnabled: value, updatedAt: DateTime.now().toUtc()), | ||
| ); | ||
| } catch (error) { |
| Future<void> _delete(BuildContext context) async { | ||
| try { | ||
| await reminders.deleteSchedule(schedule.id); | ||
| } catch (error) { |
|
|
||
| try { | ||
| await widget.reminders.upsertSchedule(schedule); | ||
| } catch (error) { |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart`:
- Around line 255-257: The Save button can trigger multiple concurrent _save()
calls and generate duplicate IDs; add a boolean flag (e.g., isSaving) in the
reminders screen state, have _save() immediately check and return if isSaving is
true, set isSaving = true at the start of _save() and reset it in a finally
block, and update the FilledButton's onPressed to be null (disabled) when
isSaving is true so repeated taps are ignored; ensure the ID generation in
create mode only runs when entering _save() once (after the isSaving guard) so
duplicates cannot be created.
In `@mobile_app/dosey_app/test/widget_test.dart`:
- Around line 77-127: Extend the existing testWidgets 'reminders tab adds edits
toggles and deletes reminders' to cover validation branches: after tapping 'Add
reminder' and populating fields, add a negative case where you clear the 'Label'
TextFormField, tap 'Save reminder', and assert a validation error is shown for
the label and that no reminder was saved (still findsOneWidget 'No reminders
yet.' or absence of the label); similarly add a case where you enter invalid
time values into the 'Hour'/'Minute' TextFormField(s) (e.g., '24' or '60'), tap
'Save reminder', and assert a time validation error is displayed and the
reminder is not created. Use the existing helpers/finders (TextFormField with
text 'Label' / 'Hour' / 'Minute', buttons 'Add reminder' / 'Save reminder', and
the page messages like 'No reminders yet.') to locate UI elements.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 81f23a36-7819-4727-ac01-f81ca801c770
📒 Files selected for processing (10)
README.mddocs/decisions.mddocs/mobile_stack.mddocs/test_plan.mdmobile_app/README.mdmobile_app/dosey_app/README.mdmobile_app/dosey_app/lib/app/dosey_app_scope.dartmobile_app/dosey_app/lib/core/reminders/reminder_schedule.dartmobile_app/dosey_app/lib/features/reminders/reminders_screen.dartmobile_app/dosey_app/test/widget_test.dart
| FilledButton( | ||
| onPressed: _save, | ||
| child: const Text('Save reminder'), |
There was a problem hiding this comment.
Prevent duplicate reminder creation from repeated Save taps.
_save is async, but the Save button remains enabled. Rapid taps can invoke _save more than once before dismissal, and Line 287 generates a new ID per invocation in create mode, causing duplicate persisted reminders.
Proposed fix
class _ReminderSheetState extends State<_ReminderSheet> {
late final TextEditingController _labelController;
late final TextEditingController _hourController;
late final TextEditingController _minuteController;
late bool _isEnabled;
+ bool _isSaving = false;
String? _errorText;
@@
FilledButton(
- onPressed: _save,
+ onPressed: _isSaving ? null : _save,
child: const Text('Save reminder'),
),
@@
Future<void> _save() async {
+ if (_isSaving) return;
+ setState(() {
+ _isSaving = true;
+ _errorText = null;
+ });
+
final label = _labelController.text.trim();
final hour = int.tryParse(_hourController.text.trim());
final minute = int.tryParse(_minuteController.text.trim());
@@
if (label.isEmpty) {
- setState(() => _errorText = 'Enter a reminder label.');
+ setState(() {
+ _errorText = 'Enter a reminder label.';
+ _isSaving = false;
+ });
return;
}
if (hour == null || hour < 0 || hour > 23) {
- setState(() => _errorText = 'Hour must be 0 through 23.');
+ setState(() {
+ _errorText = 'Hour must be 0 through 23.';
+ _isSaving = false;
+ });
return;
}
if (minute == null || minute < 0 || minute > 59) {
- setState(() => _errorText = 'Minute must be 0 through 59.');
+ setState(() {
+ _errorText = 'Minute must be 0 through 59.';
+ _isSaving = false;
+ });
return;
}
@@
try {
await widget.reminders.upsertSchedule(schedule);
} catch (error) {
if (!mounted) return;
- setState(() => _errorText = 'Reminder save failed: $error');
+ setState(() {
+ _errorText = 'Reminder save failed: $error';
+ _isSaving = false;
+ });
return;
}
if (!mounted) return;
+ setState(() => _isSaving = false);
Navigator.of(context).pop();
}
}Also applies to: 266-306
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile_app/dosey_app/lib/features/reminders/reminders_screen.dart` around
lines 255 - 257, The Save button can trigger multiple concurrent _save() calls
and generate duplicate IDs; add a boolean flag (e.g., isSaving) in the reminders
screen state, have _save() immediately check and return if isSaving is true, set
isSaving = true at the start of _save() and reset it in a finally block, and
update the FilledButton's onPressed to be null (disabled) when isSaving is true
so repeated taps are ignored; ensure the ID generation in create mode only runs
when entering _save() once (after the isSaving guard) so duplicates cannot be
created.
| testWidgets('reminders tab adds edits toggles and deletes reminders', ( | ||
| WidgetTester tester, | ||
| ) async { | ||
| final database = DoseyDatabase.inMemory(); | ||
| addTearDown(database.close); | ||
|
|
||
| await tester.pumpWidget(DoseyApp(database: database)); | ||
| await tester.tap(find.text('Reminders')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(find.text('No reminders yet.'), findsOneWidget); | ||
| expect(find.text('Add reminder'), findsOneWidget); | ||
|
|
||
| await tester.tap(find.text('Add reminder')); | ||
| await tester.pumpAndSettle(); | ||
| await tester.enterText( | ||
| find.widgetWithText(TextFormField, 'Label'), | ||
| 'Vitamin D', | ||
| ); | ||
| await tester.enterText(find.widgetWithText(TextFormField, 'Hour'), '8'); | ||
| await tester.enterText(find.widgetWithText(TextFormField, 'Minute'), '30'); | ||
| await tester.tap(find.text('Save reminder')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(find.text('Vitamin D'), findsOneWidget); | ||
| expect(find.text('08:30'), findsOneWidget); | ||
| expect(tester.widget<Switch>(find.byType(Switch)).value, isTrue); | ||
|
|
||
| await tester.tap(find.byType(Switch)); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(tester.widget<Switch>(find.byType(Switch)).value, isFalse); | ||
|
|
||
| await tester.tap(find.byTooltip('Edit reminder')); | ||
| await tester.pumpAndSettle(); | ||
| await tester.enterText( | ||
| find.widgetWithText(TextFormField, 'Label'), | ||
| 'Morning vitamin', | ||
| ); | ||
| await tester.tap(find.text('Save reminder')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(find.text('Vitamin D'), findsNothing); | ||
| expect(find.text('Morning vitamin'), findsOneWidget); | ||
|
|
||
| await tester.tap(find.byTooltip('Delete reminder')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(find.text('Morning vitamin'), findsNothing); | ||
| expect(find.text('No reminders yet.'), findsOneWidget); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add a validation-path assertion to this reminder flow test.
This test fully covers the happy path but misses the newly added validation branches (blank label / invalid time), so regressions there can slip through.
Minimal extension example
testWidgets('reminders tab adds edits toggles and deletes reminders', (
WidgetTester tester,
) async {
@@
await tester.tap(find.text('Add reminder'));
await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Save reminder'));
+ await tester.pumpAndSettle();
+ expect(find.text('Enter a reminder label.'), findsOneWidget);
+
await tester.enterText(
find.widgetWithText(TextFormField, 'Label'),
'Vitamin D',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile_app/dosey_app/test/widget_test.dart` around lines 77 - 127, Extend the
existing testWidgets 'reminders tab adds edits toggles and deletes reminders' to
cover validation branches: after tapping 'Add reminder' and populating fields,
add a negative case where you clear the 'Label' TextFormField, tap 'Save
reminder', and assert a validation error is shown for the label and that no
reminder was saved (still findsOneWidget 'No reminders yet.' or absence of the
label); similarly add a case where you enter invalid time values into the
'Hour'/'Minute' TextFormField(s) (e.g., '24' or '60'), tap 'Save reminder', and
assert a time validation error is displayed and the reminder is not created. Use
the existing helpers/finders (TextFormField with text 'Label' / 'Hour' /
'Minute', buttons 'Add reminder' / 'Save reminder', and the page messages like
'No reminders yet.') to locate UI elements.
Adds basic local reminder management to the Flutter app.
This builds on the app-basics work by making the Reminders tab usable without notifications, BLE, or cloud sync.
Changes:
Verified from
mobile_app/dosey_app/:dart format .flutter analyzeflutter testflutter build apk --debugflutter build ios --debug --no-codesignflutter doctor -vSummary by CodeRabbit
New Features
Documentation
Tests