import 'package:flutter/material.dart'; import 'package:grpc/grpc.dart'; import '../gen/notes/v1/notes.pb.dart'; import '../services/grpc_client.dart'; class NotesScreen extends StatefulWidget { const NotesScreen({super.key}); @override State createState() => _NotesScreenState(); } class _NotesScreenState extends State { final _client = GrpcClient.instance.noteService; List _notes = []; bool _loading = true; String? _error; @override void initState() { super.initState(); _loadNotes(); } Future _loadNotes() async { setState(() { _loading = true; _error = null; }); try { // Типизированный вызов — ListNotesRequest и ListNotesResponse // сгенерированы из proto. Если на Go стороне изменится // сигнатура, после перегенерации этот код не скомпилируется. final response = await _client.listNotes( ListNotesRequest(page: 1, pageSize: 50), ); setState(() { _notes = response.notes; _loading = false; }); } on GrpcError catch (e) { setState(() { _error = 'gRPC error: ${e.message}'; _loading = false; }); } catch (e) { setState(() { _error = 'Connection error: $e'; _loading = false; }); } } Future _createNote() async { final titleController = TextEditingController(); final contentController = TextEditingController(); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('New Note'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: titleController, decoration: const InputDecoration(labelText: 'Title'), autofocus: true, ), const SizedBox(height: 8), TextField( controller: contentController, decoration: const InputDecoration(labelText: 'Content'), maxLines: 3, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Create'), ), ], ), ); if (confirmed != true) return; try { // Типизированный вызов CreateNote. // CreateNoteRequest точно соответствует proto определению. await _client.createNote( CreateNoteRequest( title: titleController.text, content: contentController.text, ), ); _loadNotes(); } on GrpcError catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${e.message}')), ); } } } Future _deleteNote(Note note) async { try { await _client.deleteNote(DeleteNoteRequest(id: note.id)); _loadNotes(); } on GrpcError catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: ${e.message}')), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Notes (Flutter + Go gRPC)'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadNotes, ), ], ), floatingActionButton: FloatingActionButton( onPressed: _createNote, child: const Icon(Icons.add), ), body: _buildBody(), ); } Widget _buildBody() { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!, style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), const Text( 'Make sure Go backend is running:\n' 'cd backend && go run main.go', textAlign: TextAlign.center, ), const SizedBox(height: 16), FilledButton( onPressed: _loadNotes, child: const Text('Retry'), ), ], ), ); } if (_notes.isEmpty) { return const Center( child: Text('No notes yet. Tap + to create one.'), ); } return ListView.builder( itemCount: _notes.length, itemBuilder: (context, index) { final note = _notes[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( title: Text(note.title), subtitle: Text( note.content, maxLines: 2, overflow: TextOverflow.ellipsis, ), trailing: IconButton( icon: const Icon(Icons.delete_outline), onPressed: () => _deleteNote(note), ), ), ); }, ); } }