In this guide
Almost every Android final-year project needs the same four backend pieces: user login, a database that syncs, push notifications, and crash reports. Building those from scratch means a server, a database, an API, authentication, and hosting — a second project hiding inside your project. Firebase gives you all four as managed services with a generous free tier, wired into Android Studio in an afternoon. This guide walks through the full setup: creating the Firebase project, connecting it to your Android app, then enabling Authentication, Realtime Database, Cloud Messaging, and Crashlytics — with the free-tier quotas and the security-rules gotchas that trip up nearly every student team.
What you need: Android Studio installed, a project with
minSdk 24or higher (Firebase's current libraries target recent API levels), and a Google account. The code samples are in Kotlin with Groovy Gradle files; the steps are identical in Java.
Firebase in one paragraph
Firebase is Google's backend-as-a-service platform: instead of running your own server, your app talks directly to Google's infrastructure through SDKs. For a student project this removes server deployment, database administration, and most backend code. The tradeoff is lock-in and quota limits — both acceptable for a final-year app, both worth understanding before you design around them (see the quotas section below).
The four services this guide sets up, and what each replaces:
| Service | What it does | Replaces |
|---|---|---|
| Authentication | Sign-in with email/password, Google, phone, or anonymous | Your own user table + password hashing + session handling |
| Realtime Database | JSON database that syncs to all clients in real time | Your own API + WebSocket server + database |
| Cloud Messaging (FCM) | Push notifications to Android devices | Your own push infrastructure (which you cannot build — only FCM/APNs can wake a device reliably) |
| Crashlytics | Crash reports with stack traces, grouped by cause | Users telling you "it crashed" with no details |
Real projects shaped like this: a smart attendance tracker (auth for teachers, realtime sync of attendance records, FCM to notify students), a college bus tracker (driver location streaming to students via Realtime Database), or an offline note-taking app with a sync dashboard (local-first storage syncing to the cloud when online).
Realtime Database vs Firestore: pick before you build
Firebase offers two databases. Picking wrong means migrating mid-project, so decide now:
| Realtime Database | Cloud Firestore | |
|---|---|---|
| Data model | One big JSON tree | Collections of documents (closer to MongoDB) |
| Queries | Limited: order/filter on one field | Rich: compound queries, pagination |
| Offline support | Yes (one line to enable) | Yes, more sophisticated |
| Pricing model | Charged on data transferred/stored | Charged per read/write operation |
| Student verdict | Default choice for final-year apps. Simpler rules, simpler code, and the free tier covers typical demo loads | Choose only if you need complex queries (search, filters, pagination) |
This guide uses Realtime Database throughout. If your app needs "show buses filtered by route, sorted by ETA, paginated" — that is Firestore-shaped. If it needs "sync this JSON to every connected phone" — that is Realtime Database.
Step 1 — Create the Firebase project and register your app
- Go to the Firebase console, click Add project, name it (e.g.
attendance-app-final-year), and continue. Google Analytics is optional for a student project — you can disable it. - On the project overview, click the Android icon to add an Android app.
- Enter your app's package name exactly as it appears in your app-level
build.gradle(applicationId). This is the critical field:com.example.attendancein the console must matchcom.example.attendancein Gradle, character for character. A mismatch is the single most common setup failure. - Download
google-services.jsonand place it in your app/ module directory (next tobuild.gradle, not insidesrc/). - The SHA-1 certificate fingerprint is optional for email/password auth but required for Google Sign-In. Get your debug SHA-1 with:
Paste the SHA-1 into the Firebase app settings. You will add a separate release SHA-1 when you publish (see the Play Store guide's keystore section).keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Step 2 — Wire up Gradle
Firebase's Android SDKs are distributed through the Google Maven repository and configured with the Google Services Gradle plugin. Two files change.
Project-level build.gradle (or settings.gradle plugin management):
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
}
}
App-level build.gradle:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
}
android {
// ... your existing config ...
}
dependencies {
// Firebase Bill of Materials: one version to rule the SDKs below.
// The BOM guarantees all Firebase libraries are mutually compatible.
implementation platform('com.firebase:firebase-bom:33.1.0')
implementation 'com.google.firebase:firebase-auth'
implementation 'com.google.firebase:firebase-database'
implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-analytics'
}
The BOM (Bill of Materials) is worth understanding: without it you pin each library's version by hand and eventually combine two incompatible ones. With it, you declare versions once and Firebase resolves a tested set. When something breaks after adding Firebase, the first suspect is always a version conflict — the BOM removes that entire category.
Sync Gradle. If the sync fails with "google-services.json missing", the file is in the wrong directory — it belongs in app/, alongside the app-level build.gradle.
Step 3 — Authentication: login in an afternoon
In the Firebase console: Build → Authentication → Get started → Sign-in method, and enable Email/Password. (Add Google sign-in later; email/password is enough to demo the full flow.)
The minimal working flow in Kotlin:
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
class AuthRepository {
private val auth: FirebaseAuth = Firebase.auth
fun signUp(email: String, password: String, onDone: (Boolean, String) -> Unit) {
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
onDone(true, "Signed up: " + (auth.currentUser?.email ?: ""))
} else {
onDone(false, task.exception?.message ?: "Unknown error")
}
}
}
fun signIn(email: String, password: String, onDone: (Boolean, String) -> Unit) {
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
onDone(true, "Welcome back")
} else {
onDone(false, task.exception?.message ?: "Unknown error")
}
}
}
fun signOut() {
auth.signOut()
}
fun currentUid(): String? = auth.currentUser?.uid
}
Two details that save hours:
- The user persists across restarts.
Firebase.auth.currentUsersurvives app kills — check it in your launcher activity and skip the login screen when non-null. Half of student "login bugs" are just missing this check. - Anonymous auth is a demo superpower. Enable it in the console and call
auth.signInAnonymously()— reviewers can try the app instantly without creating an account, and you can upgrade the anonymous account to a real one later. For viva demos, this removes the most awkward 60 seconds ("please register… check your email…").
Step 4 — Realtime Database: sync in real time
In the console: Build → Realtime Database → Create Database, choose a region near your users, and start in locked mode. Locked mode denies all reads/writes by default — the safe starting point; you then open exactly what your app needs (next section).
Enable offline persistence once, in your Application class:
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
class App : Application() {
override fun onCreate() {
super.onCreate()
Firebase.database.setPersistenceEnabled(true)
}
}
With persistence on, reads and writes work offline and sync when connectivity returns — this is what makes the bus tracker keep recording the route through a tunnel.
Writing and listening:
val db = Firebase.database.reference
// Write: attendance record keyed by date + student
fun markAttendance(date: String, studentId: String) {
db.child("attendance").child(date).child(studentId)
.setValue(true)
.addOnFailureListener { e ->
Log.e("DB", "Write failed: " + e.message)
}
}
// Listen: every connected phone updates live when data changes
fun listenToAttendance(date: String, onChange: (Map<String, Boolean>) -> Unit) {
db.child("attendance").child(date)
.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val map = mutableMapOf<String, Boolean>()
for (child in snapshot.children) {
map[child.key ?: ""] = child.getValue(Boolean::class.java) ?: false
}
onChange(map)
}
override fun onCancelled(error: DatabaseError) {
Log.e("DB", "Listen cancelled: " + error.message)
}
})
}
Structure your data for how you read it. The JSON tree has no joins, so denormalise deliberately: if the student list screen needs names, store the name alongside the attendance record rather than looking it up per student. Keep trees shallow — deep nesting forces clients to download subtrees they do not need, which burns your free data-transfer quota.
Step 5 — Security rules: the part everyone skips (do not skip it)
The default locked-mode rules deny everything, so your first database call will fail with DatabaseError: Permission denied. That error is the rules working. Now write rules that allow exactly what your app needs — nothing more.
For a teacher/student attendance app where signed-in users may read and write attendance:
{
"rules": {
"attendance": {
"$date": {
".read": "auth != null",
".write": "auth != null"
}
},
"users": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
}
}
Read this as: anyone signed in can read/write attendance records; each user can only read/write their own profile node. $date and $uid are wildcards matching any child key.
The rule every student team must internalise: never ship with ".read": true, ".write": true at the root beyond a local demo. Public write access means anyone on the internet can overwrite your database — and automated scanners do find open Firebase databases within days. Firebase will email warnings about insecure rules; treat those emails as urgent. Use the console's Rules Playground (in the Realtime Database → Rules tab) to simulate reads/writes as authenticated vs unauthenticated users before publishing rules.
Step 6 — Cloud Messaging: push notifications
FCM is the only reliable way to wake an Android app with a notification — do not attempt polling or background sockets for alerts.
1. Add the service to AndroidManifest.xml inside <application>:
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
2. Implement the service:
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// Send this token to your database so the backend/console
// knows which device to target.
Firebase.database.reference
.child("fcmTokens")
.child(Firebase.auth.currentUser?.uid ?: "anon")
.setValue(token)
}
override fun onMessageReceived(message: RemoteMessage) {
val title = message.notification?.title ?: "Update"
val bodyText = message.notification?.body ?: ""
showNotification(title, bodyText) // your NotificationCompat code
}
}
3. Test from the console: Build → Cloud Messaging → Send your first message, paste the FCM token (log it once with FirebaseMessaging.getInstance().token), and the notification should arrive within seconds — app in background or killed. If it does not arrive: check the token is current (tokens rotate), confirm the device has Google Play services, and verify battery optimisation is not killing the app on the test device (a common issue on some OEM skins).
For a bus tracker, the pattern is: driver app writes location → a small Cloud Function (or your demo laptop) detects "bus within 500 m of stop" → sends FCM to subscribed students. Even the manual version — sending from the console during the demo — is a legitimate viva demonstration of the pipeline.
Step 7 — Crashlytics: know about crashes before the examiner does
Enable it in the console (Release & Monitor → Crashlytics → Enable), run the app once, then force a test crash to verify the pipeline:
// Temporary: wire to a hidden debug button, then remove it
throw RuntimeException("Crashlytics test crash")
Relaunch the app (Crashlytics uploads on next start), and within a few minutes the crash appears in the console with the full stack trace, device model, and OS version — grouped so 50 identical crashes from one bug show as one issue with a count.
Why this matters for a final-year project: the examiner's phone is never your phone. Different OEM, different Android version, different permissions state. Crashlytics is how you learn that the app crashes on Android 12 when location permission is denied — before the demo, if you distribute a test build to two friends with different phones a week early. Custom keys (FirebaseCrashlytics.getInstance().setCustomKey("screen", "checkout")) tell you which screen the user was on, which turns "it crashed" into "it crashed on the payment screen for users without a saved address".
Free-tier quotas: what "free" actually covers
Firebase's Spark (free) plan is genuinely enough for a final-year project — but the limits are real, and designing past them produces a demo-day outage. Documented Spark-plan limits for the services in this guide (always re-check the Firebase pricing page; quotas change):
| Service | Free-tier limit (Spark plan) | What it means for your demo |
|---|---|---|
| Realtime Database | 100 simultaneous connections, 1 GB stored, 10 GB/month downloaded | ~100 phones connected at once is plenty for a class demo; 10 GB/month covers thousands of small syncs |
| Authentication | 50,000 monthly active users | Effectively unlimited for a student project |
| Cloud Messaging | No charge for messaging | Send as many notifications as you like |
| Crashlytics | Free, no quota | Leave it on permanently |
Two quota traps to avoid: (a) a ValueEventListener on a huge node re-downloads the whole node on every change — attach listeners to the smallest subtree you need; (b) logging every GPS ping to the database at 1 Hz from 30 test phones will eat the 10 GB download budget surprisingly fast — throttle location writes to every 5–10 seconds and only while the trip is active.
Diagnosis table: the errors you will definitely see
| Error / symptom | Likely cause | Fix |
|---|---|---|
google-services.json / "File google-services.json is missing" at build |
File in the wrong directory or wrong filename | Must be app/google-services.json, exact name, next to app-level build.gradle |
No matching client found for package name |
Package name in Firebase console ≠ applicationId in Gradle |
Make them identical; re-download google-services.json after changing |
DatabaseError: Permission denied |
Locked-mode rules deny everything by default | Write rules opening exactly your paths to auth != null (see Step 5); test in Rules Playground |
Google Sign-In returns DEVELOPER_ERROR (code 10) |
SHA-1 fingerprint missing in console | Add debug (and later release) SHA-1 to the Firebase app settings; re-download google-services.json |
| FCM token null / notifications never arrive | Play services missing, or battery optimisation killing the app | Test on a device with Play services; exempt the app from battery optimisation during testing |
| Crashlytics shows no crashes after a test crash | Crash uploaded on next app start; needs one relaunch | Kill and relaunch the app, wait a few minutes, check console |
Duplicate class build errors after adding Firebase |
Version conflict between Firebase and another library | Use the Firebase BOM and remove hand-pinned Firebase versions |
| Auth works in debug, fails in release | Release build has a different SHA-1; Google sign-in rejects it | Add the release keystore's SHA-1 to the console before publishing |
Data modeling patterns for common student apps
Three shapes cover most final-year apps. Notice how each is deliberately denormalised — data duplicated so reads stay shallow and fast:
Chat / discussion (per-room messages):
{
"rooms": {
"room1": { "name": "Project group", "createdBy": "uid_aaa" }
},
"messages": {
"room1": {
"msg1": { "sender": "uid_aaa", "senderName": "Aarav",
"text": "Demo at 4?", "ts": 1727000000 }
}
}
}
senderName is duplicated into each message so the chat screen reads one node instead of joining the users table per message.
Attendance (date-keyed): attendance/{date}/{studentId} = true, alongside a separate students/{studentId} = {name, rollNo} roster node. Marking reads the tiny date node; display joins the roster only where names are shown.
Leaderboard: scores/{uid} = {name, points}, queried with orderByChild("points").limitToLast(10) — the database sorts, the client takes the top ten.
The rule behind all three: model for your reads, not for elegance. A normalised schema needing three round-trips per screen feels broken on a slow college network; a duplicated schema that reads in one trip feels instant. Duplication is cheap; latency is not.
Worked quota check: a bus tracker
Sanity-check the free tier against realistic load before demo day, or the demo-day outage will do it for you. Ten buses, one location write every 10 seconds, 12 operating hours:
- Writes per day: 10 buses × 6/min × 60 min × 12 h = 43,200 writes
- Payload ~200 bytes → roughly 8.6 MB uploaded per day
- Thirty students watching: each holds a listener on their bus's node, and every location update pushes ~200 bytes to each watcher — on the order of 260 MB downloaded per day in the heaviest case
Against the Spark limits (1 GB stored, 10 GB/month downloaded, 100 simultaneous connections): storage and connections are comfortable, but ~7–8 GB/month of downloads is close enough to the 10 GB ceiling that you should throttle writes to every 15 seconds and detach listeners when the app goes to background. Redo this arithmetic with your own numbers — ten minutes of math prevents the "database stopped syncing during the demo" incident, which is almost always a quota or a security rule, never a code bug.
When you outgrow client-only code: Cloud Functions
Some logic should not live in the app: sending an FCM alert when a bus nears a stop, aggregating daily attendance into a report, or validating data a malicious client could forge. That is Cloud Functions — small Node.js functions triggered by database writes, auth events, or HTTPS calls. Two student-relevant facts: functions require the Blaze (pay-as-you-go) plan, whose monthly free tier covers modest use — and you must set a budget alert on day one so a runaway function cannot surprise you. Treat functions as version two: ship the client-direct version first, and add a function only for the one trigger your demo cannot do without.
Setup checklist
- Firebase project created; Android app registered with the exact
applicationId. -
google-services.jsoninapp/; Gradle syncs cleanly with the BOM. - SHA-1 debug fingerprint added (required for Google Sign-In; do release later).
- Email/Password auth enabled; login → logout → relaunch-keeps-session tested.
- Realtime Database created in locked mode; rules written for your exact paths; tested in Rules Playground.
- Offline persistence enabled; airplane-mode test passes (writes sync on reconnect).
- FCM service declared; test notification received with the app killed.
- Crashlytics enabled; test crash visible in console; custom keys added on key screens.
- No
".write": trueat the database root; insecure-rules warning email resolved. - Quota math done: listener scopes minimal, write frequencies throttled.
Putting it together
The order in this guide is the order to build: console project → Gradle wiring → auth → database + rules → messaging → crash reporting. Each step is independently testable, so a broken step 6 never blocks step 3's demo. Before your final testing round, run the app through the pre-submission testing checklist with Firebase's console open beside it — watching live database writes and incoming crash reports during someone else's test session is the closest thing to a rehearsal your backend gets.