Привет, Хабр! Меня зовут Андрей, я Data Engineer. В свободное время я решил написать свой «велосипед» — небольшую соцсеть для поиска единомышленников по нишевым интересам.
Спойлер: Google Sign-In на Flutter Web — это отдельный вид боли, но оно того стоило.
Ниже — полная история проекта Syncory, все «грабли», с которыми я столкнулся, и 100% открытый исходный код.
Проблема: «Как найти напарника для Warhammer?»
Всё началось с простого вопроса: «Как быстро найти людей по очень нишевым интересам?»
Мне, как инженеру, не хватало платформы, где можно отфильтровать людей не по друзьям или геолокации, а по конкретным категориям интересов: flutter, data-analysis, gamedev или warhammer-40k.
Существующие соцсети решают эту задачу через группы и сообщества, но процесс поиска остаётся неудобным: нужно вступать в десятки групп, листать бесконечные ленты и надеяться наткнуться на нужного человека.
Так родился Syncory (ранее Synq) — pet-project, который быстро вырос в нечто большее, чем просто учебное приложение.
Выбор стека: простота превыше всего
Как Data Engineer, я не хотел тратить время на настройку серверов, оркестрацию контейнеров или администрирование баз данных. Мне нужно было решение, которое позволит сфокусироваться на логике приложения.
Бэкенд: 100% Serverless
Firebase — очевидный выбор:
Firebase Auth — готовая аутентификация из коробки
Firestore — NoSQL база данных в реальном времени
Бесплатный tier для старта проекта
Нулевая настройка инфраструктуры
Фронтенд: Flutter Web
Мне нужно было веб-приложение, доступное с любого ПК. Flutter Web подошёл идеально:
Быстрая разработка UI с использованием виджетов
Material Design 3 «из коробки»
Единая кодовая база для веба и мобильных платформ (на будущее)
Активное комьюнити и хорошая документация
Да, я знаю про React/Vue, но как человеку, который пишет на Python и SQL, синтаксис Dart показался мне более понятным и близким.
Что под капотом: ключевые фичи
Я хотел не просто «фид с постами», а полноценное приложение с продуманным UX. Вот что я реализовал:
1. Анимированная страница входа (wow-эффект)
Первое впечатление решает всё. Я отказался от скучного статичного лейаута с двумя колонками.
Решение: полноэкранный интерактивный фон с анимированными элементами.
Как это работает:
// Слой 1: Отслеживание позиции мыши
MouseRegion(
onHover: (event) {
setState(() {
_mousePosition = event.position;
});
},
child: Stack(
children: [
// Слой 2: Анимированные чипы
..._buildAnimatedChips(),
// Слой 3: Форма входа
_buildLoginCard(),
],
),
)
Фишка в _AnimatedChip: это StatefulWidget, который получает mousePosition и с помощью AnimatedContainer и Matrix4.translationValues плавно «убегает» от курсора.
Эффект минимальный, но он мгновенно вовлекает пользователя и создаёт ощущение «живого» приложения.
class _AnimatedChip extends StatefulWidget {
final Offset mousePosition;
final Offset initialPosition;
final String label;
// ...
}
Результат: пользователь сразу видит, что это не очередной скучный шаблон, а продуманный интерфейс.
2. Админка «по-взрослому» (роли и гейты)
С самого начала я решил, что мне нужен контроль над контентом и возможность модерации.
Как реализовано:
Роль в Firestore:
{
"users": {
"user_id": {
"email": "admin@example.com",
"role": "admin",
"isDisabled": false
}
}
}
Гейт в приложении:
// main_scaffold.dart
bool _isAdmin = false;
Future<void> _checkAdminStatus() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
setState(() {
_isAdmin = doc.data()?['role'] == 'admin';
});
}
Секретные кнопки в интерфейсе:
if (_isAdmin) ...[
IconButton(
icon: Icon(Icons.admin_panel_settings),
onPressed: () => _navigateToAdminPanel(),
),
]
Система банов:
Админ устанавливает флаг
isDisabled: trueв документе пользователяПри следующем входе
main.dartпроверяет этот флагЕсли
true— принудительныйsignOut()и редирект наLoginPageс сообщением о бане
Такой подход позволяет не удалять данные пользователя, а временно блокировать доступ.
3. Приватные комментарии (фича для вовлечения)
Чтобы стимулировать людей подавать заявки на участие в проектах, я реализовал систему скрытых комментариев.
Логика:
На странице создания поста автор выбирает политику видимости комментариев:
SegmentedButton<String>(
segments: [
ButtonSegment(value: 'all', label: Text('Все')),
ButtonSegment(value: 'approvedOnly', label: Text('Только одобренные')),
],
selected: {_commentVisibility},
onSelectionChanged: (Set<String> newSelection) {
setState(() {
_commentVisibility = newSelection.first;
});
},
)
На странице поста реализован гейт доступа:
class _CommentsAccessGate extends StatelessWidget {
Widget build(BuildContext context) {
final commentPolicy = post['commentVisibility'] ?? 'all';
// Автор видит всё
if (post['userId'] == currentUserId) {
return _buildCommentsSection(showInput: true);
}
// Публичные комментарии
if (commentPolicy == 'all') {
return _buildCommentsSection(showInput: true);
}
// Приватные: проверяем статус заявки
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('posts/${post.id}/applicants')
.doc(currentUserId)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.data?.data()?['status'] == 'approved') {
return _buildCommentsSection(showInput: true);
}
return _buildAccessDeniedMessage();
},
);
}
}
Результат: пользователи более активно подают заявки, чтобы получить доступ к обсуждению.
4. Три круга ада: Google Sign-In на Flutter Web
А вот здесь началось самое интересное. Я наивно думал, что Google Auth — это просто await GoogleSignIn.instance.signIn().
Как же я ошибался.
Я потратил 3 дня на отладку, прежде чем всё заработало. Вот что нужно знать, если вы делаете Google Auth на Flutter Web:
Круг ада №1: OAuth Client ID vs Firebase
Проблема: Firebase Console даёт вам Web API Key, но для Google Sign-In на вебе этого недостаточно.
Решение: идём в Google Cloud Console → APIs & Services → Credentials и создаём OAuth 2.0 Client ID типа «Web application».
Круг ада №2: Redirect URI Mismatch
Проблема: получаете Error 400: redirect_uri_mismatch, хотя вроде всё настроили.
Решение: в настройках OAuth Client ID нужно добавить ваш URL в ДВА списка:
Authorized JavaScript origins:
https://your-app.web.appAuthorized redirect URIs:
https://your-app.web.app/__/auth/handler
Если пропустите второй — получите ошибку.
Круг ада №3: People API
Проблема: Error 403: PERMISSION_DENIED при попытке входа.
Решение: в Google Cloud Console → APIs & Services → Library ищем People API и включаем её для проекта.
Круг ада №4: Конфигурация в коде
После всех настроек в консолях, нужно правильно сконфигурировать Flutter:
В web/index.html:
<head>
<meta name="google-signin-client_id"
content="YOUR-CLIENT-ID.apps.googleusercontent.com">
</head>
В login_page.dart:
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [
'email',
'openid', // ОБЯЗАТЕЛЕН для получения idToken!
],
);
Future<void> _signInWithGoogle() async {
try {
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) return; // Пользователь отменил вход
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final OAuthCredential credential = GoogleAuthProvider.credential(
idToken: googleAuth.idToken,
// accessToken будет null на вебе!
);
await FirebaseAuth.instance.signInWithCredential(credential);
} catch (e) {
print('Error: $e');
}
}
Важно: если видите призрачные ошибки типа Couldn't find constructor 'GoogleSignIn', хотя код правильный — выполните:
flutter clean
flutter pub get
И в VS Code: Ctrl+Shift+P → Reload Window.
Работа с Firestore: индексы и оптимизация
Firestore требует создания индексов для каждого сложного запроса. При первом выполнении нового запроса вы получите ошибку failed-precondition с ссылкой на создание индекса.
Пример:
// Такой запрос требует составного индекса
FirebaseFirestore.instance
.collection('posts')
.where('categories', arrayContains: 'flutter')
.orderBy('createdAt', descending: true)
.limit(20);
Решение: просто переходите по ссылке из ошибки в консоли, Firebase автоматически создаст нужный индекс. Через 1-2 минуты запрос заработает.
Что в итоге?
Получилось быстрое веб-приложение с продуманным UX и полным контролем над данными.
Технические метрики:
Время загрузки главной страницы: ~1.5 сек
Полностью serverless архитектура
Затраты на хостинг: $0 (в рамках бесплатного тира Firebase)
Время разработки: ~6 часов активного вайбкодинга
Благодарности
Отдельное огромное спасибо моему коллеге Aibol Nazenov (Айболу Назенову). Он был моим партнёром по брейнштормингу и принимал ключевое участие в разработке концепции Syncory. Без его фидбэка и критики проект не был бы таким, какой он есть сейчас.
Попробуйте сами (и посмотрите код)
Я выложил весь код на GitHub под лицензией GNU GPLv3.
? Живое демо: syncory-flutter-app.web.app
? GitHub: github.com/ungernthabaron/syncory-flutter-app
Проект полностью открыт — можете использовать его как основу для своих идей или просто изучить реализацию.
Вопрос к сообществу
А с какими «неочевидными» граблями Flutter Web или Firebase сталкивались вы? Особенно интересно услышать про проблемы с производительностью или специфичные кейсы интеграции.
Делитесь опытом в комментариях!
Комментарии (7)

