Android SharedPreferences Tutorial and Example

Reading and Writing Data to Firebase Firestore in Android

Cloud databases make it easier for an Android app to store information beyond the device. Firebase Cloud Firestore is a document-based database that supports real-time updates, offline caching, authentication integration and flexible queries. It is a practical alternative to using SQLite when several users need access to the same data.

For Android developers, the main tasks are creating a Firebase project, adding the Android app, configuring the Gradle dependencies and writing Kotlin code that communicates with a Firestore collection. The same approach works for shopping lists, bookings, chat messages, stock records and user profiles.

A small café application in Melbourne, for example, could store menu items in a products collection and update availability during the day. A customer in Brisbane could read the latest data while staff members update stock from another device.

Firestore stores data as collections containing documents. Each document has fields such as strings, numbers, Boolean values, timestamps or nested maps. This structure is different from relational SQLite tables, so planning document names and field types before coding will prevent inconsistent records.

Configure Firestore in an Android Project

Create a project in the Firebase Console, register your Android application using its package name, and download google-services.json. Place that file in the application module directory, usually app/. Firebase Assistant in Android Studio can also help connect an existing project.

Add the Google services plugin and the Firebase Firestore library. With a modern Gradle version catalog, the dependency may look like this:

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    implementation("com.google.firebase:firebase-firestore")
}

The Firebase BoM keeps related Firebase libraries compatible. In an application class or activity, obtain a database reference:

private val firestore = FirebaseFirestore.getInstance()
private val products = firestore.collection("products")

Keep database access outside the user interface where possible. A repository or ViewModel can call Firestore and expose loading, success and error states to a Compose screen or XML-based activity. This separation makes the code easier to test and prevents network work from being mixed into click listeners.

Add Documents and Store Application Data

Use add() when Firestore should generate a unique document ID. This is suitable for a new order or feedback entry:

val product = hashMapOf(
    "name" to "Flat white",
    "price" to 5.50,
    "available" to true,
    "createdAt" to FieldValue.serverTimestamp()
)

products.add(product)
    .addOnSuccessListener { documentReference ->
        Log.d("Firestore", "Created ${documentReference.id}")
    }
    .addOnFailureListener { error ->
        Log.e("Firestore", "Write failed", error)
    }

For a known ID, use document(id).set(data). Calling set() creates the document if it does not exist and replaces its fields by default. Use SetOptions.merge() when existing fields must be retained:

val userData = mapOf(
    "displayName" to "Alex",
    "city" to "Sydney"
)

firestore.collection("users")
    .document("user_123")
    .set(userData, SetOptions.merge())

A Firestore write is asynchronous. The success callback means the request was accepted, while the failure callback can expose permission problems, unavailable connectivity or invalid data. Avoid reporting success in the UI before the task finishes, especially when an Australian customer is using mobile data on a train or in an area with limited coverage.

Read Collections and Query Documents

To retrieve documents once, call get() and convert each result into a Kotlin data class. toObjects() is convenient when property names match Firestore field names:

data class Product(
    val name: String = "",
    val price: Double = 0.0,
    val available: Boolean = false
)

products.whereEqualTo("available", true)
    .orderBy("name")
    .get()
    .addOnSuccessListener { snapshot ->
        val items = snapshot.toObjects<Product>()
        items.forEach { item ->
            Log.d("Firestore", "${item.name}: ${item.price}")
        }
    }
    .addOnFailureListener { error ->
        Log.e("Firestore", "Read failed", error)
    }

Queries can filter by equality, ranges and arrays, then limit or sort the returned documents. Some compound queries require a Firestore index. If that happens, the error normally includes a link that opens the Firebase Console with the required index configuration.

For live data, use a snapshot listener. Firestore calls the listener when the collection changes and can provide cached results while the device is offline. Always remove listeners when a screen is destroyed or when a lifecycle-aware component stops observing, otherwise an activity can continue receiving updates unnecessarily.

Operation Kotlin method Suitable use
Create with automatic ID collection.add(data) New orders, comments or events
Create or replace by ID document(id).set(data) Profiles or records with known keys
Partially update document(id).update(fields) Changing stock or a single preference
Read once get() Loading a screen or running a one-off search
Observe changes addSnapshotListener() Chat, live stock and shared lists
Remove a document document(id).delete() Deleting an obsolete record

Update, Delete and Secure Firestore Records

Use update() when a document already exists and only selected fields should change:

products.document(productId)
    .update(
        "available", false,
        "updatedAt", FieldValue.serverTimestamp()
    )
    .addOnSuccessListener {
        Log.d("Firestore", "Product updated")
    }

Delete operations should normally require a confirmation in the user interface. A soft-delete field such as archived = true can be preferable when records may be needed for reporting, refunds or customer support. Firestore also supports transactions and batched writes when several related changes must succeed together.

Do not leave Firestore in test mode for a released application. Security Rules should verify authentication and ownership. A basic user-owned document rule could be:

match /users/{userId} {
  allow read, write: if request.auth != null
                    && request.auth.uid == userId;
}

Rules are not a replacement for careful data design. Avoid storing payment card details or unnecessary personal information. If an app collects names, addresses or location data from people in Australia, consider the Privacy Act 1988 and the Australian Privacy Principles. Explain collection, retention and access practices in a privacy policy, and limit staff access to the records required for their role.

Handle Offline Use, Costs and Production Testing

Firestore includes offline persistence on Android, which helps apps continue displaying previously loaded data and queue suitable writes until connectivity returns. This is useful for a stocktaking app used in a regional area or a bushwalking checklist opened away from reliable reception. The app must still show users whether information is pending, cached or confirmed by the server.

Choose a Firestore location close to the intended users, such as australia-southeast1 in Sydney, when creating the database. Melbourne, Brisbane and other Australian cities can benefit from lower latency than an overseas region, although the best choice depends on the full user base and any organisational data-residency requirements. A database location cannot be changed casually later, so make the decision before production data is added.

Firestore billing is based largely on document reads, writes, deletes and stored data. A listener that refreshes a large collection can become expensive, particularly if it is attached to every screen. Use pagination, limit(), targeted queries and compact documents. Test with the Firebase Local Emulator Suite, use realistic Australian time zones and prices in dollars, and inspect security rules before release. A production-ready app should also handle empty results, duplicate taps, permission errors and a user signing out while a request is still running.