Fix bugs found in full code audit
- Edit screens (note, task): cache _initFuture in initState instead of
calling _loadExisting() from FutureBuilder on every rebuild. Prevents
race condition where multiple concurrent loads could overwrite edits.
Also removes unnecessary _loaded flag and simplifies the pattern.
- Note editor: remove onChanged: (_) => setState((){}) on the body
TextField — triggered a full rebuild on every keystroke for no reason.
- SSE streaming: use utf8.decode(chunk, allowMalformed: true) so a
malformed byte sequence from the server skips rather than throwing an
unhandled FormatException that escaped all catch blocks.
- Chat provider: guard msgs.isEmpty before accessing msgs.last during
SSE chunk processing to prevent a potential index error on empty state.
- Offline queue drain: check mounted before and after each async gap in
_drainQueue so ref is never accessed on a disposed widget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -245,12 +245,15 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
|
||||
}
|
||||
|
||||
Future<void> _drainQueue() async {
|
||||
if (!mounted) return;
|
||||
final queue = ref.read(captureQueueProvider);
|
||||
if (queue.isEmpty) return;
|
||||
final api = ref.read(quickCaptureApiProvider);
|
||||
for (final text in List<String>.from(queue)) {
|
||||
if (!mounted) break;
|
||||
try {
|
||||
final result = await api.capture(text);
|
||||
if (!mounted) break;
|
||||
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||
switch (result.type) {
|
||||
case 'note':
|
||||
@@ -263,7 +266,7 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
|
||||
break; // Still offline — stop draining.
|
||||
} catch (_) {
|
||||
// Server/parse error — remove to avoid infinite retries.
|
||||
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||
if (mounted) await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class ChatApi {
|
||||
String currentEvent = '';
|
||||
|
||||
await for (final chunk in stream) {
|
||||
buf.write(utf8.decode(chunk));
|
||||
buf.write(utf8.decode(chunk, allowMalformed: true));
|
||||
final raw = buf.toString();
|
||||
final lines = raw.split('\n');
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class MessagesNotifier extends FamilyAsyncNotifier<List<Message>, int> {
|
||||
await for (final chunk in repo.streamGeneration(convId)) {
|
||||
streamedContent = true;
|
||||
final msgs = state.requireValue;
|
||||
if (msgs.isEmpty) continue;
|
||||
final updated = msgs.last.copyWith(content: msgs.last.content + chunk);
|
||||
state = AsyncData([...msgs.sublist(0, msgs.length - 1), updated]);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,16 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
final _contentController = TextEditingController();
|
||||
bool _preview = false;
|
||||
bool _saving = false;
|
||||
bool _loaded = false;
|
||||
|
||||
// Future is created once in initState so FutureBuilder never restarts it.
|
||||
late final Future<void> _initFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initFuture =
|
||||
widget.noteId != null ? _loadExisting() : Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -30,15 +39,10 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadExisting() async {
|
||||
if (_loaded || widget.noteId == null) {
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
final note =
|
||||
await ref.read(notesRepositoryProvider).getOne(widget.noteId!);
|
||||
_titleController.text = note.title;
|
||||
_contentController.text = note.body;
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
@@ -97,7 +101,7 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _loadExisting(),
|
||||
future: _initFuture,
|
||||
builder: (context, snapshot) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -159,7 +163,6 @@ class _NoteEditScreenState extends ConsumerState<NoteEditScreen> {
|
||||
expands: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,16 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
||||
TaskPriority _priority = TaskPriority.medium;
|
||||
DateTime? _dueDate;
|
||||
bool _saving = false;
|
||||
bool _loaded = false;
|
||||
|
||||
// Future is created once in initState so FutureBuilder never restarts it.
|
||||
late final Future<void> _initFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initFuture =
|
||||
widget.taskId != null ? _loadExisting() : Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -33,17 +42,13 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadExisting() async {
|
||||
if (_loaded || widget.taskId == null) {
|
||||
_loaded = true;
|
||||
return;
|
||||
}
|
||||
final task = await ref.read(tasksRepositoryProvider).getOne(widget.taskId!);
|
||||
final task =
|
||||
await ref.read(tasksRepositoryProvider).getOne(widget.taskId!);
|
||||
_titleController.text = task.title;
|
||||
_descController.text = task.description ?? '';
|
||||
_status = task.status;
|
||||
_priority = task.priority;
|
||||
_dueDate = task.dueDate;
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
@@ -117,7 +122,7 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _loadExisting(),
|
||||
future: _initFuture,
|
||||
builder: (context, snapshot) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
||||
Reference in New Issue
Block a user