
Всем привет! С вами Анна Жаркова, ведущий мобильный разработчик компании Usetech. Продолжаем вам рассказывать про интересные технологии мобильной разработки и об их эффективном применении в приложениях на практике. Сегодня поговорим про то, как с помощью Firebase (без помощи бэкенд-разработчика), а именно облачных хранилищ Firebase Realtime Database/Firestore и Cloud Storage, создать свой собственный бэкенд для мобильного приложения. В качестве примера напишем приложение-аналог известного сервиса с картинками, фотографиями и постами. UI у нас уже готов, подробнее можно посмотреть в этой статье.
Облачные хранилища дают широкий спектр возможностей при работе с ними. Это и бэкап данных, и удобство доступа к ним с другого устройства, и инструментарий для разработки на его основе простого бэкенда для приложения или сервиса.
Начнем с Firebase Realtime Database и Firestore. Это NoSQL (schemeless) хранилища, поддерживающие данные любой структуры. Обе технологии поддерживают обновление запросов в режиме реального времени, работу в оффлайн (при появлении сети данные автоматически синхронизируется с remote-версией), просто и удобно масштабируются.
Внутри Realtime Database данные хранятся в виде Json, Firestore оперирует коллекциями документов (как MongoDB).
Если сравнивать эти 2 хранилища между собой, то Firestore представляет собой расширенную и улучшенную версию функционала Realtime Database:
- больше возможностей записи и транзакций
- сортировка и фильтрация в едином запросе (меньше запросов, меньше нагрузка)
- запрос конкретного документа или коллекции (снижает трафик)
- возможность настроить правила безопасности на конкретный сегмент без каскада.
В целом, для небольшого приложения или сервиса, где нет особой разницы, можно выбрать любое из этих хранилищ. Мы остановим свой выбор на Firestore.
Итак, UI у нас есть, займемся нашей бизнес-логикой. Для работы нам понадобятся:
- Firebase Authentication для авторизации и регистрации нашего пользователя.
- Storage для хранения изображений.
- Firestore для хранения наших данных и отработку запросов.
Начнем с подключения и настройки. Для этого заходим на https://console.firebase.google.com, нажимаем на Add project и проходим по мастеру создания проекта.
В консоли уже созданного проекта выбираем таргет (Add an app to get started) и переходим к мастеру подключения. На этапе 2 скачиваем конфигурационный файл и кладем туда, куда предлагает инструкция:

На этапе 3 копируем код в build.gradle.kts:

Добавляем нужные нам библиотеки:
implementation 'com.google.firebase:firebase-firestore' implementation 'com.google.firebase:firebase-auth-ktx' implementation 'com.google.firebase:firebase-storage-ktx'
Приступим к авторизации с помощью Firebase Authentication. Все операции мы будем проводить через инстанс FirebaseAuth. В нашем решении используется синглтон-хелпер для поддержки работы с Auth.
private val auth: FirebaseAuth by lazy { Firebase.auth }
Через auth мы также можем получить доступ к авторизованному юзеру:
var currentUser: FirebaseUser? = null get() = auth.currentUser //Проверяем факт авторизации по юзеру var isAuthorized: Boolean = false get() = auth.currentUser != null
Нам потребуется реализовать как авторизацию, так и регистрацию. Начнем с проверки:
fun check(hasAuth: (Boolean) -> Unit) { auth.addAuthStateListener(object:FirebaseAuth.AuthStateListener{ override fun onAuthStateChanged(fauth: FirebaseAuth) { hasAuth(fauth.currentUser != null) } }) }
Firebase Authentication поддерживает авторизацию пользователя по email и паролю:
suspend fun signInWithEmail(email: String, password: String): Result<FirebaseUser?> { try { val response = auth.signInWithEmailAndPassword(email, password).await() return Result.Success(auth.currentUser) } catch (e: Exception) { return Result.Error(e) } }
Для выхода просто вызываем signOut:
fun logout() { auth.signOut() }
Для регистрации пользователя нам необходимо сначала создать его:
suspend fun createUser(email: String, password: String):Result<AuthResult?> { return try{ val data = auth .createUserWithEmailAndPassword(email,password) .await() return Result.Success(data) }catch (e : Exception){ return Result.Error(e) } }
Затем с этими же данными проводим авторизацию, изменение профиля пользователя для добавления нужных полей (например, имени):
suspend fun changeProfile(currentUser: FirebaseUser, name: String ) { val request = userProfileChangeRequest { displayName = name } currentUser.updateProfile(request).await() }
Для работы с Firestore нам нужно обращаться через инстанс FirebaseFirestore.getInstance(). При необходимости мы можем включить поддержку оффлайн-режима через настройки:
val db = FirebaseFirestore.getInstance() val settings = firestoreSettings { isPersistenceEnabled = true } db.firestoreSettings = settings
Или изменить размер кэша:
val settings = FirebaseFirestoreSettings.Builder() .setCacheSizeBytes(FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED) .build() db.firestoreSettings = settings
Переходим к работе с лентой постов. Подготовим модельку данных для поста. Для Firestore необходимо пометить Exclude те поля, которые мы отправлять не будем:
class PostItem{ var uuid: String = UUID.randomUUID().toString() var imageLink: String = "" var postText: String = "" var date:String = "" var userId: String = "" var userName: String = "" var likeItems: ArrayList<LikeItem> = arrayListOf() var editedTime: String? = null var editor: String? = null var timeStamp: Date? = null @Exclude @com.google.firebase.firestore.Exclude var hasLike: Boolean = false }
Обратите внимание, что у нас нет вложенных объектов. Firestore не обрабатывает вложенные коллекции автоматически. Если вы хотите использовать такое поле в модель, например, для вывода на UI, исключите его с помощью Exclude.
Чтобы отслеживать посты в режиме реального времени, создаем подписку на события. Указываем ту коллекцию, с которой будем работать. Если коллекции еще нет, она будет создана:
private var listener: ListenerRegistration? = null fun startListenToPosts(result: (List<PostItem>) -> Unit) { val collection = FirebaseFirestore.getInstance().collection("posts") listener = collection.orderBy("timeStamp", Query.Direction.DESCENDING) .addSnapshotListener(MetadataChanges.INCLUDE) { data, firebaseFirestoreExceptioor -> if (data != null) { //Десериализуем в нашу модель данных val posts = data.toObjects(PostItem::class.java) //Вот тут проверяем лайки result(checkLiked(posts)) } } }
Включим в снепшот коллекции метаданные. Если данные к нам придут, десериализуем в нашу модель данных. Первую порцию данных мы получим сразу после подключения. Если посты изменятся или удалятся, листенер получит автоматически изменения.
Для отписки удаляем листенер:
fun stopListening() { listener?.remove() listener = null }
Чтобы добавить новый документ в коллекцию, просто запрашиваем ссылку на него в коллекции по UUID объекта. Если объекта не было, он создастся автоматически, либо мы получим существующий объект:
//Создание suspend fun createPost(postItem: PostItem) { val currentUser = FirebaseAuthHelper.instance.currentUser postItem.userId = currentUser?.uid.orEmpty() postItem.userName = currentUser?.displayName.orEmpty() val collection = FirebaseFirestore.getInstance().collection("posts") val document = collection.document(postItem.uuid) document.set(postItem).await() } //Редактирование suspend fun editPost(postItem: PostItem) { postItem.editor = FirebaseAuthHelper.instance.currentUser?.uid.toString() val collection = FirebaseFirestore.getInstance().collection("posts") val document = collection.document(postItem.uuid) document.set(postItem).await() }
Меняем в модели данных нужные поля и применяем к документу.
Чтобы удалить документ, также получаем его по id из коллекции и вызываем команду delete:
suspend fun deletePost(id: String) { val collection = FirebaseFirestore.getInstance().collection("posts") val document = collection.document(id) document.delete().await() }
Пост может содержать изображение, т.е. ссылку на него. Картинки мы будем грузить в Cloud Storage как массив байтов:
val storage = Firebase.storage suspend fun uploadImage(bytes: ByteArray): Uri? { //Ссылка на объект val reference = storage.reference.child("image-${Date()}.jpg") var uploadTask = reference.putBytes(bytes) // Ожидаем загрузку uploadTask.await() //Обрабатываем конечный url return reference.downloadUrl.await() }
Полученный url добавляем к посту и отправляем на синхронизацию.
Каждый пост связан с внутренней коллекцией комментариев. Но чтобы их получить, нам нужна отдельная подписка именно на эту коллекцию: FirebaseFirestore.getInstance().collection(«posts»).document(postId)
.collection(«comments»). Автоматически для поста мы такие данные не получим.
fun startListenToComments(postId: String, result: (List<CommentItem>) -> Unit) { val collection = FirebaseFirestore.getInstance().collection("posts").document(postId) .collection("comments") commentListener = collection.orderBy("date", Query.Direction.DESCENDING) .addSnapshotListener(MetadataChanges.INCLUDE) { data, firebaseFirestoreExceptioor -> if (data != null) { val comments = data.toObjects(CommentItem::class.java) result(comments) } } } fun stopCommentListening() { commentListener?.remove() commentListener = null }
Создание комментария абсолютно аналогично созданию поста. Запрашиваем элемент в коллекции по его id и меняем поля:
suspend fun sendComment(commentItem: CommentItem) { val currentUser = FirebaseAuthHelper.instance.currentUser commentItem.userId = currentUser?.uid.orEmpty() commentItem.userName = currentUser?.displayName.orEmpty() val collection = FirebaseFirestore.getInstance().collection("posts").document(commentItem.postId) .collection("comments") val document = collection.document(commentItem.uuid) document.set(commentItem).await() }
Помимо создания комментариев, у нас есть поддержка лайков наших постов (как положено). Мы храним в массиве id тех юзеров, кто его лайкнул. Но т.к лайк можно отменить, или несколько юзеров могут делать это одновременно, то тут нам потребуется использовать транзакции:
fun changeLike(postItem: PostItem, onCompleted: (Result<Boolean>) -> Unit) { val currentUser = FirebaseAuthHelper.instance.currentUser val document = FirebaseFirestore.getInstance().collection("posts").document(postItem.uuid) if (currentUser != null) { // Подготавливаем здесь контент FirebaseFirestore.getInstance().runTransaction { transition -> //Обновляем поля transition.update(document, "likeItems", newLikes) newLikes }.addOnSuccessListener { result -> onCompleted(Result.Success(true)) }.addOnFailureListener { e -> onCompleted(Result.Error(e)) } } }
Так как транзакции считаются тяжелой операцией, как пакетные, то рекомендуется подготовить данные заранее, т.е. до блока runTransaction.
И последнее, но не по значению, это безопасность наших данных. Для этого настроим специальные правила безопасности нашего хранилища.
Для того, чтобы их прописать, переходим на вкладку Rules.
По умолчанию у любого пользователя полный доступ к данным и их изменению:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write } } }
Изменим их, чтобы доступ был только у авторизованных пользователей. После внесения новых правил, нужно будет нажать Develop&Test и подождать некоторое время:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function signedIn() { return request.auth.uid != null; } match /users/{user} { allow read, write: if (signedIn() == true); } match /posts/{post} { allow read, write: if (signedIn() == true); } match /comments/{comment} { allow read, write: if (signedIn() == true); } } }
Теперь дадим доступ на редактирование только автору. При этом не забудем, что лайкать и комментировать могут все авторизованные юзеры:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function signedIn() { return request.auth.uid != null; } match /users/{user} { allow read, write: if (signedIn() == true); } match /posts/{post} { allow read: if (signedIn() == true); allow write: if (signedIn() == true); allow update: if ((request.auth != null && request.auth.uid == request.resource.data.userId) || request.resource.data.diff(resource.data).affectedKeys() .hasAny(['likeItems', "comments"])); } match /posts/{post}/comments/{comment} { allow read, write: if (signedIn() == true); } match /comments/{comment} { allow read, write: if (signedIn() == true); } } }
Готово, мы великолепны. Теперь наше приложение с лентой постов функционирует, как нужно.
Ссылка на исходники:
https://github.com/anioutkazharkova/android-firestore_realtime/
Полезные статьи:
https://firebase.google.com/docs/firestore
https://firebase.google.com/docs/auth
ссылка на оригинал статьи https://habr.com/ru/company/usetech/blog/719102/
Добавить комментарий