Initial commit

This commit is contained in:
2026-02-23 13:02:22 +03:00
commit 42d2900df9
108 changed files with 4491 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
// google.protobuf.Empty — пустое сообщение для RPC без возврата данных.
import 'package:protobuf/protobuf.dart' as $pb;
class Empty extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'Empty',
package: const $pb.PackageName('google.protobuf'),
createEmptyInstance: create,
)..hasRequiredFields = false;
Empty._() : super();
factory Empty() => create();
static Empty create() => Empty._();
@override
$pb.BuilderInfo get info_ => _i;
@override
Empty createEmptyInstance() => create();
@override
Empty clone() => Empty()..mergeFromMessage(this);
}

View File

@@ -0,0 +1,318 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: notes/v1/notes.proto
//
// ЭТО СГЕНЕРИРОВАННЫЙ ФАЙЛ — не редактируй вручную.
// Перегенерируй командой: cd proto && buf generate
//
// Для демо: создан вручную, чтобы проект компилировался.
// В реальном проекте — автогенерация из proto.
import 'package:protobuf/protobuf.dart' as $pb;
/// Note — заметка. Сгенерирована из message Note в proto.
/// Тот же тип, что и на Go стороне. Один proto — два языка.
class Note extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'Note',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..aOS(1, 'id')
..aOS(2, 'title')
..aOS(3, 'content')
..aOS(4, 'createdAt')
..aOS(5, 'updatedAt')
..hasRequiredFields = false;
Note._() : super();
factory Note({
String? id,
String? title,
String? content,
String? createdAt,
String? updatedAt,
}) {
final result = create();
if (id != null) result.id = id;
if (title != null) result.title = title;
if (content != null) result.content = content;
if (createdAt != null) result.createdAt = createdAt;
if (updatedAt != null) result.updatedAt = updatedAt;
return result;
}
static Note create() => Note._();
static $pb.PbList<Note> createRepeated() => $pb.PbList<Note>();
@override
$pb.BuilderInfo get info_ => _i;
@override
Note createEmptyInstance() => create();
@override
Note clone() => Note()..mergeFromMessage(this);
@$pb.TagNumber(1)
String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id(String v) => $_setString(0, v);
@$pb.TagNumber(2)
String get title => $_getSZ(1);
@$pb.TagNumber(2)
set title(String v) => $_setString(1, v);
@$pb.TagNumber(3)
String get content => $_getSZ(2);
@$pb.TagNumber(3)
set content(String v) => $_setString(2, v);
@$pb.TagNumber(4)
String get createdAt => $_getSZ(3);
@$pb.TagNumber(4)
set createdAt(String v) => $_setString(3, v);
@$pb.TagNumber(5)
String get updatedAt => $_getSZ(4);
@$pb.TagNumber(5)
set updatedAt(String v) => $_setString(4, v);
}
class CreateNoteRequest extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'CreateNoteRequest',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..aOS(1, 'title')
..aOS(2, 'content')
..hasRequiredFields = false;
CreateNoteRequest._() : super();
factory CreateNoteRequest({String? title, String? content}) {
final result = create();
if (title != null) result.title = title;
if (content != null) result.content = content;
return result;
}
static CreateNoteRequest create() => CreateNoteRequest._();
@override
$pb.BuilderInfo get info_ => _i;
@override
CreateNoteRequest createEmptyInstance() => create();
@override
CreateNoteRequest clone() => CreateNoteRequest()..mergeFromMessage(this);
@$pb.TagNumber(1)
String get title => $_getSZ(0);
@$pb.TagNumber(1)
set title(String v) => $_setString(0, v);
@$pb.TagNumber(2)
String get content => $_getSZ(1);
@$pb.TagNumber(2)
set content(String v) => $_setString(1, v);
}
class GetNoteRequest extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'GetNoteRequest',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..aOS(1, 'id')
..hasRequiredFields = false;
GetNoteRequest._() : super();
factory GetNoteRequest({String? id}) {
final result = create();
if (id != null) result.id = id;
return result;
}
static GetNoteRequest create() => GetNoteRequest._();
@override
$pb.BuilderInfo get info_ => _i;
@override
GetNoteRequest createEmptyInstance() => create();
@override
GetNoteRequest clone() => GetNoteRequest()..mergeFromMessage(this);
@$pb.TagNumber(1)
String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id(String v) => $_setString(0, v);
}
class UpdateNoteRequest extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'UpdateNoteRequest',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..aOS(1, 'id')
..aOS(2, 'title')
..aOS(3, 'content')
..hasRequiredFields = false;
UpdateNoteRequest._() : super();
factory UpdateNoteRequest({String? id, String? title, String? content}) {
final result = create();
if (id != null) result.id = id;
if (title != null) result.title = title;
if (content != null) result.content = content;
return result;
}
static UpdateNoteRequest create() => UpdateNoteRequest._();
@override
$pb.BuilderInfo get info_ => _i;
@override
UpdateNoteRequest createEmptyInstance() => create();
@override
UpdateNoteRequest clone() => UpdateNoteRequest()..mergeFromMessage(this);
@$pb.TagNumber(1)
String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id(String v) => $_setString(0, v);
@$pb.TagNumber(2)
String get title => $_getSZ(1);
@$pb.TagNumber(2)
set title(String v) => $_setString(1, v);
@$pb.TagNumber(3)
String get content => $_getSZ(2);
@$pb.TagNumber(3)
set content(String v) => $_setString(2, v);
}
class DeleteNoteRequest extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'DeleteNoteRequest',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..aOS(1, 'id')
..hasRequiredFields = false;
DeleteNoteRequest._() : super();
factory DeleteNoteRequest({String? id}) {
final result = create();
if (id != null) result.id = id;
return result;
}
static DeleteNoteRequest create() => DeleteNoteRequest._();
@override
$pb.BuilderInfo get info_ => _i;
@override
DeleteNoteRequest createEmptyInstance() => create();
@override
DeleteNoteRequest clone() => DeleteNoteRequest()..mergeFromMessage(this);
@$pb.TagNumber(1)
String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id(String v) => $_setString(0, v);
}
class ListNotesRequest extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'ListNotesRequest',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..a<int>(1, 'page', $pb.PbFieldType.O3)
..a<int>(2, 'pageSize', $pb.PbFieldType.O3)
..hasRequiredFields = false;
ListNotesRequest._() : super();
factory ListNotesRequest({int? page, int? pageSize}) {
final result = create();
if (page != null) result.page = page;
if (pageSize != null) result.pageSize = pageSize;
return result;
}
static ListNotesRequest create() => ListNotesRequest._();
@override
$pb.BuilderInfo get info_ => _i;
@override
ListNotesRequest createEmptyInstance() => create();
@override
ListNotesRequest clone() => ListNotesRequest()..mergeFromMessage(this);
@$pb.TagNumber(1)
int get page => $_getIZ(0);
@$pb.TagNumber(1)
set page(int v) => $_setSignedInt32(0, v);
@$pb.TagNumber(2)
int get pageSize => $_getIZ(1);
@$pb.TagNumber(2)
set pageSize(int v) => $_setSignedInt32(1, v);
}
class ListNotesResponse extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
'ListNotesResponse',
package: const $pb.PackageName('notes.v1'),
createEmptyInstance: create,
)
..pc<Note>(1, 'notes', $pb.PbFieldType.PM, subBuilder: Note.create)
..a<int>(2, 'totalCount', $pb.PbFieldType.O3)
..hasRequiredFields = false;
ListNotesResponse._() : super();
factory ListNotesResponse({List<Note>? notes, int? totalCount}) {
final result = create();
if (notes != null) result.notes.addAll(notes);
if (totalCount != null) result.totalCount = totalCount;
return result;
}
static ListNotesResponse create() => ListNotesResponse._();
@override
$pb.BuilderInfo get info_ => _i;
@override
ListNotesResponse createEmptyInstance() => create();
@override
ListNotesResponse clone() => ListNotesResponse()..mergeFromMessage(this);
@$pb.TagNumber(1)
$pb.PbList<Note> get notes => $_getList(0);
@$pb.TagNumber(2)
int get totalCount => $_getIZ(1);
@$pb.TagNumber(2)
set totalCount(int v) => $_setSignedInt32(1, v);
}

