Инженеры из команд Firebase SDK прилагают огромные усилия, чтобы сделать свои API-интерфейсы последовательными и простыми в использовании. Одна вещь, которую вы, возможно, заметили в SDK мобильного клиента, заключается в том, что все вызовы API, которые имеют дело с чтением и записью данных, полностью асинхронны. Это означает, что вызов всегда возвращается немедленно, не блокируя код для ожидания результата. Результаты появятся спустя некоторое время, когда они будут готовы.

В JavaScript эти асинхронные методы обычно возвращают обещание. Для Android метод вернет объект Task, что очень похоже на обещание. А для iOS вы передадите блок завершения (закрытие в Swift), который будет вызываться позже для обработки результата.

Проблема асинхронного программирования в том, что поначалу оно не совсем интуитивно понятно. Если вы хотите получить какие-то данные, естественно захотеть написать код, который имеет примерно такую ​​структуру:

try {
    result = database.get("the_thing_i_want")
    // handle the results here
}
catch (error) {
    // handle any errors here
}

Это синхронный вызов, он короткий и понятный. Результат get() возвращается непосредственно из функции, и вызывающий код ожидает его завершения. Но проблема именно в этом. Вы не хотите, чтобы ваш код останавливался в ожидании чего-то, что может занять много времени.

Вы можете думать об асинхронных API Firebase так же, как вы думаете об использовании духовки. Если вы хотите испечь пирог, вам, как правило, не стоит стоять у духовки и ждать, пока он закончится. Никто не взаимодействует со своей духовкой так, как синхронный API! Вместо этого вы устанавливаете таймер на духовке, и он сообщает вам асинхронно, когда работа сделана, пока вы идете и занимаетесь другими делами. Намного эффективнее распоряжаться своим временем, не так ли?

Итак, как выглядит асинхронный вызов в Firebase? Возьмем, например, API Cloud Firestore. Для извлечения документа требуется сетевое соединение или локальный дисковый кеш, и нет никаких гарантий относительно скорости любого из них. Итак, API выборки документов должен вызываться следующим образом:

Android / Java:

Task<DocumentSnapshot> task =
    FirebaseFirestore.getInstance().document("users/pat").get();
task.addOnSuccessListener(new OnSuccessListener() {
    public void onSuccess(DocumentSnapshot snapshot) {
        // handle the document snapshot here
    }
});
task.addOnFailureListener(new OnFailureListener() {
    public void onFailure(Exception e) {
        // handle any errors here
    }
});

Интернет / JavaScript:

var promise = firebase.firestore().doc("users/pat").get();
promise.then(snapshot => {
    // handle the document snapshot here
})
.catch(error => {
    // handle any errors here
});

iOS / Swift:

Firestore.firestore().document("users/pat")
        .getDocument() { (snapshot, err) in
    if let snapshot = snapshot {
        // handle the document snapshot here
    }
    else {
        // handle any errors here
    }
}

Что ж, это немного лишний набор текста по сравнению с более простым синхронным прототипом!

Итак, зачем заставлять потребителя API испытывать дополнительные проблемы с использованием асинхронного API? Разве нельзя вместо этого использовать более простые синхронные API? Что ж, Firebase может предоставлять синхронные API-интерфейсы для мобильных приложений, но тогда мы унаследуем одну из двух проблем, которые даже хуже, чем написание этого дополнительного кода:

  1. Вызов синхронной функции в основном потоке вашего приложения может заморозить приложение на неопределенный срок, что ужасно для пользователя. На Android также может происходить мягкий сбой с диалоговым окном Приложение не отвечает (ANR).
  2. Чтобы избежать блокировки основного потока синхронным методом, нам пришлось бы управлять своими собственными потоками для правильного вызова этих API. Это даже больше кода, и его бывает сложно исправить. Даже у опытных инженеров есть проблемы с правильным поведением потоков.

Если проблема №1 неясна, я расскажу об этом подробнее в следующем разделе.

Стоит отметить, что использование асинхронного API дает еще несколько преимуществ:

  1. У поставщика API есть возможность оптимизировать поведение потоковой передачи таким образом, чтобы вы не могли дублировать себя. (Вы должны доверять поставщику API, чтобы понять характеристики производительности его работы!)
  2. Это открывает дверь для будущих улучшений, таких как отмена текущей работы.

Если вы спросите меня, я бы предпочел асинхронный API, который за кулисами управляет всеми необходимыми потоками. Благодаря этому писать мое приложение становится намного проще.

Почему так плохо блокировать основной поток?

Среды выполнения приложений, которые имеют пользовательский интерфейс, обычно управляют всеми взаимодействиями с этим пользовательским интерфейсом через один поток, называемый основным потоком. Подавляющее большинство написанного вами кода приложения будет выполняться в этом потоке. Поток управляется циклом событий, который, по сути, постоянно перемешивает очередь рабочих элементов, пока процесс приложения не завершится. Эти элементы работы включают обработку событий, рендеринг на экран, анимацию и все, что связано с пользовательским интерфейсом. Он должен выполнять эти операции достаточно быстро, чтобы он также мог отображать экран с плавной скоростью 60 кадров в секунду. Это означает, что на выполнение элемента работы остается не более 16 мсек. Более того, и основной поток пропустил рендеринг пользовательского интерфейса для вашего приложения до тех пор, пока эта работа, наконец, не будет завершена. Пропускать кадры - это нехорошо!

Итак, если одна из этих частей работы в цикле событий занимает слишком много времени, она застревает в очереди, и приложение будет казаться «неуклюжим» или даже полностью зависшим, даже если оно все еще что-то делает! Очень важно убедиться, что ваша работа в основном потоке никогда не блокируется, независимо от причины, чтобы ваш пользовательский интерфейс всегда был плавным и отзывчивым. Не будь таким:

