fix: prevent StateError crash in getTriggeredAlarm lookup (#906)#923
fix: prevent StateError crash in getTriggeredAlarm lookup (#906)#923Varvyju wants to merge 1 commit intoCCExtractor:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a crash in triggered-alarm lookup by making both ISAR and Firestore implementations return null when no enabled alarm matches the requested time, and by reducing query overhead.
Changes:
- ISAR: switch from
findAll()tofindFirst()and returnnullinstead of throwing on empty results. - Firestore: add
limit(1), guard empty snapshots, and returnnullinstead of indexing into an empty list.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| lib/app/data/providers/isar_provider.dart | Makes triggered-alarm query nullable and more efficient via findFirst() + empty-result guard. |
| lib/app/data/providers/firestore_provider.dart | Adds empty-result guard and query optimization via limit(1), returning nullable results. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .findFirst(); | ||
|
|
||
| if (alarm == null) { | ||
| debugPrint('No enabled alarm found in ISAR for time: $time'); |
There was a problem hiding this comment.
Logging a debugPrint every time no alarm matches can get noisy if this lookup runs frequently (e.g., background polling). Consider either removing this log, downgrading it behind a debug flag, or rate-limiting it to avoid log spam in normal operation.
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| if (kDebugMode) { | |
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| } |
| // static Future<AlarmModel> getTriggeredAlarm(String time) async { | ||
| // final isarProvider = IsarDb(); | ||
| // final db = await isarProvider.db; | ||
|
|
||
| // final alarms = await db.alarmModels | ||
| // .where() | ||
| // .filter() | ||
| // .isEnabledEqualTo(true) | ||
| // .and() | ||
| // .alarmTimeEqualTo(time) | ||
| // .findAll(); | ||
| // return alarms.first; | ||
| // } | ||
|
|
There was a problem hiding this comment.
The previous implementation of getTriggeredAlarm is left in the file as commented-out code. Since it’s already available in git history, please remove the dead commented-out block to reduce clutter and avoid future confusion.
| // static Future<AlarmModel> getTriggeredAlarm(String time) async { | |
| // final isarProvider = IsarDb(); | |
| // final db = await isarProvider.db; | |
| // final alarms = await db.alarmModels | |
| // .where() | |
| // .filter() | |
| // .isEnabledEqualTo(true) | |
| // .and() | |
| // .alarmTimeEqualTo(time) | |
| // .findAll(); | |
| // return alarms.first; | |
| // } |
| static Future<AlarmModel?> getTriggeredAlarm(String time) async { | ||
| final isarProvider = IsarDb(); | ||
| final db = await isarProvider.db; | ||
|
|
||
| // Use findFirst() instead of findAll() to save memory and IO | ||
| final alarm = await db.alarmModels | ||
| .where() | ||
| .filter() | ||
| .isEnabledEqualTo(true) | ||
| .and() | ||
| .alarmTimeEqualTo(time) | ||
| .findFirst(); | ||
|
|
||
| if (alarm == null) { | ||
| debugPrint('No enabled alarm found in ISAR for time: $time'); | ||
| return null; // Return null instead of throwing an error to prevent crashes | ||
| } | ||
|
|
||
| return alarm; | ||
| } | ||
|
|
There was a problem hiding this comment.
getTriggeredAlarm is not indented consistently with the rest of the IsarDb class (it starts at column 0). Please run dart format / adjust indentation so the method aligns with surrounding static methods, improving readability and keeping style consistent.
| static Future<AlarmModel?> getTriggeredAlarm(String time) async { | |
| final isarProvider = IsarDb(); | |
| final db = await isarProvider.db; | |
| // Use findFirst() instead of findAll() to save memory and IO | |
| final alarm = await db.alarmModels | |
| .where() | |
| .filter() | |
| .isEnabledEqualTo(true) | |
| .and() | |
| .alarmTimeEqualTo(time) | |
| .findFirst(); | |
| if (alarm == null) { | |
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| return null; // Return null instead of throwing an error to prevent crashes | |
| } | |
| return alarm; | |
| } | |
| static Future<AlarmModel?> getTriggeredAlarm(String time) async { | |
| final isarProvider = IsarDb(); | |
| final db = await isarProvider.db; | |
| // Use findFirst() instead of findAll() to save memory and IO | |
| final alarm = await db.alarmModels | |
| .where() | |
| .filter() | |
| .isEnabledEqualTo(true) | |
| .and() | |
| .alarmTimeEqualTo(time) | |
| .findFirst(); | |
| if (alarm == null) { | |
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| return null; // Return null instead of throwing an error to prevent crashes | |
| } | |
| return alarm; | |
| } |
| .findFirst(); | ||
|
|
||
| if (alarm == null) { | ||
| debugPrint('No enabled alarm found in ISAR for time: $time'); |
There was a problem hiding this comment.
Logging a debugPrint every time no alarm matches can get noisy if this lookup runs frequently (e.g., background polling). Consider removing this log, guarding it behind a debug-only flag, or rate-limiting it to avoid log spam in normal operation.
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| assert(() { | |
| debugPrint('No enabled alarm found in ISAR for time: $time'); | |
| return true; | |
| }()); |
| // static Future<AlarmModel> getTriggeredAlarm( | ||
| // UserModel? user, | ||
| // String time, | ||
| // ) async { | ||
| // HomeController homeController = Get.find<HomeController>(); | ||
| // if (user == null) return homeController.genFakeAlarmModel(); | ||
| // QuerySnapshot snapshot = await _alarmsCollection(user) | ||
| // .where('isEnabled', isEqualTo: true) | ||
| // .where('alarmTime', isEqualTo: time) | ||
| // .get(); | ||
|
|
||
| // List list = snapshot.docs.map((DocumentSnapshot document) { | ||
| // return AlarmModel.fromDocumentSnapshot( | ||
| // documentSnapshot: document, | ||
| // user: user, | ||
| // ); | ||
| // }).toList(); | ||
|
|
||
| // return list[0]; |
There was a problem hiding this comment.
The previous getTriggeredAlarm implementation is kept as commented-out code. Please remove the dead commented-out block (git history preserves it) to reduce noise and maintenance overhead.
| // static Future<AlarmModel> getTriggeredAlarm( | |
| // UserModel? user, | |
| // String time, | |
| // ) async { | |
| // HomeController homeController = Get.find<HomeController>(); | |
| // if (user == null) return homeController.genFakeAlarmModel(); | |
| // QuerySnapshot snapshot = await _alarmsCollection(user) | |
| // .where('isEnabled', isEqualTo: true) | |
| // .where('alarmTime', isEqualTo: time) | |
| // .get(); | |
| // List list = snapshot.docs.map((DocumentSnapshot document) { | |
| // return AlarmModel.fromDocumentSnapshot( | |
| // documentSnapshot: document, | |
| // user: user, | |
| // ); | |
| // }).toList(); | |
| // return list[0]; |
| static Future<AlarmModel?> getTriggeredAlarm(UserModel? user, String time) async { | ||
| try { | ||
| // Use their built-in _alarmsCollection function to get the correct path | ||
| QuerySnapshot querySnapshot = await _alarmsCollection(user) | ||
| .where('isEnabled', isEqualTo: true) | ||
| .where('alarmTime', isEqualTo: time) |
There was a problem hiding this comment.
This change removes the prior user == null guard (it used to return a fake model). As written, the method will query _alarmsCollection(null) (which points at the top-level alarms collection) and then build an AlarmModel with user: null. Please add an explicit user == null early return (likely null) to avoid unintended/unauthorized reads and to keep null-user behavior consistent with other methods in this provider (e.g., getLatestAlarm, getAlarm, addAlarm).
| } catch (e) { | ||
| debugPrint('Error fetching triggered alarm from Firestore: $e'); | ||
| return null; |
There was a problem hiding this comment.
Catching a broad catch (e) and returning null will also swallow unexpected non-Firestore errors (e.g., mapping/logic bugs), making defects harder to detect. Consider catching FirebaseException (or a narrower set of expected exceptions) and rethrowing unexpected exceptions after logging so real errors aren’t silently masked.
| } catch (e) { | |
| debugPrint('Error fetching triggered alarm from Firestore: $e'); | |
| return null; | |
| } on FirebaseException catch (e) { | |
| debugPrint('Error fetching triggered alarm from Firestore: $e'); | |
| return null; | |
| } catch (e, stackTrace) { | |
| debugPrint('Unexpected error fetching triggered alarm from Firestore: $e'); | |
| debugPrint('Stack trace: $stackTrace'); | |
| rethrow; |
Summary
Fixes Issue #906.
Prevents a fatal
StateErrorcrash whengetTriggeredAlarmis called and no enabled alarms match the current time.Changes Made
.findAll()query with.findFirst()for better memory and IO performance. Added a null-safety guard to returnnullgracefully instead of throwing a StateError..limit(1)to optimize the document read query, and properly returningnullif the snapshot is empty.Testing Done
flutter analyzepasses with no errors.