Irbisa · Шпаргалка

Firebase и бэкенд-сервисы

FlutterFire, Auth, Firestore, FCM, Remote Config, Crashlytics, App Check, Supabase

Middle-разработчик · 24 вопроса

  1. 01

    Как подключить Firebase к Flutter-приложению и что main() должен дождаться до runApp?

    Запустите flutterfire configure, чтобы зарегистрировать приложения и сгенерировать firebase_options.dart, добавьте firebase_core и по плагину на каждый продукт, а в main() до runApp дожидайтесь только Firebase.initializeApp().

    // Once per machine:  dart pub global activate flutterfire_cli// Once per project:  flutterfire configureimport 'package:firebase_core/firebase_core.dart';import 'firebase_options.dart'; Future<void> main() async {…
  2. 02

    Как читать и писать данные Firestore во Flutter и чем get() отличается от snapshots()?

    get() читает один раз и возвращает Future; snapshots() возвращает Stream, который выдаёт значение сразу и потом при каждом изменении, пока вы не отмените подписку.

    final notes = FirebaseFirestore.instance    .collection('notes')    .withConverter<Note>(      fromFirestore: (snap, _) => Note.fromJson(snap.data()!),      toFirestore: (note, _) => note.toJson(),    );…
  3. 03

    Чем Cloud Firestore отличается от Realtime Database и когда выбирать каждую?

    Firestore хранит документы в коллекциях, умеет составные запросы по индексам и берёт плату за каждую операцию с документом; Realtime Database представляет собой одно JSON-дерево с очень низкой задержкой и простыми запросами, оплата идёт за хранение и трафик.

  4. 04

    Как реализовать вход через Google с Firebase Auth и почему он часто работает в debug, но ломается в сборке из Play Store?

    Пакет google_sign_in получает Google ID-токен, GoogleAuthProvider.credential оборачивает его, а signInWithCredential обменивает на пользователя Firebase.

    Future<void> initGoogle() => GoogleSignIn.instance.initialize(); Future<UserCredential?> signInWithGoogle() async {  if (kIsWeb) {    return FirebaseAuth.instance.signInWithPopup(GoogleAuthProvider());  }…
  5. 05

    На холодном старте приложение на кадр показывает экран логина, а потом прыгает на ленту. Что на самом деле делает authStateChanges()?

    authStateChanges() отдаёт первый User? только после того, как SDK восстановит сохранённую сессию с диска.

    class AuthGate extends StatelessWidget {  const AuthGate({super.key});   @override  Widget build(BuildContext context) {    return StreamBuilder<User?>(…
  6. 06

    Стейджинговая сборка пишет в продовый Firestore, хотя firebase_options.dart вы подменили. Что всё ещё смотрит в прод?

    flutterfire configure создаёт три конфига, а не один: lib/firebase_options.dart плюс нативные google-services.json (Android) и GoogleService-Info.plist (iOS).

    # One Firebase project per environment, one configure run per flavor.# repeat per build configuration (Release-staging, Profile-staging, ...)flutterfire configure --project=myapp-staging \  --out=lib/firebase/options_staging.dart \  --android-package-name=com.myapp.staging \  --android-out=android/app/src/staging/google-services.json \…
  7. 07

    Гость неделю играл, потом вошёл через Google, и всё сохранённое исчезло. Что сделал этот вход?

    signInWithCredential залогинил пользователя в другой аккаунт.

    Future<void> startAsGuest() async {  if (FirebaseAuth.instance.currentUser == null) {    await FirebaseAuth.instance.signInAnonymously();  }}…
  8. 08

    Лента должна показывать три свежих комментария под каждым постом. Как это требование меняет вашу модель данных в Firestore?

    В Firestore нет джойнов, поэтому экрану, который должен отрисоваться за один поход в сеть, нужен документ под этот экран: скопируйте три свежих комментария прямо в документ поста.

  9. 09

    Только что отправленное сообщение мгновенно появляется в списке, а через секунду прыгает на другую позицию. О чём говорят снапшоты?

    Вы видите два события снапшота на одну запись: сначала локальное, с hasPendingWrites true и ещё пустым createdAt, потом серверное, с настоящим временем.

  10. 10

    В авиарежиме ваш await docRef.set(...) не возвращается никогда, но новая строка уже на экране. Это баг?

    Нет. Future от set() завершается только после подтверждения сервера, а локальный кеш применил запись сразу; настоящий баг здесь в самом await записи в UI.

    // wrong: offline this Future never completes and the spinner never stopsFuture<void> badSave(String text) async {  setState(() => _saving = true);  await db.collection('notes').add({'text': text});  setState(() => _saving = false);}…
  11. 11

    Два пользователя одновременно нажимают «Купить» на последнем товаре, и оба получают заказ. Что не так с этим кодом?

    Future<void> buy(String itemId, String uid) async {  final item = db.doc('items/$itemId');  final snap = await item.get();  final stock = snap.get('stock') as int;  if (stock <= 0) throw const SoldOut();   await item.update({'stock': stock - 1});  await db.collection('orders').add({'itemId': itemId, 'uid': uid});}

    Это гонка read-modify-write: оба клиента читают stock: 1, оба проходят проверку, оба пишут 0, и на один товар получается два заказа.

  12. 12

    Этот запрос работает на эмуляторе, а в проде стрим падает с failed-precondition. Почему и как исправить это раз и навсегда?

    Stream<List<Order>> openOrders(String uid) => db    .collection('orders')    .where('userId', isEqualTo: uid)    .where('status', isEqualTo: 'open')    .orderBy('createdAt', descending: true)    .limit(20)    .snapshots()    .map((s) => s.docs.map(Order.fromDoc).toList());

    Запросу нужен составной индекс по userId, status и createdAt.

  13. 13

    Как работают security rules в Firestore и почему их не заменить тем, что кнопка «Удалить» спрятана от не-авторов?

    Правила выполняются на серверах Google для каждого запроса и решают по request.auth, сохранённому документу (resource) и входящему (request.resource).

    rules_version = '2';service cloud.firestore {  match /databases/{database}/documents {    match /notes/{noteId} {      allow read, delete: if request.auth != null                          && resource.data.ownerId == request.auth.uid;…
  14. 14

    Пуши приходят, когда приложение закрыто, но ничего не появляется, пока пользователь держит его открытым. Как firebase_messaging обрабатывает каждое состояние приложения?

    Сообщение с блоком notification рисует операционная система, и она намеренно не рисует его, пока приложение на переднем плане; оно приходит в FirebaseMessaging.onMessage как данные, и показать его должны вы сами.

    @pragma('vm:entry-point') // release builds tree-shake it otherwiseFuture<void> _onBackground(RemoteMessage message) async {  // A separate isolate: no providers, no navigator, no app state.  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);}…
  15. 15

    Найдите баг: после нескольких переходов по экранам один пуш в foreground показывает один и тот же баннер два-три раза.

    class _InboxScreenState extends State<InboxScreen> {  int _unread = 0;   @override  void initState() {    super.initState();    FirebaseMessaging.onMessage.listen((message) {      showLocalBanner(message.notification?.title);      setState(() => _unread++);    });  }   @override  Widget build(BuildContext context) => Text('Unread: $_unread');}

    Подписка на onMessage никогда не отменяется.

  16. 16

    Crashlytics подключён, пользователи каждый день жалуются на вылеты, а дашборд пустой. Чего не хватает в обвязке?

    Подключение плагина ловит только нативные краши.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);  final crashlytics = FirebaseCrashlytics.instance;  await crashlytics.setCrashlyticsCollectionEnabled(!kDebugMode);…
  17. 17

    Что вы логируете в Firebase Analytics во Flutter-приложении и что туда попадать не должно никогда?

    Логируйте события, которые отвечают на продуктовые вопросы (шаги воронки, использование функций, покупки), с несколькими типизированными параметрами, явно отслеживайте экраны и никогда не отправляйте персональные данные: ни email, ни имён, ни телефонов, ни свободного текста.

  18. 18

    На слабой сети сплэш висит восемь секунд, потому что старт ждёт Remote Config. Как это должно быть устроено?

    Никогда не ждите сетевой fetch до первого кадра.

    final rc = FirebaseRemoteConfig.instance; Future<void> bootstrapConfig() async {  await rc.setConfigSettings(RemoteConfigSettings(    fetchTimeout: const Duration(seconds: 8), // default 60 s    minimumFetchInterval:…
  19. 19

    Callable-функция работает на эмуляторе, а в проде возвращает internal без сообщения. Что происходит?

    Callable пробрасывает клиенту только HttpsError (код, сообщение и details).

  20. 20

    Счёт за Firestore за месяц вырос до пятизначного, а число пользователей не изменилось. Куда смотреть во Flutter-коде в первую очередь?

    Firestore тарифицирует чтение по документам, поэтому ищите код, который читает намного больше документов, чем показывает экран: запросы без ограничения, слушатель на каждую строку списка, фильтрацию или подсчёт в Dart и слушатели, которые утекают или пересоздаются.

    // wrong: a new stream on every rebuild re-attaches the listener@overrideWidget build(BuildContext context) => StreamBuilder<QuerySnapshot>(      stream: db.collection('orders').where('userId', isEqualTo: uid).snapshots(),      builder: _rows,    );…
  21. 21

    Найдите баг в этом OrdersRepository: watchMyOrders() должен следить за тем, кто вошёл в аккаунт прямо сейчас.

    class OrdersRepository {  OrdersRepository(this._auth, this._db);  final FirebaseAuth _auth;  final FirebaseFirestore _db;   // Orders of whoever is signed in right now.  Stream<List<Order>> watchMyOrders() =>      _auth.authStateChanges().asyncExpand((user) {        if (user == null) return Stream.value(const <Order>[]);        return _db            .collection('orders')            .where('userId', isEqualTo: user.uid)            .snapshots()            .map((s) => s.docs.map(Order.fromDoc).toList());      });}

    asyncExpand не переходит к следующему событию авторизации, пока не завершится внутренний стрим, а стрим snapshots() не завершается никогда.

  22. 22

    Любой может вытащить API-ключ из APK и обратиться к Firestore напрямую. Что добавляет App Check и от чего он всё равно не защищает?

    App Check подтверждает, что запрос пришёл из вашего настоящего, неизменённого приложения на настоящем устройстве.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);   // Before the first Firestore, Storage or Functions call.  await FirebaseAppCheck.instance.activate(…
  23. 23

    Как тестировать Flutter-приложение на Firebase так, чтобы ни один прогон не трогал прод и ни одному widget-тесту не нужна была сеть?

    Три слоя: интерфейс репозитория, чтобы widget-тесты вообще не видели Firebase, Emulator Suite для кода, который реально ходит в Firestore, и JavaScript-библиотека для тестов самих правил.

    // 1. Widget tests: the seam is a repository, Firebase never loads.class FakeOrders implements OrderRepository {  FakeOrders(this.orders);  final List<Order> orders;  @override  Stream<List<Order>> watch(String uid) => Stream.value(orders);…
  24. 24

    Клиент спрашивает, строить на Firebase, на Supabase или на своём бэкенде. Как выбирать и как оставить себе путь к отступлению?

    Firestore подходит для realtime и offline-first приложений с чтениями «по экрану»; Supabase выбирают, когда данные реляционные и нужен SQL; свой бэкенд нужен, когда решают размещение данных, готовая серверная команда или нестандартная логика.