How I Built GiveEase: A Verified Donation Platform Using Kotlin & Firebase

๐Ÿ“… Published: July 19, 2026 ยท โšก Last Updated: July 30, 2026 ยท ๐Ÿ‘จโ€๐Ÿ’ป Author: Muhammad Saad

1. How does GiveEase solve the online donation trust gap?

GiveEase is an Android donation application built with Kotlin, Jetpack Compose, and Firebase that connects individual donors directly with verified NGOs to provide transparent crowdsourced relief. In the digital era, crowdsourced charity and online donations have become the primary method for individuals to seek help. However, this ease of fund generation has brought a critical challenge: a trust gap. Donation appeals regularly flood social media platforms like Twitter, Instagram, and WhatsApp, with door-to-door solicitation remaining common. For the average donor, distinguishing between a valid humanitarian emergency and a fraudulent profile is nearly impossible.

To address this gap, I engineered GiveEase, a verified Android donation application. The project serves as a secure bridge connecting donors with trusted, vetted Non-Governmental Organizations (NGOs). GiveEase facilitates transparent contributions of monetary funds, clothes, blood, and food while providing live impact tracking. In this article, I discuss how I designed the MVVM application codebase, set up Firebase cloud infrastructure, implemented multi-role access controls, and solved real-time synchronization challenges.

2. Core Architecture: Android MVVM Guidelines

In building GiveEase, I adhered strictly to Google's official Android architecture recommendations. The codebase is written entirely in Kotlin and is structured around the Model-View-ViewModel (MVVM) pattern. This separation of concerns ensures that business logic remains independent of UI components, leading to a scalable, testable, and robust mobile application.

UI/View Layer

The UI layer is composed of Activities and Fragments that handle user interaction and display data. To preserve responsive rendering, these classes perform no business calculations. They observe state streams exposed by the ViewModel and update the layouts accordingly.

ViewModel Layer

ViewModels act as logic controllers. They survive configuration changes (such as device rotations) by binding to the lifecycle of their parent activity or fragment. ViewModels execute coroutine threads to pull or modify data via repository abstractions and update UI states using `LiveData` or Kotlin `StateFlow`.

Repository / Data Layer

The Repository layer serves as the single source of truth for application data. It manages communication with local database nodes (via Room) and remote servers (via Firebase Services). The presentation layer (ViewModel) has no direct visibility into Firestore collections or network operations, isolating data access protocols.

// Repositories isolate view models from direct Firebase APIs
class DonationRepository @Inject constructor(
    private val firestore: FirebaseFirestore,
    private val storage: FirebaseStorage
) {
    fun createDonationCampaign(campaign: Campaign): Flow<Resource<String>> = flow {
        emit(Resource.Loading())
        try {
            val docRef = firestore.collection("campaigns").document()
            campaign.id = docRef.id
            docRef.set(campaign).await()
            emit(Resource.Success(docRef.id))
        } catch (e: Exception) {
            emit(Resource.Error(e.localizedMessage ?: "Unknown network failure"))
        }
    }.flowOn(Dispatchers.IO)
}

3. Secure Authentication: Firebase Multi-Role Management

One of the most complex requirements of GiveEase is secure authentication and role isolation. The app supports three distinct user types:

  1. Donors: Regular individuals who browse campaigns and donate goods or money. They verify their accounts by uploading scans of government-issued IDs.
  2. NGO Admins: Vetted organizational managers who create donation campaigns, request specific relief goods, and log impact receipts.
  3. Platform Admin: A centralized role tasked with reviewing uploaded credentials, verifying NGO registrations, and approving new campaigns before they go live on the public feed.

Authentication is implemented using Firebase Authentication. When a user creates an account, their credentials are saved in Firebase Auth, and an associated document is created in the `users` Firestore collection to store their role and verification status.

To enforce strict security boundaries, Firestore security rules validate roles server-side, preventing regular donors from modifying NGO listings or approving campaigns:

// Firestore Security Rules for role-based write access
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /campaigns/{campaignId} {
      allow read: if true;
      allow create, update: if request.auth != null && 
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "ngo" &&
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isVerified == true;
      allow delete: if request.auth != null && 
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "admin";
    }
  }
}

4. Database Architecture: Real-time Firestore Database Integration

We selected Cloud Firestore as our primary database because of its native support for real-time document listeners. When an NGO updates a campaign's donation progress (e.g. tracking how many blankets out of a target 500 have been received), other active users see this progress update instantly.

In the Firestore data schema:

  • The `/users` collection stores basic user profiles, roles, and verification states.
  • The `/campaigns` collection stores campaign parameters (title, description, target item counts, images, active status, and NGO reference).
  • The `/donations` collection tracks individual contributions, mapping donor IDs to target campaigns and logging progress status (Pending, Dispatched, Received, Distributed).

This structure allows NGOs to provide direct impact proofs (such as photographs of local distributions) directly to the specific donation record, satisfying the donor's transparency expectations.

5. Cloud Messaging: Real-time Matching via FCM

To keep donors active and alert them of emergency relief efforts, we integrated Firebase Cloud Messaging (FCM). Using Cloud Functions, the system triggers push notifications to users based on local parameters.

For example, if an NGO logs an urgent request for 'O-Negative' blood bags at a clinic in Lahore, the Cloud Function automatically triggers a localized FCM notification to nearby users registered with matching profiles. This notification is processed in the background of the Android app using an `FCMService` class to alert the user instantly:

// Custom FCM service listening for emergency alerts
class GiveEaseFCMService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        remoteMessage.data["type"]?.let { type ->
            if (type == "emergency_alert") {
                val title = remoteMessage.data["title"] ?: "Urgent Donation Request"
                val body = remoteMessage.data["body"] ?: "An NGO near you requires emergency items."
                showNotification(title, body)
            }
        }
    }

    private fun showNotification(title: String, body: String) {
        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
        val channelId = "emergency_donations_channel"

        val builder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)

        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, "Emergency Alerts", NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(channel)
        }
        manager.notify(System.currentTimeMillis().toInt(), builder.build())
    }
}

6. Core UI Optimization: Core Web Vitals & Jetpack Compose Layouts

In addition to optimizing remote data synchronization, we focused heavily on client-side rendering speed. The interface of GiveEase was transitioned to Jetpack Compose, minimizing layout hierarchies and eliminating nested views. This reduction in the view rendering hierarchy resolved scroll stuttering and reduced memory footprints by up to 25%.

We also implemented strict image parameters, storing all campaign photography in WebP format on Cloud Storage and utilizing the `Coil` image loading library to apply disc caching and lazy loadings, preventing Layout Shift (CLS) in feed lists.

7. Conclusion & Key Takeaways

Building GiveEase verified that combining Kotlin's concise language features with Firebase's scalable real-time integrations provides a high-fidelity mobile experience. By establishing strict role authorization structures, Firestore security limits, and reliable background notification queues, the platform addresses the crucial trust gap in digital charity apps.

If you are looking to build a clean Android app or implement robust Firebase backend architectures, please explore my featured projects on the Muhammad Saad portfolio home page, or submit an inquiry directly through the contact form to discuss your mobile application ideas.


References & Resources: