Привет, Хабр! Меня зовут Андрей, я 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 сталкивались вы? Особенно интересно услышать про проблемы с производительностью или специфичные кейсы интеграции.
Делитесь опытом в комментариях!
DanielKross
Зашел на демо страницу, все работает, все хорошо(на мой взгляд дизайн надо переработать), но неудобно, что когда заходишь в "пост", исчезает колонка слева, с темами, надо нажимать "назад", чтоб ее увидеть, наверное практичнее было бы менялся бы фрейм в середине страницы, а все остальное, оставалось бы в доступности. А за фишки с авторизацией гугла, спасибо, пригодится.