View File

@@ -0,0 +1,91 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: notes/v1/notes.proto
//
// ЭТО СГЕНЕРИРОВАННЫЙ ФАЙЛ — gRPC клиентский код.
// В реальном проекте генерируется из proto.
//
// NoteServiceClient — типизированный клиент.
// Dart знает сигнатуры всех RPC методов.
// Если proto изменится и ты перегенерируешь —
// все вызовы с неверными типами дадут ошибку компиляции.
import 'package:grpc/grpc.dart' as $grpc;
import 'empty.pb.dart' as $empty;
import 'notes.pb.dart' as $0;
/// Типизированный gRPC клиент — сгенерирован из service NoteService.
/// Каждый метод соответствует rpc из proto файла.
class NoteServiceClient extends $grpc.Client {
static final _$createNote =
$grpc.ClientMethod<$0.CreateNoteRequest, $0.Note>(
'/notes.v1.NoteService/CreateNote',
($0.CreateNoteRequest value) => value.writeToBuffer(),
(List<int> value) => $0.Note()..mergeFromBuffer(value),
);
static final _$getNote = $grpc.ClientMethod<$0.GetNoteRequest, $0.Note>(
'/notes.v1.NoteService/GetNote',
($0.GetNoteRequest value) => value.writeToBuffer(),
(List<int> value) => $0.Note()..mergeFromBuffer(value),
);
static final _$updateNote =
$grpc.ClientMethod<$0.UpdateNoteRequest, $0.Note>(
'/notes.v1.NoteService/UpdateNote',
($0.UpdateNoteRequest value) => value.writeToBuffer(),
(List<int> value) => $0.Note()..mergeFromBuffer(value),
);
static final _$deleteNote =
$grpc.ClientMethod<$0.DeleteNoteRequest, $empty.Empty>(
'/notes.v1.NoteService/DeleteNote',
($0.DeleteNoteRequest value) => value.writeToBuffer(),
(List<int> value) => $empty.Empty()..mergeFromBuffer(value),
);
static final _$listNotes =
$grpc.ClientMethod<$0.ListNotesRequest, $0.ListNotesResponse>(
'/notes.v1.NoteService/ListNotes',
($0.ListNotesRequest value) => value.writeToBuffer(),
(List<int> value) => $0.ListNotesResponse()..mergeFromBuffer(value),
);
NoteServiceClient(
super.channel, {
super.options,
super.interceptors,
});
/// Создать заметку
$grpc.ResponseFuture<$0.Note> createNote($0.CreateNoteRequest request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$createNote, request, options: options);
}
/// Получить заметку по ID
$grpc.ResponseFuture<$0.Note> getNote($0.GetNoteRequest request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$getNote, request, options: options);
}
/// Обновить заметку
$grpc.ResponseFuture<$0.Note> updateNote($0.UpdateNoteRequest request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$updateNote, request, options: options);
}
/// Удалить заметку
$grpc.ResponseFuture<$empty.Empty> deleteNote(
$0.DeleteNoteRequest request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$deleteNote, request, options: options);
}
/// Получить список заметок
$grpc.ResponseFuture<$0.ListNotesResponse> listNotes(
$0.ListNotesRequest request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$listNotes, request, options: options);
}
}