Цикл обработки событий вашего приложения не может быть заблокирован работой, которая занимает так много времени, что она пропускает кадры. Последствиями являются плохие оценки для вашего приложения из-за плохого взаимодействия с пользователем.

Но не получается так, как я хочу!

Давайте возьмем приведенные выше примеры, пока проигнорируем случаи ошибок и добавим код. В каждом примере есть три строки журнала с номерами 1, 2 и 3, в которых печатается значение переменной firstName. Выберите язык, который вы предпочитаете, и попытайтесь угадать вывод журнала. Строки журнала выделены жирным шрифтом.

Android / Java:

private mFirstName = "unknown";  // defined as a class member
Log.d("DEBUG", "1. firstName = "+ mFirstName);
Task<DocumentSnapshot> task =
    FirebaseFirestore.getInstance().document("users/bob").get();
task.addOnSuccessListener(new OnSuccessListener() {
    public void onSuccess(DocumentSnapshot snapshot) {
        mFirstName = snapshot.getString("firstName")
        Log.d("DEBUG", "2. firstName = "+ mFirstName);
    }
});
Log.d("DEBUG", "3. firstName = "+ mFirstName);

Интернет / JavaScript:

var firstName = "unknown"
console.log("1. firstName = " + firstName);
var promise = firebase.firestore().doc("users/pat").get();
promise.then(snapshot => {
    firstName = snapshot.get("firstName");
    console.log("2. firstName = "+ firstName);
})
console.log("3. firstName = "+ firstName);

iOS / Swift:

var firstName = "unknown"
print("1. firstName = \(firstName)")
Firestore.firestore().document("users/pat")
        .getDocument() { (snapshot, err) in
    firstName = snapshot!.data()!["firstName"] as! String
    print("2. firstName = \(firstName)")
}
print("3. firstName = \(firstName)")

Итак, как вы думаете, каким будет результат, если свойство firstName документа - «Pat»?

Ваше первое впечатление может быть примерно таким:

1. name = unknown
2. name = Pat
3. name = Pat

В конце концов, многие программы выполняют строки кода в том порядке, в котором они появляются. Однако помните, что метод выборки документа get() (или getDocument()) является асинхронным и возвращает немедленно до того, как будет вызван обратный вызов, обрабатывающий моментальный снимок. Затем этот обратный вызов будет вызван позже в основном потоке, чтобы при необходимости можно было безопасно обновить пользовательский интерфейс. Это означает, что журнал № 3 фактически выполняется сразу после № 1, до того, как обратный вызов будет вызван с № 2, например:

1. name = unknown
3. name = unknown
2. name = Pat

Почему №3 показывает значение «неизвестно»? Потому что он выполняет перед обратным вызовом, в котором регистрируется # 2. Этот результат может сбить с толку, если вы не думаете асинхронно!

Важно отметить, какие API Firebase являются асинхронными. Их легко обнаружить, потому что их документы по API показывают, что они не возвращают напрямую нужные вам данные. Вместо этого они будут возвращать такие вещи, как задачи и обещания, или принимать блоки завершения или слушателей. Вам нужно будет использовать эти механизмы, чтобы получать результаты звонков. (Чтобы узнать, когда запекание в духовке завершено, повторите предыдущую аналогию.)

Чтобы узнать о Task API, используемом на Android, у меня есть расширенное руководство в блоге Firebase. Обещания JavaScript содержат много документации и примеров, которые вы можете найти с помощью веб-поиска. Блоки завершения Swift также очень хорошо задокументированы Apple.

И обязательно используйте Справочник по Firebase API, чтобы узнать об API Firebase, которые вы хотите использовать, до их фактического использования, чтобы вы могли выяснить, какие вызовы являются асинхронными.

Подумайте об асинхронности!

API Firebase чувствительны к производительности основного потока вашего приложения. Это означает, что любой API Firebase, который должен работать с данными на диске или в сети, реализован в асинхронном стиле. Функции возвращают немедленно, поэтому вы можете вызывать их в основном потоке, не беспокоясь о производительности. Все, что вам нужно сделать, это реализовать обратные вызовы, используя шаблоны, установленные в документации и примерах.

Если у вас есть вопросы по программированию API Firebase, не стесняйтесь задавать их в Stack Overflow с тегом firebase. И обязательно научитесь как задать хороший вопрос, прежде чем размещать свой вопрос.

Для дальнейшего рассмотрения

Проницательные читатели могут знать, что Kotlin делает работу с Android Task API намного чище, чем с Java. Вот первый пример Java, преобразованный в Kotlin. Сравните сами, сколько кода было сохранено здесь:

val task =
    FirebaseFirestore.getInstance().document("users/pat").get()
task.addOnSuccessListener { snapshot ->
    // handle the document snapshot here
}
task.addOnFailureListener { e ->
    // handle any errors here
}

Кроме того, если вы программист на JavaScript, возможно, вы знаете о новом синтаксисе async / await, доступном в ECMAScript 2017 и TypeScript. Это делает пример JavaScript синхронным, хотя это и не так. Посмотрите, насколько чище он выглядит в преобразованном виде:

try {
    const doc = await firebase.firestore().doc("users/pat").get();
    // handle the document snapshot here
}
catch(e) {
    // handle any errors here
}

Это очень похоже на исходный синхронный пример, не так ли?

(Обратите внимание, что это работает только в том случае, если включающая функция объявлена ​​async, что означает, что возвращаемое ею значение будет обещанием, исходящим из последнего ожидания в этой функции.)