dedm
16.11.2025 12:36Сори ну меня чет забомбило нельзя так так делать
Я переписывал раз 5 так как в 1ой версии были просто маты но ии меня остановил-
ОТСУТСТВИЕ STATE MANAGEMENT
────────────────────────────────────────────────────────
Проект полностью игнорирует современные практики управления состоянием.
Нет ни Provider, ни Riverpod, ни Bloc, ни даже простого InheritedWidget.Последствия:
• Невозможно эффективно делиться состоянием между виджетами
• Каждый виджет сам управляет своим состоянием через setState()
• Дублирование логики проверки роли админа в MainScaffold, ProfilePage, PostDetailPage
• Нет единого источника правды для данных пользователя
• При изменении данных нужно вручную обновлять все зависимые виджетыПримеры проблем:
_checkUserRole() дублируется в 3+ местах
Данные пользователя загружаются заново в каждом виджете
Нет кеширования состояния аутентификации
-
ОТСУТСТВИЕ СЛОЯ АБСТРАКЦИИ ДЛЯ ДАННЫХ
─────────────────────────────────────────────────────────
Прямые вызовы FirebaseFirestore.instance и FirebaseAuth.instance везде.
Нет репозиториев, нет сервисов, нет слоя данных.Проблемы:
• Невозможно заменить Firebase на другой бэкенд без переписывания всего кода
• Нет централизованной обработки ошибок
• Нет кеширования данных
• Невозможно протестировать бизнес-логику без моков Firebase
• Дублирование логики работы с коллекциямиПримеры:
FirebaseFirestore.instance.collection('users').doc(uid).get() повторяется 20+ раз
Нет единого места для работы с постами, комментариями, пользователями
-
ОТСУТСТВИЕ МОДЕЛЕЙ ДАННЫХ
─────────────────────────────────────────────────────────
Везде используется Map вместо типизированных моделей.Последствия:
• Нет автодополнения в IDE
• Ошибки типов обнаруживаются только в runtime
• Нет валидации данных при получении из Firebase
• Сложно понять структуру данных без чтения кода
• Легко допустить опечатку в ключах (например, 'author_name' vs 'authorName')Примеры:
data['displayName'] ?? 'Anonymous' - что если поле называется по-другому?
Нет гарантии, что все поля присутствуют
-
СМЕШЕНИЕ БИЗНЕС-ЛОГИКИ И UI
─────────────────────────────────────────────────────────
Вся бизнес-логика находится прямо в StatefulWidget.Проблемы:
• Невозможно переиспользовать логику
• Сложно тестировать
• Виджеты становятся огромными (PostDetailPage - 714 строк!)
• Нарушение принципа единственной ответственностиПримеры:
toggleLike(), toggleBookmark(), _postComment() - это бизнес-логика, не UI
Вся логика работы с заявками (applicants) в UI-слое
-
ОТСУТСТВИЕ ОБРАБОТКИ ОШИБОК
─────────────────────────────────────────────────────────
Ошибки либо игнорируются, либо показываются через print().
где трай кетч где runZonedGuardedПроблемы:
• Нет централизованной обработки ошибок
• Пользователь видит технические сообщения об ошибках
• Нет логирования ошибок для отладки
• Нет retry-логики при сетевых ошибкахПримеры:
print("Error: $e") - бесполезно в production
catch (e) { _showError('Save Error: $e') } - показывает технические детали
-
ОТСУТСТВИЕ ВАЛИДАЦИИ
─────────────────────────────────────────────────────────
Минимальная валидация форм, нет проверки на стороне клиента.Проблемы:
• Можно отправить пустые данные
• Нет проверки формата email
• Нет ограничений на длину текста
• Нет проверки обязательных полей перед отправкойПримеры:
if (_titleController.text.isEmpty || _selectedCategories.isEmpty) - только базовая проверка
Нет валидации email в login_page.dart
-
ПРОБЛЕМЫ С ПРОИЗВОДИТЕЛЬНОСТЬЮ
─────────────────────────────────────────────────────────
Множественные StreamBuilder'ы, нет оптимизации запросов.Проблемы:
• Один и тот же документ загружается несколько раз
• Нет пагинации для списков (загружаются все посты сразу)
• StreamBuilder'ы создаются без ограничений
• Нет debounce для поиска
• Фильтрация происходит на клиенте после загрузки всех данныхПримеры:
FeedPage загружает все посты сразу, затем фильтрует на клиенте
PostDetailPage делает два FutureBuilder для одного и того же поста
Нет индексов для сложных запросов
ОТСУТСТВИЕ ТЕСТОВ
─────────────────────────────────────────────────────────
Только пустой widget_test.dart, нет unit-тестов, нет integration-тестов.
Ладно тесты еще ок могу понять ну фаллатер аналйз же можно было настроить это базовая штука-
ОТСУТСТВИЕ КОНСТАНТ И КОНФИГУРАЦИИ
─────────────────────────────────────────────────────────
Магические числа и строки везде, нет централизованных констант.Проблемы:
• Сложно изменить значения (например, максимальную ширину контента)
• Нет единого стиля для отступов, размеров шрифтов
• Легко допустить ошибку при копировании значенийПримеры:
BoxConstraints(maxWidth: 800) повторяется в нескольких местах
EdgeInsets.all(24) - разные значения в разных местах
Нет константы для названий коллекций Firebase
-
ОТСУТСТВИЕ DEPENDENCY INJECTION
─────────────────────────────────────────────────────────
Все зависимости создаются напрямую в коде.Проблемы:
• Невозможно заменить реализации для тестирования
• Тесная связанность компонентов
• Сложно управлять жизненным циклом зависимостейПримеры:
FirebaseAuth.instance.currentUser - прямая зависимость
FirebaseFirestore.instance - везде напрямую
-
НЕОПТИМАЛЬНАЯ СТРУКТУРА ПРОЕКТА
─────────────────────────────────────────────────────────
Все файлы в корне lib/, нет разделения на слои.Проблемы:
• Сложно найти нужный код
• Нет разделения на features/modules
• Нет разделения на layers (data, domain, presentation)
• Все в одном местеРекомендуемая структура:
lib/
core/
constants/
errors/
utils/
data/
models/
repositories/
services/
domain/
entities/
usecases/
presentation/
pages/
widgets/
providers/ -
ПРОБЛЕМЫ С БЕЗОПАСНОСТЬЮ
─────────────────────────────────────────────────────────
Отсутствие проверок прав доступа на клиенте, утечки данных.Проблемы:
• Проверка роли админа только на клиенте (можно обойти)
• Нет проверки прав перед выполнением действий
• Потенциальные утечки данных через ошибки
• Нет шифрования чувствительных данныхПримеры:
_isAdmin проверяется только на клиенте
Нет валидации на сервере (Firestore Rules должны быть строгими)
-
НЕСООТВЕТСТВИЕ BEST PRACTICES FLUTTER
─────────────────────────────────────────────────────────
Игнорирование рекомендаций Flutter team.Проблемы:
• Нет использования const конструкторов где возможно
• Нет оптимизации rebuild'ов
• Неправильное использование StreamBuilder (множественные подписки)
• Нет использования keys где необходимо
• Неоптимальное использование ListView.builderПримеры:
Отсутствие const для статических виджетов
StreamBuilder без ограничений
Нет использования AutomaticKeepAliveClientMixin
-
ОТСУТСТВИЕ ОБРАБОТКИ СОСТОЯНИЙ ЗАГРУЗКИ
─────────────────────────────────────────────────────────
Примитивная обработка состояний загрузки, нет skeleton screens.Проблемы:
• Пользователь видит только CircularProgressIndicator
• Нет обработки пустых состояний (empty states) везде
• Нет обработки состояний ошибок
• Нет retry механизмов
• Почти нету if (!mounted) return; -
ПРОБЛЕМЫ С ДОСТУПНОСТЬЮ
─────────────────────────────────────────────────────────
Отсутствие поддержки accessibility.Проблемы:
• Нет semantic labels
• Нет поддержки screen readers
• Нет поддержки больших шрифтов
• Нет поддержки высокого контраста -
ОТСУТСТВИЕ ОФФЛАЙН-ПОДДЕРЖКИ
───────────────────────────────────────────────────────────────────────────────
Приложение не работает без интернета.Проблемы:
• Нет кеширования данных
• Нет синхронизации при восстановлении соединения
• Нет индикации офлайн-режима
• Пользователь теряет данные при обрыве соединения
══════════════════════════════════════════════════════════════
ИТОГОВАЯ ОЦЕНКА
══════════════════════════════════════════════════════════════Этот проект демонстрирует классические антипаттерны разработки Flutter приложений:
❌ Нет архитектуры - все в одном слое
❌ Нет state management - невозможно масштабировать
❌ Нет абстракций - тесная связанность с Firebase
❌ Нет тестов - нет гарантий качества
❌ Нет документации - сложно поддерживать
❌ Проблемы с производительностью - неоптимальные запросы
❌ Проблемы с безопасностью - проверки только на клиенте
❌ Несоблюдение best practices - игнорирование рекомендаций
Это получился очень сырой и хаотичный код.
Такое ощущение, что архитектура и принципы ООП просто не учитывались.
С таким подходом проект действительно может «ложиться» в проде очень быстро — не потому что он безнадёжен, а потому что фундамент не продуман.
И, пожалуйста, не оправдывай это «вайб-кодингом» — тут реально видно, что проблема не в стиле, а в отсутствии структуры.
Даже нейросеть не стала бы генерировать подобный набор противоречий — это просто результат того, что ты писал на ходу, без плана и слоёв абстракции.══════════════════════════════════════════════════════════════

GenomeDust Автор
16.11.2025 12:36да, на самом деле так и было, что логика повлялась вместо с кодом, плана не было,
Спасибо за коммент, постараюсь применить рекомендации в свободное время!
dedm
16.11.2025 12:36Тут дело вообще не в “плане”, а в том, что ты, похоже, не понял базовую парадигму Flutter.
Это реактивный фреймворк — он не перерисовывает всю страницу, только те виджеты, которые подписаны на состояние.Поэтому state-management (Provider / Riverpod / Bloc) — это не “совет”, а нормальная архитектура.
А вот императивный стиль “как в Python”, когда логика смешана с UI — это уже точно не про Flutter.

dkfbm
16.11.2025 12:36Сори ну меня чет забомбило нельзя так так делать
Github не смотрел, но охотно верю, что всё так и есть – мне уже в примерах кода метод присвоения роли админа глаз резанул. Про ACL автор, очевидно, не слышал. Да и читая про "круги ада" всё время вспоминалось сакральное: RTFM.
Это не говоря уж о том, что в любой соцсети фронтенд – лишь надводная часть айсберга. Без развитой админки она попросту не поплывёт, а тут даже зачатков нет, и ни о каких планах создания не упоминается.
-

smessing
16.11.2025 12:36Наивный думает одного приложения сети - хватит. А зачем концепт ?)... Главное уметь программировать, и начать описывать функционал :)
DanielKross
Зашел на демо страницу, все работает, все хорошо(на мой взгляд дизайн надо переработать), но неудобно, что когда заходишь в "пост", исчезает колонка слева, с темами, надо нажимать "назад", чтоб ее увидеть, наверное практичнее было бы менялся бы фрейм в середине страницы, а все остальное, оставалось бы в доступности. А за фишки с авторизацией гугла, спасибо, пригодится.
GenomeDust Автор
Спасибо за комментарий!