Unit 1 - Notes
Unit 1: Building Cloud-Enabled Applications with Firebase
1. Introduction to Firebase
Firebase is a Backend-as-a-Service (BaaS) platform developed by Google. It provides developers with a suite of tools and services to build high-quality web and mobile applications quickly, without the need to manage backend infrastructure like servers, databases, or authentication systems.
Key Concepts:
- BaaS (Backend-as-a-Service): A cloud computing service model that serves as the backend for an application. It provides pre-built components for common backend tasks, such as user authentication, database management, cloud storage, and push notifications. This allows frontend developers to focus on the user experience and client-side logic.
- Serverless Architecture: Firebase operates on a serverless model. This means you don't have to worry about provisioning, scaling, or maintaining servers. Google handles all the backend infrastructure, and it automatically scales with your app's usage.
- Real-time Capabilities: A core feature of many Firebase services is their real-time nature. Data is synchronized across all connected clients almost instantaneously, enabling collaborative and highly responsive applications.
2. Features of Firebase
Firebase is a comprehensive platform with a wide range of services, often categorized into three groups: Build, Release & Monitor, and Engage.
Build
- Authentication: Securely manage users with pre-built UI and SDKs for various providers (email/password, phone, Google, Facebook, Twitter, etc.).
- Real-time Database: A NoSQL cloud database that stores and syncs data in real-time as a large JSON tree. It's ideal for applications requiring low-latency data synchronization.
- Cloud Firestore: The next generation of the Real-time Database. A more powerful, scalable, and flexible NoSQL document database with richer querying capabilities.
- Cloud Storage: Store and manage user-generated content like images, audio, and videos. The SDKs provide robust upload and download functionalities.
- Cloud Functions: Run backend code in response to events triggered by Firebase features and HTTPS requests, without managing servers. For example, you can run a function when a new user signs up or a new document is added to the database.
- Firebase Hosting: Fast and secure web hosting for static assets (HTML, CSS, JS), single-page apps, and dynamic content with Cloud Functions integration.
- ML Kit: Brings Google's machine learning expertise to mobile apps with a set of powerful, easy-to-use on-device and cloud-based APIs (e.g., text recognition, face detection, image labeling).
Release & Monitor
- Crashlytics: A powerful crash reporter that provides real-time insights into app stability issues, helping you track, prioritize, and fix bugs.
- Performance Monitoring: Gain insights into your app's performance characteristics from the user's perspective, identifying and resolving performance bottlenecks.
- Test Lab: Test your application on a wide range of physical and virtual devices hosted in Google's data centers.
Engage
- Google Analytics: A free, unlimited analytics solution that provides deep insights into user behavior, app usage, and engagement.
- Cloud Messaging (FCM): Send push notifications and messages to users across platforms (Android, iOS, Web) for free.
- Remote Config: Customize your app's behavior and appearance for different user segments on the fly, without shipping a new version.
- A/B Testing: Run experiments to test changes to your app's UI, features, or engagement campaigns to see what works best.
3. Connecting Firebase to an Android App
Connecting your Android project to Firebase is a straightforward process handled through the Firebase console and Android Studio.
Step 1: Create a Firebase Project
- Go to the Firebase Console.
- Click "Add project".
- Enter a project name, accept the terms, and click "Continue".
- (Optional but recommended) Enable Google Analytics for the project.
- Click "Create project".
Step 2: Register Your App with Firebase
- In your new project's dashboard, click the Android icon ( </> ) to add an Android app.
- Android package name: Enter your app's package name. You can find this in your
app/build.gradlefile (theapplicationIdproperty). This must be unique and cannot be changed later. - App nickname (Optional): An internal-facing name for your app.
- Debug signing certificate SHA-1 (Optional but required for some features): This is needed for Firebase Authentication (Google Sign-In, Phone Auth) and other services.
- You can get your debug SHA-1 key by running the following command in the Android Studio terminal:
BASH./gradlew signingReport - Copy the SHA-1 value from the
debugvariant.
- You can get your debug SHA-1 key by running the following command in the Android Studio terminal:
- Click "Register app".
Step 3: Add the Firebase Configuration File
- Firebase will prompt you to download a configuration file named
google-services.json. - Download this file.
- Switch to the Project view in Android Studio's project pane.
- Move the
google-services.jsonfile into theappdirectory of your Android project.
Step 4: Add Firebase SDKs to Your App
-
Project-level
build.gradle(<project>/build.gradle):
Ensure you have Google's Maven repository and add the Google services classpath.
GROOVY// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { // ... other dependencies classpath 'com.google.gms:google-services:4.3.15' // Use the latest version } } allprojects { repositories { google() mavenCentral() } } -
App-level
build.gradle(<project>/app/build.gradle):
Apply the Google services plugin and add the desired Firebase dependencies.
GROOVYplugins { id 'com.android.application' id 'kotlin-android' // Add the Google services plugin id 'com.google.gms.google-services' } dependencies { // ... other dependencies // Import the Firebase BoM (Bill of Materials) // This allows you to manage all Firebase library versions by specifying only the BoM version. implementation platform('com.google.firebase:firebase-bom:32.2.3') // Add specific Firebase SDKs without specifying versions implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.google.firebase:firebase-auth-ktx' implementation 'com.google.firebase:firebase-database-ktx' // For Firebase UI (optional, but very useful) implementation 'com.firebaseui:firebase-ui-auth:8.0.2' } -
Click "Sync Now" in Android Studio. Your app is now connected to Firebase.
4. Firebase Authentication
Firebase Authentication provides a complete backend service to authenticate users. It supports various providers and integrates tightly with other Firebase services.
Core Concepts
- FirebaseAuth instance: The main entry point for the Authentication SDK.
FirebaseAuth.getInstance() - FirebaseUser object: Represents a signed-in user's profile information. It contains a unique UID, display name, email, photo URL, etc.
- AuthStateListener: A listener that gets triggered whenever the user's sign-in state changes (e.g., user signs in, user signs out). This is crucial for updating the UI.
Email and Password Authentication
1. Enable in Firebase Console
- Go to Firebase Console -> Authentication -> Sign-in method.
- Enable the "Email/Password" provider.
2. Create a New User
val auth = FirebaseAuth.getInstance()
fun createUser(email: String, password: String) {
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Sign up success, update UI with the signed-in user's information
Log.d("AUTH", "createUserWithEmail:success")
val user = auth.currentUser
// Navigate to main activity or update UI
} else {
// If sign up fails, display a message to the user.
Log.w("AUTH", "createUserWithEmail:failure", task.exception)
Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show()
}
}
}
3. Sign In an Existing User
val auth = FirebaseAuth.getInstance()
fun signInUser(email: String, password: String) {
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// Sign in success
Log.d("AUTH", "signInWithEmail:success")
val user = auth.currentUser
} else {
// If sign in fails
Log.w("AUTH", "signInWithEmail:failure", task.exception)
Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show()
}
}
}
4. Manage User State
It's best practice to check the user's current state when your activity starts.
private lateinit var auth: FirebaseAuth
override fun onStart() {
super.onStart()
auth = FirebaseAuth.getInstance()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
if(currentUser != null){
// User is already signed in, navigate to the main screen
} else {
// User is not signed in, show the login screen
}
}
5. Sign Out
fun signOut() {
FirebaseAuth.getInstance().signOut()
// Navigate back to the login screen
}
5. Firebase UI
FirebaseUI is an open-source library built on top of the Firebase SDK that provides pre-built UI flows for common tasks, most notably authentication.
Benefits:
- Rapid Development: Drastically reduces the amount of boilerplate code needed for sign-in, sign-up, and password recovery flows.
- Best Practices: Implements best practices for user authentication, handling edge cases like account linking.
- Customizable: Supports theming and customization to match your app's branding.
Implementing FirebaseUI Auth
-
Add Dependency: Make sure you've added
implementation 'com.firebaseui:firebase-ui-auth:8.0.2'to yourapp/build.gradle. -
Launch the Sign-In Flow:
KOTLINimport com.firebase.ui.auth.AuthUI import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult import com.google.firebase.auth.FirebaseAuth class LoginActivity : AppCompatActivity() { // See: https://developer.android.com/training/basics/intents/result private val signInLauncher = registerForActivityResult( FirebaseAuthUIActivityResultContract() ) { res -> this.onSignInResult(res) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Check if the user is already signed in if (FirebaseAuth.getInstance().currentUser != null) { // User is signed in, launch MainActivity // ... } else { // User is not signed in, launch the FirebaseUI sign-in flow launchSignInFlow() } } private fun launchSignInFlow() { // Choose authentication providers val providers = arrayListOf( AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build(), AuthUI.IdpConfig.PhoneBuilder().build() ) // Create and launch sign-in intent val signInIntent = AuthUI.getInstance() .createSignInIntentBuilder() .setAvailableProviders(providers) .setLogo(R.drawable.my_app_logo) // Set your app's logo .setTheme(R.style.MyAuthTheme) // Set a custom theme .build() signInLauncher.launch(signInIntent) } private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) { val response = result.idpResponse if (result.resultCode == RESULT_OK) { // Successfully signed in val user = FirebaseAuth.getInstance().currentUser Log.d("AUTH", "Sign-in successful for user: ${user?.displayName}") // Navigate to MainActivity } else { // Sign in failed. If response is null the user canceled the // sign-in flow using the back button. if (response == null) { Toast.makeText(this, "Sign-in cancelled", Toast.LENGTH_SHORT).show() } else { Log.e("AUTH", "Sign-in error: ${response.error?.errorCode}") } } } }
6. Real-time Database: Configure and Setup
The Firebase Real-time Database is a cloud-hosted NoSQL database. Data is stored as a single large JSON tree and is synchronized in real-time to every connected client.
Data Structure
- NoSQL: There are no tables or rows. All data is stored in a JSON tree structure.
- Nodes: The JSON tree consists of nodes. Each node has a key and can have child nodes or a value (string, number, boolean).
- Denormalization: Unlike SQL databases, data in the Real-time Database is often denormalized (duplicated) to optimize for fast read operations. You structure your data based on how your app will read it.
Example Data Structure:
{
"users": {
"user_id_1": {
"username": "alice",
"email": "alice@example.com",
"profile_picture": "url_to_image.jpg"
},
"user_id_2": {
"username": "bob",
"email": "bob@example.com",
"profile_picture": "url_to_image2.jpg"
}
},
"posts": {
"post_id_1": {
"author_uid": "user_id_1",
"author_name": "alice",
"title": "My First Post",
"body": "Hello, Firebase!",
"timestamp": 1678886400
},
"post_id_2": {
"author_uid": "user_id_2",
"author_name": "bob",
"title": "Real-time is cool",
"body": "This updates instantly.",
"timestamp": 1678887000
}
}
}
Setup
- Add Dependency: Ensure
implementation 'com.google.firebase:firebase-database-ktx'is in yourapp/build.gradle. - Create Database in Console:
- Go to Firebase Console -> Real-time Database -> Create database.
- Choose a server location.
- Start in Test Mode for initial development. This allows anyone to read/write to your database. IMPORTANT: You must secure your database with security rules before launching your app.
- Click Enable.
Security Rules
Security rules define who has read and write access to your data and how it's structured.
- Go to the Rules tab in the Real-time Database section of the console.
- Example: Allow only authenticated users to read/write their own data.
JSON{ "rules": { "users": { "$uid": { // Only the user with the matching UID can read/write their own data ".read": "$uid === auth.uid", ".write": "$uid === auth.uid" } }, "posts": { // Any authenticated user can read all posts ".read": "auth != null", // Users can only write new posts if they are authenticated ".write": "auth != null" } } }
7. CRUD Operations (Create, Read, Update, Delete)
All operations are performed on a DatabaseReference, which points to a specific location (node) in your JSON tree.
Get Database Reference
First, get an instance of the database and a reference to the node you want to work with.
// Get a reference to the root of the database
val database = FirebaseDatabase.getInstance()
// Get a reference to a specific node, e.g., "users"
val usersRef = database.getReference("users")
Create (Write/Set Data)
setValue(): Writes or replaces data at a specified path.push(): Generates a unique, timestamp-based key (likepost_id_1in the example) and returns a newDatabaseReferenceto that location. This is the preferred method for adding items to a list of data.
Example: Creating a new user after sign-up
data class User(val username: String? = null, val email: String? = null)
fun writeNewUser(userId: String, name: String, email: String) {
val user = User(name, email)
// usersRef.child(userId) points to "users/<the_user's_id>"
usersRef.child(userId).setValue(user)
.addOnSuccessListener {
// Write was successful!
}
.addOnFailureListener {
// Write failed
}
}
Example: Adding a new post to a list
val postsRef = database.getReference("posts")
// Use a data class to represent a Post object
data class Post(
val author_uid: String? = null,
val title: String? = null,
val body: String? = null
)
fun addNewPost(userId: String, title: String, body: String) {
// Create new post at /posts/<post-id>
val newPostRef = postsRef.push()
val post = Post(userId, title, body)
newPostRef.setValue(post)
}
Read (Listen for Data)
Firebase is real-time. Instead of a one-time get(), you typically attach a listener that fires once with the initial data and again anytime that data changes.
ValueEventListener: Reads the entire contents of a path. Fires every time the data at that location changes, including children.ChildEventListener: Listens for changes to the children of a specific node. More granular, with callbacks foronChildAdded,onChildChanged,onChildRemoved, etc. Best for lists.
Example: Reading a single user's profile
val userProfileRef = usersRef.child("user_id_1")
val userListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// Get User object and use the values to update the UI
val user = dataSnapshot.getValue(User::class.java)
// ... update your UI with user.username, user.email
}
override fun onCancelled(databaseError: DatabaseError) {
// Getting Post failed, log a message
Log.w("DB_READ", "loadUser:onCancelled", databaseError.toException())
}
}
userProfileRef.addValueEventListener(userListener)
// To stop listening, e.g., in onDestroy()
// userProfileRef.removeEventListener(userListener)
Update
updateChildren(): Updates specific children at a location without overwriting other children.
Example: Updating a user's username
fun updateUserUsername(userId: String, newUsername: String) {
val userUpdates = mapOf<String, Any>(
"/users/$userId/username" to newUsername,
// You can also update related denormalized data in the same operation
// "/posts/post_id_1/author_name" to newUsername
)
database.reference.updateChildren(userUpdates)
}
Delete
removeValue(): Deletes all data at a specifiedDatabaseReference.
Example: Deleting a post
fun deletePost(postId: String) {
database.getReference("posts").child(postId).removeValue()
.addOnSuccessListener {
// Deletion successful
}
.addOnFailureListener {
// Deletion failed
}
}
8. Firebase Notification (Cloud Messaging - FCM)
FCM allows you to send push notifications to your users to re-engage them or deliver important information.
Setup
- Add Dependency: Add
implementation 'com.google.firebase:firebase-messaging-ktx'to yourapp/build.gradle. - Declare the Service in
AndroidManifest.xml:
You need a service that extendsFirebaseMessagingServiceto handle incoming messages.
XML<application ...> ... <service android:name=".MyFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> </application>
Handling Messages
Create a Kotlin class that extends FirebaseMessagingService.
onNewToken(): Called when a new FCM registration token is generated for the device. You should send this token to your application server to be able to send messages to this specific device.onMessageReceived(): Called when the app is in the foreground and receives a message. If the app is in the background or killed, FCM automatically displays a notification in the system tray if the message payload has anotificationobject.
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import android.util.Log
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// This is called when the app is in the foreground.
Log.d("FCM", "From: ${remoteMessage.from}")
// Check if message contains a data payload.
remoteMessage.data.isNotEmpty().let {
Log.d("FCM", "Message data payload: " + remoteMessage.data)
}
// Check if message contains a notification payload.
remoteMessage.notification?.let {
Log.d("FCM", "Message Notification Body: ${it.body}")
// Here you can create a custom notification if you want to handle it
// while the app is in the foreground.
}
}
override fun onNewToken(token: String) {
Log.d("FCM", "Refreshed token: $token")
// If you want to send messages to this application instance or
// manage subscriptions on the server side, send the
// FCM registration token to your app server.
sendRegistrationToServer(token)
}
private fun sendRegistrationToServer(token: String?) {
// TODO: Implement this method to send token to your app server.
}
}
Sending Notifications
From the Firebase Console (for testing/marketing)
- Go to Firebase Console -> Engage -> Cloud Messaging.
- Click "Send your first message" or "New notification".
- Fill in the notification title and text.
- Select your app as the target.
- You can target all users, a specific user segment, or a single device using its FCM token.
- Click "Review" and "Publish".
From a Backend Server (for transactional notifications)
For programmatic sending (e.g., notifying a user about a new message), you must use the Firebase Admin SDK on a trusted server environment (like Cloud Functions or your own server). You cannot send messages from the Android client itself.
Example using Node.js Admin SDK:
// This code runs on a server, NOT in the Android app.
const admin = require('firebase-admin');
// The registration token of the device you want to send to
const registrationToken = 'DEVICE_FCM_TOKEN_HERE';
const message = {
notification: {
title: 'New Follower!',
body: 'Bob just started following you.'
},
token: registrationToken
};
// Send a message to the device corresponding to the provided registration token.
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message:', response);
})
.catch((error) => {
console.log('Error sending message:', error);
});