Android SharedPreferences Tutorial and Example

Saving Android App Data with Cloud Firestore

Cloud Firestore gives Android apps a server-side place to store profiles, notes, and shared records. The Firebase SDK handles document reads, writes, and live updates, so a phone does not need a reliable connection for every action. This suits Australians moving between Sydney’s transport network, regional mobile coverage, and household NBN connections.

Firestore organises information into documents and collections rather than rows and tables. A document can represent one saved trail note, while a collection holds related records. Subcollections provide another way to group data without placing every field in one large map.

Consider a walking-notes app used by a group travelling from Adelaide to the Great Ocean Road. Each person creates notes tagged with a route, and authorised group members see changes on their devices. A local database could support offline use, while Firestore provides a shared backend across multiple phones.

The implementation starts with Firebase setup, a document model, and Android code for creating and observing records. It also covers cached access when reception disappears, security rules, and production testing. Developers should compare Firestore with SharedPreferences or SQLite because cloud synchronisation is not automatically the best choice for every value an app stores.

Connect the Android app to Firebase

Open the Firebase console, create a project, and add an Android app. Its package name must match the applicationId in your Gradle configuration exactly. Download google-services.json, place it in the app directory, and apply the Google services plugin:

plugins {
    id("com.google.gms.google-services")
}

Add the Firebase Android BoM and the required libraries to the app module. Use a current BoM version so Firestore and Firebase Authentication remain compatible:

implementation(platform("com.google.firebase:firebase-boom:33.7.0"))
implementation("com.google.firebase:firebase-firestore")
implementation("com.google.firebase:firebase-auth")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services")

The Android configuration file normally handles Firebase initialisation. Authentication is useful because rules can associate each document with a Firebase user ID. This matters for Australia’s substantial Google Play audience, where an Android-first release is a normal commercial choice.

Design a Firestore document model

Create a Kotlin data class that represents one document. The @DocumentID annotation lets Firestore populate the identifier, while a nullable timestamp accommodates the short delay before a server timestamp reaches the client.

data class TrailNote(
    @DocumentID val id: String = "",
    val ownerId: String = "",
    val title: String = "",
    val body: String = "",
    val locationName: String = "",
    val createdAt: Timestamp? = null
)

Firestore accepts Kotlin objects, maps, or custom objects. Maps are convenient when a field is optional, but typed classes make invalid states easier to spot. Store images in Cloud Storage and keep their download URLs in Firestore; embedding large image files in documents increases transfer costs and memory use.

Use FieldValue.serverTimestamp() for creation and modification times instead of trusting each phone’s clock. Avoid deeply nested structures, and keep frequently queried fields at the document’s top level. For larger records, create subcollections such as trailNotes/{noteId}/comments/{commentId}.

Create and observe cloud records

A write should include the authenticated user’s identifier and let the server generate its timestamp. The await() extension converts the Firestore task into a coroutine that the caller can manage through normal Android lifecycle code.

suspend fun addTrailNote(
    title: String,
    body: String,
    locationName: String
) {
    val uid = FirebaseAuth.getInstance().currentUser?.uid ?: return

    val data = mapOf(
        "ownerId" to uid,
        "title" to title,
        "body" to body,
        "locationName" to locationName,
        "createdAt" to FieldValue.serverTimestamp()
    )

    firestore.collection("trailNotes").add(data).await()
}

Observe the collection with a snapshot listener. Firestore can deliver an initial result, local changes, and remote updates through the same listener:

val uid = FirebaseAuth.getInstance().currentUser?.uid ?: return

firestore.collection("trailNotes")
    .whereEqualTo("ownerId", uid)
    .orderBy("createdAt", Query.Direction.DESCENDING)
    .addSnapshotListener { snapshot, error ->
        if (error != null) {
            Log.e("Firestore", "Read failed", error)
        } else {
            val notes = snapshot?.documents.orEmpty()
                .mapNotNull { it.toObject(TrailNote::class.java) }
        }
    }

The right storage approach depends on how an app uses its information:

Storage choice Best suited to Offline behaviour Main caution
SharedPreferences Small settings and preferences Available locally Unsuitable for substantial records
Room or SQLite Large local datasets and offline-first source of truth Fully available offline Cloud synchronisation must be designed separately
Cloud Firestore Shared records, live updates, and multi-device access Cached reads and queued writes Requires rules, indexes, and cost monitoring

Work through weak connectivity

Android Firestore clients maintain a local cache, and writes can be queued when the device cannot reach the service. This is valuable on a Melbourne train with intermittent reception or in regional areas where mobile coverage changes quickly. The UI should distinguish a confirmed server value from a pending local write instead of presenting uncertain data as final.

Snapshot metadata reports whether a document came from the cache or server and whether changes still contain pending writes. A practical interface shows a small pending indicator, preserves the user’s text, and reports a persistent failure with a retry action. Avoid editing the same document concurrently across several devices, because concurrent writes can overwrite one another. Model independent events as separate documents when every change must survive.

Caching reduces repeated network reads but does not remove the need for connectivity handling. Close listeners when their screen or component is destroyed, and avoid attaching several listeners to the same query. On shared café Wi-Fi, the client should still assume that the network can be unavailable, slow, or operating on metered mobile data.

Secure data with Firebase rules

Authentication identifies the caller, but security rules determine what that caller may read or change. A basic ownership rule prevents one account from accessing another account’s trail notes:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function signedIn() {
      return request.auth != null;
    }
    function ownsExisting() {
      return signedIn() && resource.data.ownerId == request.auth.uid;
    }
    allow read: if ownsExisting();
    allow create: if signedIn()
      && request.resource.data.ownerId == request.auth.uid;
    allow update, delete: if ownsExisting()
      && request.resource.data.ownerId == request.auth.uid;
  }
}

Rules are filters rather than automatic query restrictions. If a query requests documents belonging to another user, it is rejected rather than quietly hiding those documents. Test rules with the Firestore emulator and test users before deploying them to production.

For organisations subject to the Privacy Act 1988 and the Australian Privacy Principles, personal information needs a defined purpose, appropriate handling, and clear access and correction processes. Precise location, health details, and photographs can raise additional sensitivity. The Notifiable Data Breaches scheme may also apply when eligible personal information is compromised.

Test and prepare for production

Run Android integration tests against the Firestore emulator, including rule violations, offline writes, reconnects, and deleted accounts. A composite query such as filtering by ownerId and sorting by createdAt may require a composite index, which Firestore can create through a generated configuration file. Track rejected requests and client exceptions in Crashlytics or another monitoring service.

Review Firestore’s read, write, storage, and network charges, then configure billing alerts in Australian dollars. Google Cloud provides Australian regions in Sydney (australia-southeast1) and Melbourne (australia-southeast2); check the current Firestore location picker for the required residency option. A database location cannot be changed after creation, so this decision belongs in the initial architecture review. A reliable release process combines emulator tests, rule deployment, staged Android releases, and monitored usage limits.