Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Sunshine And March Vibes (2025 Wallpapers Edition)

      June 2, 2025

      The Case For Minimal WordPress Setups: A Contrarian View On Theme Frameworks

      June 2, 2025

      How To Fix Largest Contentful Paint Issues With Subpart Analysis

      June 2, 2025

      How To Prevent WordPress SQL Injection Attacks

      June 2, 2025

      How Red Hat just quietly, radically transformed enterprise server Linux

      June 2, 2025

      OpenAI wants ChatGPT to be your ‘super assistant’ – what that means

      June 2, 2025

      The best Linux VPNs of 2025: Expert tested and reviewed

      June 2, 2025

      One of my favorite gaming PCs is 60% off right now

      June 2, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      `document.currentScript` is more useful than I thought.

      June 2, 2025
      Recent

      `document.currentScript` is more useful than I thought.

      June 2, 2025

      Adobe Sensei and GenAI in Practice for Enterprise CMS

      June 2, 2025

      Over The Air Updates for React Native Apps

      June 2, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      You can now open ChatGPT on Windows 11 with Win+C (if you change the Settings)

      June 2, 2025
      Recent

      You can now open ChatGPT on Windows 11 with Win+C (if you change the Settings)

      June 2, 2025

      Microsoft says Copilot can use location to change Outlook’s UI on Android

      June 2, 2025

      TempoMail — Command Line Temporary Email in Linux

      June 2, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»Android Development Codelab: Mastering Advanced Concepts

    Android Development Codelab: Mastering Advanced Concepts

    April 10, 2025
    Android Development Codelab: Mastering Advanced Concepts

     

    This guide will walk you through building a small application step-by-step, focusing on integrating several powerful tools and concepts essential for modern Android development.

    What We’ll Cover:

    • Jetpack Compose: Building the UI declaratively.
    • NoSQL Database (Firestore): Storing and retrieving data in the cloud.
    • WorkManager: Running reliable background tasks.
    • Build Flavors: Creating different versions of the app (e.g., dev vs. prod).
    • Proguard/R8: Shrinking and obfuscating your code for release.
    • Firebase App Distribution: Distributing test builds easily.
    • CI/CD (GitHub Actions): Automating the build and distribution process.

    The Goal: Build a “Task Reporter” app. Users can add simple task descriptions. These tasks are saved to Firestore. A background worker will periodically “report” (log a message or update a counter in Firestore) that the app is active. We’ll have dev and prod flavors pointing to different Firestore collections/data and distribute the dev build for testing.

    Prerequisites:

    • Android Studio (latest stable version recommended).
    • Basic understanding of Kotlin and Android development fundamentals.
    • Familiarity with Jetpack Compose basics (Composable functions, State).
    • A Google account to use Firebase.
    • A GitHub account (for CI/CD).

    Let’s get started!


    Step 0: Project Setup

    1. Create New Project: Open Android Studio -> New Project -> Empty Activity (choose Compose).
    2. Name: AdvancedConceptsApp (or your choice).
    3. Package Name: Your preferred package name (e.g., com.yourcompany.advancedconceptsapp).
    4. Language: Kotlin.
    5. Minimum SDK: API 24 or higher.
    6. Build Configuration Language: Kotlin DSL (build.gradle.kts).
    7. Click Finish.

    Step 1: Firebase Integration (Firestore & App Distribution)

    1. Connect to Firebase: In Android Studio: Tools -> Firebase.
      • In the Assistant panel, find Firestore. Click “Get Started with Cloud Firestore”. Click “Connect to Firebase”. Follow the prompts to create a new Firebase project or connect to an existing one.
      • Click “Add Cloud Firestore to your app”. Accept changes to your build.gradle.kts (or build.gradle) files. This adds the necessary dependencies.
      • Go back to the Firebase Assistant, find App Distribution. Click “Get Started”. Add the App Distribution Gradle plugin by clicking the button. Accept changes.
    2. Enable Services in Firebase Console:
      • Go to the Firebase Console and select your project.
      • Enable Firestore Database (start in Test mode).
      • In the left menu, go to Build -> Firestore Database. Click “Create database”.
        • Start in Test mode for easier initial development (we’ll secure it later if needed). Choose a location close to your users. Click “Enable”.
      • Ensure App Distribution is accessible (no setup needed here yet).
    3. Download Initial google-services.json:
      • In Firebase Console -> Project Settings (gear icon) -> Your apps.
      • Ensure your Android app (using the base package name like com.yourcompany.advancedconceptsapp) is registered. If not, add it.
      • Download the google-services.json file.
      • Switch Android Studio to the Project view and place the file inside the app/ directory.
      • Note: We will likely replace this file in Step 4 after configuring build flavors.

    Step 2: Building the Basic UI with Compose

    Let’s create a simple UI to add and display tasks.

    1. Dependencies: Ensure necessary dependencies for Compose, ViewModel, Firestore, and WorkManager are in app/build.gradle.kts.
      app/build.gradle.kts
      
      dependencies {
          // Core & Lifecycle & Activity
          implementation("androidx.core:core-ktx:1.13.1") // Use latest versions
          implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
          implementation("androidx.activity:activity-compose:1.9.0")
          // Compose
          implementation(platform("androidx.compose:compose-bom:2024.04.01")) // Check latest BOM
          implementation("androidx.compose.ui:ui")
          implementation("androidx.compose.ui:ui-graphics")
          implementation("androidx.compose.ui:ui-tooling-preview")
          implementation("androidx.compose.material3:material3")
          implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1")
          // Firebase
          implementation(platform("com.google.firebase:firebase-bom:33.0.0")) // Check latest BOM
          implementation("com.google.firebase:firebase-firestore-ktx")
          // WorkManager
          implementation("androidx.work:work-runtime-ktx:2.9.0") // Check latest version
      }
                      

      Sync Gradle files.

    2. Task Data Class: Create data/Task.kt.
      data/Task.kt
      
      package com.yourcompany.advancedconceptsapp.data
      
      import com.google.firebase.firestore.DocumentId
      
      data class Task(
          @DocumentId
          val id: String = "",
          val description: String = "",
          val timestamp: Long = System.currentTimeMillis()
      ) {
          constructor() : this("", "", 0L) // Firestore requires a no-arg constructor
      }
                      
    3. ViewModel: Create ui/TaskViewModel.kt. (We’ll update the collection name later).
      ui/TaskViewModel.kt
      
      package com.yourcompany.advancedconceptsapp.ui
      
      import androidx.lifecycle.ViewModel
      import androidx.lifecycle.viewModelScope
      import com.google.firebase.firestore.ktx.firestore
      import com.google.firebase.firestore.ktx.toObjects
      import com.google.firebase.ktx.Firebase
      import com.yourcompany.advancedconceptsapp.data.Task
      // Import BuildConfig later when needed
      import kotlinx.coroutines.flow.MutableStateFlow
      import kotlinx.coroutines.flow.StateFlow
      import kotlinx.coroutines.launch
      import kotlinx.coroutines.tasks.await
      
      // Temporary placeholder - will be replaced by BuildConfig field
      const val TEMPORARY_TASKS_COLLECTION = "tasks"
      
      class TaskViewModel : ViewModel() {
          private val db = Firebase.firestore
          // Use temporary constant for now
          private val tasksCollection = db.collection(TEMPORARY_TASKS_COLLECTION)
      
          private val _tasks = MutableStateFlow<List<Task>>(emptyList())
          val tasks: StateFlow<List<Task>> = _tasks
      
          private val _error = MutableStateFlow<String?>(null)
          val error: StateFlow<String?> = _error
      
          init {
              loadTasks()
          }
      
          fun loadTasks() {
              viewModelScope.launch {
                  try {
                       tasksCollection.orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING)
                          .addSnapshotListener { snapshots, e ->
                              if (e != null) {
                                  _error.value = "Error listening: ${e.localizedMessage}"
                                  return@addSnapshotListener
                              }
                              _tasks.value = snapshots?.toObjects<Task>() ?: emptyList()
                              _error.value = null
                          }
                  } catch (e: Exception) {
                      _error.value = "Error loading: ${e.localizedMessage}"
                  }
              }
          }
      
           fun addTask(description: String) {
              if (description.isBlank()) {
                  _error.value = "Task description cannot be empty."
                  return
              }
              viewModelScope.launch {
                   try {
                       val task = Task(description = description, timestamp = System.currentTimeMillis())
                       tasksCollection.add(task).await()
                       _error.value = null
                   } catch (e: Exception) {
                      _error.value = "Error adding: ${e.localizedMessage}"
                  }
              }
          }
      }
                      
    4. Main Screen Composable: Create ui/TaskScreen.kt.
      ui/TaskScreen.kt
      
      package com.yourcompany.advancedconceptsapp.ui
      
      // Imports: androidx.compose.*, androidx.lifecycle.viewmodel.compose.viewModel, java.text.SimpleDateFormat, etc.
      import androidx.compose.foundation.layout.*
      import androidx.compose.foundation.lazy.LazyColumn
      import androidx.compose.foundation.lazy.items
      import androidx.compose.material3.*
      import androidx.compose.runtime.*
      import androidx.compose.ui.Alignment
      import androidx.compose.ui.Modifier
      import androidx.compose.ui.unit.dp
      import androidx.lifecycle.viewmodel.compose.viewModel
      import com.yourcompany.advancedconceptsapp.data.Task
      import java.text.SimpleDateFormat
      import java.util.Date
      import java.util.Locale
      import androidx.compose.ui.res.stringResource
      import com.yourcompany.advancedconceptsapp.R // Import R class
      
      @OptIn(ExperimentalMaterial3Api::class) // For TopAppBar
      @Composable
      fun TaskScreen(taskViewModel: TaskViewModel = viewModel()) {
          val tasks by taskViewModel.tasks.collectAsState()
          val errorMessage by taskViewModel.error.collectAsState()
          var taskDescription by remember { mutableStateOf("") }
      
          Scaffold(
              topBar = {
                  TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }) // Use resource for flavor changes
              }
          ) { paddingValues ->
              Column(modifier = Modifier.padding(paddingValues).padding(16.dp).fillMaxSize()) {
                  // Input Row
                  Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
                      OutlinedTextField(
                          value = taskDescription,
                          onValueChange = { taskDescription = it },
                          label = { Text("New Task Description") },
                          modifier = Modifier.weight(1f),
                          singleLine = true
                      )
                      Spacer(modifier = Modifier.width(8.dp))
                      Button(onClick = {
                          taskViewModel.addTask(taskDescription)
                          taskDescription = ""
                      }) { Text("Add") }
                  }
                  Spacer(modifier = Modifier.height(16.dp))
                  // Error Message
                  errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp)) }
                  // Task List
                  if (tasks.isEmpty() && errorMessage == null) {
                      Text("No tasks yet. Add one!")
                  } else {
                      LazyColumn(modifier = Modifier.weight(1f)) {
                          items(tasks, key = { it.id }) { task ->
                              TaskItem(task)
                              Divider()
                          }
                      }
                  }
              }
          }
      }
      
      @Composable
      fun TaskItem(task: Task) {
          val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) }
          Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
              Column(modifier = Modifier.weight(1f)) {
                  Text(task.description, style = MaterialTheme.typography.bodyLarge)
                  Text("Added: ${dateFormat.format(Date(task.timestamp))}", style = MaterialTheme.typography.bodySmall)
              }
          }
      }
                      
    5. Update MainActivity.kt: Set the content to TaskScreen.
      MainActivity.kt
      
      package com.yourcompany.advancedconceptsapp
      
      import android.os.Bundle
      import androidx.activity.ComponentActivity
      import androidx.activity.compose.setContent
      import androidx.compose.foundation.layout.fillMaxSize
      import androidx.compose.material3.MaterialTheme
      import androidx.compose.material3.Surface
      import androidx.compose.ui.Modifier
      import com.yourcompany.advancedconceptsapp.ui.TaskScreen
      import com.yourcompany.advancedconceptsapp.ui.theme.AdvancedConceptsAppTheme
      // Imports for WorkManager scheduling will be added in Step 3
      
      class MainActivity : ComponentActivity() {
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContent {
                  AdvancedConceptsAppTheme {
                      Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                          TaskScreen()
                      }
                  }
              }
              // TODO: Schedule WorkManager job in Step 3
          }
      }
                      
    6. Run the App: Test basic functionality. Tasks should appear and persist in Firestore’s `tasks` collection (initially).

    Step 3: WorkManager Implementation

    Create a background worker for periodic reporting.

    1. Create the Worker: Create worker/ReportingWorker.kt. (Collection name will be updated later).
      worker/ReportingWorker.kt
      
      package com.yourcompany.advancedconceptsapp.worker
      
      import android.content.Context
      import android.util.Log
      import androidx.work.CoroutineWorker
      import androidx.work.WorkerParameters
      import com.google.firebase.firestore.ktx.firestore
      import com.google.firebase.ktx.Firebase
      // Import BuildConfig later when needed
      import kotlinx.coroutines.tasks.await
      
      // Temporary placeholder - will be replaced by BuildConfig field
      const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs"
      
      class ReportingWorker(appContext: Context, workerParams: WorkerParameters) :
          CoroutineWorker(appContext, workerParams) {
      
          companion object { const val TAG = "ReportingWorker" }
          private val db = Firebase.firestore
      
          override suspend fun doWork(): Result {
              Log.d(TAG, "Worker started: Reporting usage.")
              return try {
                  val logEntry = hashMapOf(
                      "timestamp" to System.currentTimeMillis(),
                      "message" to "App usage report.",
                      "worker_run_id" to id.toString()
                  )
                  // Use temporary constant for now
                  db.collection(TEMPORARY_USAGE_LOG_COLLECTION).add(logEntry).await()
                  Log.d(TAG, "Worker finished successfully.")
                  Result.success()
              } catch (e: Exception) {
                  Log.e(TAG, "Worker failed", e)
                  Result.failure()
              }
          }
      }
                      
    2. Schedule the Worker: Update MainActivity.kt‘s onCreate method.
      MainActivity.kt additions
      
      // Add these imports to MainActivity.kt
      import android.content.Context
      import android.util.Log
      import androidx.work.*
      import com.yourcompany.advancedconceptsapp.worker.ReportingWorker
      import java.util.concurrent.TimeUnit
      
      // Inside MainActivity class, after setContent { ... } block in onCreate
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContent {
              // ... existing code ...
          }
          // Schedule the worker
          schedulePeriodicUsageReport(this)
      }
      
      // Add this function to MainActivity class
      private fun schedulePeriodicUsageReport(context: Context) {
          val constraints = Constraints.Builder()
              .setRequiredNetworkType(NetworkType.CONNECTED)
              .build()
      
          val reportingWorkRequest = PeriodicWorkRequestBuilder<ReportingWorker>(
                  1, TimeUnit.HOURS // ~ every hour
               )
              .setConstraints(constraints)
              .addTag(ReportingWorker.TAG)
              .build()
      
          WorkManager.getInstance(context).enqueueUniquePeriodicWork(
              ReportingWorker.TAG,
              ExistingPeriodicWorkPolicy.KEEP,
              reportingWorkRequest
          )
          Log.d("MainActivity", "Periodic reporting work scheduled.")
      }
                      
    3. Test WorkManager:
      • Run the app. Check Logcat for messages from ReportingWorker and MainActivity about scheduling.
      • WorkManager tasks don’t run immediately, especially periodic ones. You can use ADB commands to force execution for testing:
        • Find your package name: com.yourcompany.advancedconceptsapp
        • Force run jobs: adb shell cmd jobscheduler run -f com.yourcompany.advancedconceptsapp 999 (The 999 is usually sufficient, it’s a job ID).
        • Or use Android Studio’s App Inspection tab -> Background Task Inspector to view and trigger workers.
      • Check your Firestore Console for the usage_logs collection.

    Step 4: Build Flavors (dev vs. prod)

    Create dev and prod flavors for different environments.

    1. Configure app/build.gradle.kts:
      app/build.gradle.kts
      
      android {
          // ... namespace, compileSdk, defaultConfig ...
      
          // ****** Enable BuildConfig generation ******
          buildFeatures {
              buildConfig = true
          }
          // *******************************************
      
          flavorDimensions += "environment"
      
          productFlavors {
              create("dev") {
                  dimension = "environment"
                  applicationIdSuffix = ".dev" // CRITICAL: Changes package name for dev builds
                  versionNameSuffix = "-dev"
                  resValue("string", "app_name", "Task Reporter (Dev)")
                  buildConfigField("String", "TASKS_COLLECTION", ""tasks_dev"")
                  buildConfigField("String", "USAGE_LOG_COLLECTION", ""usage_logs_dev"")
              }
              create("prod") {
                  dimension = "environment"
                  resValue("string", "app_name", "Task Reporter")
                  buildConfigField("String", "TASKS_COLLECTION", ""tasks"")
                  buildConfigField("String", "USAGE_LOG_COLLECTION", ""usage_logs"")
              }
          }
      
          // ... buildTypes, compileOptions, etc ...
      }
                      

      Sync Gradle files.

      Important: We added applicationIdSuffix = ".dev". This means the actual package name for your development builds will become something like com.yourcompany.advancedconceptsapp.dev. This requires an update to your Firebase project setup, explained next. Also note the buildFeatures { buildConfig = true } block which is required to use buildConfigField.
    2. Handling Firebase for Suffixed Application IDs

      Because the `dev` flavor now has a different application ID (`…advancedconceptsapp.dev`), the original `google-services.json` file (downloaded in Step 1) will not work for `dev` builds, causing a “No matching client found” error during build.

      You must add this new Application ID to your Firebase project:

      1. Go to Firebase Console: Open your project settings (gear icon).
      2. Your apps: Scroll down to the “Your apps” card.
      3. Add app: Click “Add app” and select the Android icon (</>).
      4. Register dev app:
        • Package name: Enter the exact suffixed ID: com.yourcompany.advancedconceptsapp.dev (replace `com.yourcompany.advancedconceptsapp` with your actual base package name).
        • Nickname (Optional): “Task Reporter Dev”.
        • SHA-1 (Optional but Recommended): Add the debug SHA-1 key from `./gradlew signingReport`.
      5. Register and Download: Click “Register app”. Crucially, download the new google-services.json file offered. This file now contains configurations for BOTH your base ID and the `.dev` suffixed ID.
      6. Replace File: In Android Studio (Project view), delete the old google-services.json from the app/ directory and replace it with the **newly downloaded** one.
      7. Skip SDK steps: You can skip the remaining steps in the Firebase console for adding the SDK.
      8. Clean & Rebuild: Back in Android Studio, perform a Build -> Clean Project and then Build -> Rebuild Project.
      Now your project is correctly configured in Firebase for both `dev` (with the `.dev` suffix) and `prod` (base package name) variants using a single `google-services.json`.
    3. Create Flavor-Specific Source Sets:
      • Switch to Project view in Android Studio.
      • Right-click on app/src -> New -> Directory. Name it dev.
      • Inside dev, create res/values/ directories.
      • Right-click on app/src -> New -> Directory. Name it prod.
      • Inside prod, create res/values/ directories.
      • (Optional but good practice): You can now move the default app_name string definition from app/src/main/res/values/strings.xml into both app/src/dev/res/values/strings.xml and app/src/prod/res/values/strings.xml. Or, you can rely solely on the resValue definitions in Gradle (as done above). Using resValue is often simpler for single strings like app_name. If you had many different resources (layouts, drawables), you’d put them in the respective dev/res or prod/res folders.
    4. Use Build Config Fields in Code:
        • Update TaskViewModel.kt and ReportingWorker.kt to use BuildConfig instead of temporary constants.

      TaskViewModel.kt change

      
      // Add this import
      import com.yourcompany.advancedconceptsapp.BuildConfig
      
      // Replace the temporary constant usage
      // const val TEMPORARY_TASKS_COLLECTION = "tasks" // Remove this line
      private val tasksCollection = db.collection(BuildConfig.TASKS_COLLECTION) // Use build config field
                          

      ReportingWorker.kt change

      
      // Add this import
      import com.yourcompany.advancedconceptsapp.BuildConfig
      
      // Replace the temporary constant usage
      // const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs" // Remove this line
      
      // ... inside doWork() ...
      db.collection(BuildConfig.USAGE_LOG_COLLECTION).add(logEntry).await() // Use build config field
                          

      Modify TaskScreen.kt to potentially use the flavor-specific app name (though resValue handles this automatically if you referenced @string/app_name correctly, which TopAppBar usually does). If you set the title directly, you would load it from resources:

       // In TaskScreen.kt (if needed)
      import androidx.compose.ui.res.stringResource
      import com.yourcompany.advancedconceptsapp.R // Import R class
      // Inside Scaffold -> topBar

      TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }) // Use string resource

    5. Select Build Variant & Test:
      • In Android Studio, go to Build -> Select Build Variant… (or use the “Build Variants” panel usually docked on the left).
      • You can now choose between devDebug, devRelease, prodDebug, and prodRelease.
      • Select devDebug. Run the app. The title should say “Task Reporter (Dev)”. Data should go to tasks_dev and usage_logs_dev in Firestore.
      • Select prodDebug. Run the app. The title should be “Task Reporter”. Data should go to tasks and usage_logs.

    Step 5: Proguard/R8 Configuration (for Release Builds)

    R8 is the default code shrinker and obfuscator in Android Studio (successor to Proguard). It’s enabled by default for release build types. We need to ensure it doesn’t break our app, especially Firestore data mapping.

      1. Review app/build.gradle.kts Release Build Type:
        app/build.gradle.kts
        
        android {
            // ...
            buildTypes {
                release {
                    isMinifyEnabled = true // Should be true by default for release
                    isShrinkResources = true // R8 handles both
                    proguardFiles(
                        getDefaultProguardFile("proguard-android-optimize.txt"),
                        "proguard-rules.pro" // Our custom rules file
                    )
                }
                debug {
                    isMinifyEnabled = false // Usually false for debug
                    proguardFiles(
                        getDefaultProguardFile("proguard-android-optimize.txt"),
                        "proguard-rules.pro"
                    )
                }
                // ... debug build type ...
            }
            // ...
        }
                   

        isMinifyEnabled = true enables R8 for the release build type.

      2. Configure app/proguard-rules.pro:
        • Firestore uses reflection to serialize/deserialize data classes. R8 might remove or rename classes/fields needed for this process. We need to add “keep” rules.
        • Open (or create) the app/proguard-rules.pro file. Add the following:
        
        # Keep Task data class and its members for Firestore serialization
        -keep class com.yourcompany.advancedconceptsapp.data.Task { (...); *; }
        # Keep any other data classes used with Firestore similarly
        # -keep class com.yourcompany.advancedconceptsapp.data.AnotherFirestoreModel { (...); *; }
        
        # Keep Coroutine builders and intrinsics (often needed, though AGP/R8 handle some automatically)
        -keepnames class kotlinx.coroutines.intrinsics.** { *; }
        
        # Keep companion objects for Workers if needed (sometimes R8 removes them)
        -keepclassmembers class * extends androidx.work.Worker {
            public static ** Companion;
        }
        
        # Keep specific fields/methods if using reflection elsewhere
        # -keepclassmembers class com.example.SomeClass {
        #    private java.lang.String someField;
        #    public void someMethod();
        # }
        
        # Add rules for any other libraries that require them (e.g., Retrofit, Gson, etc.)
        # Consult library documentation for necessary Proguard/R8 rules.
      • Explanation:
        • -keep class ... { <init>(...); *; }: Keeps the Task class, its constructors (<init>), and all its fields/methods (*) from being removed or renamed. This is crucial for Firestore.
        • -keepnames: Prevents renaming but allows removal if unused.
        • -keepclassmembers: Keeps specific members within a class.

    3. Test the Release Build:

      • Select the prodRelease build variant.
      • Go to Build -> Generate Signed Bundle / APK…. Choose APK.
      • Create a new keystore or use an existing one (follow the prompts). Remember the passwords!
      • Select prodRelease as the variant. Click Finish.
      • Android Studio will build the release APK. Find it (usually in app/prod/release/).
      • Install this APK manually on a device: adb install app-prod-release.apk.
      • Test thoroughly. Can you add tasks? Do they appear? Does the background worker still log to Firestore (check usage_logs)? If it crashes or data doesn’t save/load correctly, R8 likely removed something important. Check Logcat for errors (often ClassNotFoundException or NoSuchMethodError) and adjust your proguard-rules.pro file accordingly.

     


     

    Step 6: Firebase App Distribution (for Dev Builds)

    Configure Gradle to upload development builds to testers via Firebase App Distribution.

    1. Download private key: on Firebase console go to Project Overview  at left top corner -> Service accounts -> Firebase Admin SDK -> Click on “Generate new private key” button ->
      api-project-xxx-yyy.json move this file to root project at the same level of app folder *Ensure that this file be in your local app, do not push it to the remote repository because it contains sensible data and will be rejected later
    2. Configure App Distribution Plugin in app/build.gradle.kts:
      app/build.gradle.kts
      
      // Apply the plugin at the top
      plugins {
          // ... other plugins id("com.android.application"), id("kotlin-android"), etc.
          alias(libs.plugins.google.firebase.appdistribution)
      }
      
      android {
          // ... buildFeatures, flavorDimensions, productFlavors ...
      
          buildTypes {
              getByName("release") {
                  isMinifyEnabled = true // Should be true by default for release
                  isShrinkResources = true // R8 handles both
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro" // Our custom rules file
                  )
              }
              getByName("debug") {
                  isMinifyEnabled = false // Usually false for debug
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro"
                  )
              }
              firebaseAppDistribution {
                  artifactType = "APK"
                  releaseNotes = "Latest build with fixes/features"
                  testers = "briew@example.com, bri@example.com, cal@example.com"
                  serviceCredentialsFile="$rootDir/api-project-xxx-yyy.json"//do not push this line to the remote repository or stablish as local variable } } } 

      Add library version to libs.version.toml

      
      [versions]
      googleFirebaseAppdistribution = "5.1.1"
      [plugins]
      google-firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "googleFirebaseAppdistribution" }
      
      Ensure the plugin classpath is in the 

      project-level

       build.gradle.kts: 

      project build.gradle.kts

      
      plugins {
          // ...
          alias(libs.plugins.google.firebase.appdistribution) apply false
      }
                      

      Sync Gradle files.

    3. Upload a Build Manually:
      • Select the desired variant (e.g., devDebug , devRelease, prodDebug , prodRelease).
      • In Android Studio Terminal  run  each commmand to generate apk version for each environment:
        • ./gradlew assembleRelease appDistributionUploadProdRelease
        • ./gradlew assembleRelease appDistributionUploadDevRelease
        • ./gradlew assembleDebug appDistributionUploadProdDebug
        • ./gradlew assembleDebug appDistributionUploadDevDebug
      • Check Firebase Console -> App Distribution -> Select .dev project . Add testers or use the configured group (`android-testers`).

    Step 7: CI/CD with GitHub Actions

    Automate building and distributing the `dev` build on push to a specific branch.

    1. Create GitHub Repository. Create a new repository on GitHub and push your project code to it.
      1. Generate FIREBASE_APP_ID:
        • on Firebase App Distribution go to Project Overview -> General -> App ID for com.yourcompany.advancedconceptsapp.dev environment (1:xxxxxxxxx:android:yyyyyyyyyy)
        • In GitHub repository go to Settings -> Secrets and variables -> Actions -> New repository secret
        • Set the name: FIREBASE_APP_ID and value: paste the App ID generated
      2. Add FIREBASE_SERVICE_ACCOUNT_KEY_JSON:
        • open api-project-xxx-yyy.json located at root project and copy the content
        • In GitHub repository go to Settings -> Secrets and variables -> Actions -> New repository secret
        • Set the name: FIREBASE_SERVICE_ACCOUNT_KEY_JSON and value: paste the json content
      3. Create GitHub Actions Workflow File:
        • In your project root, create the directories .github/workflows/.
        • Inside .github/workflows/, create a new file named android_build_distribute.yml.
        • Paste the following content:
      4. 
        name: Android CI 
        
        on: 
          push: 
            branches: [ "main" ] 
          pull_request: 
            branches: [ "main" ] 
        jobs: 
          build: 
            runs-on: ubuntu-latest 
            steps: 
            - uses: actions/checkout@v3
            - name: set up JDK 17 
              uses: actions/setup-java@v3 
              with: 
                java-version: '17' 
                distribution: 'temurin' 
                cache: gradle 
            - name: Grant execute permission for gradlew 
              run: chmod +x ./gradlew 
            - name: Build devRelease APK 
              run: ./gradlew assembleRelease 
            - name: upload artifact to Firebase App Distribution
              uses: wzieba/Firebase-Distribution-Github-Action@v1
              with:
                appId: ${{ secrets.FIREBASE_APP_ID }}
                serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY_JSON }}
                groups: testers
                file: app/build/outputs/apk/dev/release/app-dev-release-unsigned.apk
        
      1. Commit and Push: Commit the .github/workflows/android_build_distribute.yml file and push it to your main branch on GitHub.
      1. Verify: Go to the “Actions” tab in your GitHub repository. You should see the workflow running. If it succeeds, check Firebase App Distribution for the new build. Your testers should get notified.

     


     

    Step 8: Testing and Verification Summary

      • Flavors: Switch between devDebug and prodDebug in Android Studio. Verify the app name changes and data goes to the correct Firestore collections (tasks_dev/tasks, usage_logs_dev/usage_logs).
      • WorkManager: Use the App Inspection -> Background Task Inspector or ADB commands to verify the ReportingWorker runs periodically and logs data to the correct Firestore collection based on the selected flavor.
      • R8/Proguard: Install and test the prodRelease APK manually. Ensure all features work, especially adding/viewing tasks (Firestore interaction). Check Logcat for crashes related to missing classes/methods.
      • App Distribution: Make sure testers receive invites for the devDebug (or devRelease) builds uploaded manually or via CI/CD. Ensure they can install and run the app.
      • CI/CD: Check the GitHub Actions logs for successful builds and uploads after pushing to the develop branch. Verify the build appears in Firebase App Distribution.

     

    Conclusion

    Congratulations! You’ve navigated complex Android topics including Firestore, WorkManager, Compose, Flavors (with correct Firebase setup), R8, App Distribution, and CI/CD.

    This project provides a solid foundation. From here, you can explore:

      • More complex WorkManager chains or constraints.
      • Deeper R8/Proguard rule optimization.
      • More sophisticated CI/CD pipelines (deploy signed apks/bundles, running tests, deploying to Google Play).
      • Using different NoSQL databases or local caching with Room.
      • Advanced Compose UI patterns and state management.
      • Firebase Authentication, Cloud Functions, etc.

    If you want to have access to the full code in my GitHub repository, contact me in the comments.


     

    Project Folder Structure (Conceptual)

    
    AdvancedConceptsApp/
    ├── .git/
    ├── .github/workflows/android_build_distribute.yml
    ├── .gradle/
    ├── app/
    │   ├── build/
    │   ├── libs/
    │   ├── src/
    │   │   ├── main/           # Common code, res, AndroidManifest.xml
    │   │   │   └── java/com/yourcompany/advancedconceptsapp/
    │   │   │       ├── data/Task.kt
    │   │   │       ├── ui/TaskScreen.kt, TaskViewModel.kt, theme/
    │   │   │       ├── worker/ReportingWorker.kt
    │   │   │       └── MainActivity.kt
    │   │   ├── dev/            # Dev flavor source set (optional overrides)
    │   │   ├── prod/           # Prod flavor source set (optional overrides)
    │   │   ├── test/           # Unit tests
    │   │   └── androidTest/    # Instrumentation tests
    │   ├── google-services.json # *** IMPORTANT: Contains configs for BOTH package names ***
    │   ├── build.gradle.kts    # App-level build script
    │   └── proguard-rules.pro # R8/Proguard rules
    ├── api-project-xxx-yyy.json # Firebase service account key json
    ├── gradle/wrapper/
    ├── build.gradle.kts      # Project-level build script
    ├── gradle.properties
    ├── gradlew
    ├── gradlew.bat
    └── settings.gradle.kts
            

     

    Source: Read More 

    Hostinger
    Facebook Twitter Reddit Email Copy Link
    Previous ArticlePersonalized Optimizely CMS Website Search Experiences Azure AI Search & Personalizer
    Next Article @lib/sixel – Bitmap graphics in the terminal

    Related Posts

    Security

    Chrome Zero-Day Alert: CVE-2025-5419 Actively Exploited in the Wild

    June 2, 2025
    Security

    CISA Adds 5 Actively Exploited Vulnerabilities to KEV Catalog: ASUS Routers, Craft CMS, and ConnectWise Targeted

    June 2, 2025
    Leave A Reply Cancel Reply

    Continue Reading

    Here are some new juicy details about Lego’s Nintendo Game Boy while we’re waiting for Switch 2

    Operating Systems

    How the Amazon TimeHub team designed a recovery and validation framework for their data replication framework: Part 4

    Databases

    CVE-2025-23165 – Node.js ReadFileUtf8 Memory Leak Denial of Service

    Common Vulnerabilities and Exposures (CVEs)

    TorrentLocker: Racketeering ransomware disassembled by ESET experts

    Development

    Highlights

    Development

    CISA’s Latest Advisories Expose High-Risk Vulnerabilities in Industrial Control Systems

    April 3, 2025

    The Cybersecurity and Infrastructure Security Agency (CISA) issued two crucial Industrial Control Systems (ICS) advisories,…

    CVE-2025-4146 – Netgear EX6200 Remote Buffer Overflow Vulnerability

    May 1, 2025

    Step-by-Step Guide: Converting a Normal User to a Partner User in Salesforce

    June 26, 2024

    Meet Tsinghua University’s GLM-4-9B-Chat-1M: An Outstanding Language Model Challenging GPT 4V, Gemini Pro (on vision), Mistral and Llama 3 8B

    June 6, 2024
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.