24
frontend/lib/main.dart Normal file
View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'screens/notes_screen.dart';
void main() {
runApp(const NotesApp());
}
class NotesApp extends StatelessWidget {
const NotesApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Notes — Flutter + Go gRPC',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const NotesScreen(),
);
}
}

View File

@@ -0,0 +1,202 @@
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<NotesScreen> createState() => _NotesScreenState();
}
class _NotesScreenState extends State<NotesScreen> {
final _client = GrpcClient.instance.noteService;
List<Note> _notes = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadNotes();
}
Future<void> _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<void> _createNote() async {
final titleController = TextEditingController();
final contentController = TextEditingController();
final confirmed = await showDialog<bool>(
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<void> _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),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:grpc/grpc.dart';
import '../gen/notes/v1/notes.pbgrpc.dart';
/// Singleton gRPC клиент.
/// Подключается к Go бекенду на localhost:50051.
class GrpcClient {
static GrpcClient? _instance;
late final ClientChannel _channel;
late final NoteServiceClient noteService;
GrpcClient._() {
_channel = ClientChannel(
'localhost',
port: 50051,
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
),
);
// noteService — типизированный клиент.
// Все методы (createNote, listNotes и т.д.)
// сгенерированы из proto и принимают/возвращают
// типизированные объекты. Невозможно передать
// неверный тип — ошибка компиляции Dart.
noteService = NoteServiceClient(_channel);
}
static GrpcClient get instance {
_instance ??= GrpcClient._();
return _instance!;
}
Future<void> shutdown() async {
await _channel.shutdown();
}
}