This information will stroll you thru constructing a small software step-by-step, specializing in integrating a number of highly effective instruments and ideas important for contemporary Android improvement.
What We’ll Cowl:
- Jetpack Compose: Constructing the UI declaratively.
- NoSQL Database (Firestore): Storing and retrieving knowledge within the cloud.
- WorkManager: Working dependable background duties.
- Construct Flavors: Creating totally different variations of the app (e.g., dev vs. prod).
- Proguard/R8: Shrinking and obfuscating your code for launch.
- Firebase App Distribution: Distributing check builds simply.
- CI/CD (GitHub Actions): Automating the construct and distribution course of.
The Objective: Construct a “Process Reporter” app. Customers can add easy job descriptions. These duties are saved to Firestore. A background employee will periodically “report” (log a message or replace a counter in Firestore) that the app is energetic. We’ll have dev
and prod
flavors pointing to totally different Firestore collections/knowledge and distribute the dev
construct for testing.
Conditions:
- Android Studio (newest secure model beneficial).
- Fundamental understanding of Kotlin and Android improvement fundamentals.
- Familiarity with Jetpack Compose fundamentals (Composable features, State).
- A Google account to make use of Firebase.
- A GitHub account (for CI/CD).
Let’s get began!
Step 0: Venture Setup
- Create New Venture: Open Android Studio -> New Venture -> Empty Exercise (select Compose).
- Identify:
AdvancedConceptsApp
(or your alternative). - Package deal Identify: Your most popular bundle identify (e.g.,
com.yourcompany.advancedconceptsapp
). - Language: Kotlin.
- Minimal SDK: API 24 or increased.
- Construct Configuration Language: Kotlin DSL (
construct.gradle.kts
). - Click on End.
Step 1: Firebase Integration (Firestore & App Distribution)
- Hook up with Firebase: In Android Studio: Instruments -> Firebase.
- Within the Assistant panel, discover Firestore. Click on “Get Began with Cloud Firestore”. Click on “Hook up with Firebase”. Observe the prompts to create a brand new Firebase challenge or hook up with an present one.
- Click on “Add Cloud Firestore to your app”. Settle for modifications to your
construct.gradle.kts
(orconstruct.gradle
) information. This provides the mandatory dependencies. - Return to the Firebase Assistant, discover App Distribution. Click on “Get Began”. Add the App Distribution Gradle plugin by clicking the button. Settle for modifications.
- Allow Providers in Firebase Console:
- Go to the Firebase Console and choose your challenge.
- Allow Firestore Database (begin in Take a look at mode).
- Within the left menu, go to Construct -> Firestore Database. Click on “Create database”.
- Begin in Take a look at mode for simpler preliminary improvement (we’ll safe it later if wanted). Select a location near your customers. Click on “Allow”.
- Guarantee App Distribution is accessible (no setup wanted right here but).
- Obtain Preliminary
google-services.json
:- In Firebase Console -> Venture Settings (gear icon) -> Your apps.
- Guarantee your Android app (utilizing the bottom bundle identify like
com.yourcompany.advancedconceptsapp
) is registered. If not, add it. - Obtain the
google-services.json
file. - Change Android Studio to the Venture view and place the file contained in the
app/
listing. - Be aware: We’ll seemingly exchange this file in Step 4 after configuring construct flavors.
Step 2: Constructing the Fundamental UI with Compose
Let’s create a easy UI so as to add and show duties.
- Dependencies: Guarantee vital dependencies for Compose, ViewModel, Firestore, and WorkManager are in
app/construct.gradle.kts
.
app/construct.gradle.ktsdependencies { // Core & Lifecycle & Exercise implementation("androidx.core:core-ktx:1.13.1") // Use newest variations implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1") implementation("androidx.exercise:activity-compose:1.9.0") // Compose implementation(platform("androidx.compose:compose-bom:2024.04.01")) // Test newest 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")) // Test newest BOM implementation("com.google.firebase:firebase-firestore-ktx") // WorkManager implementation("androidx.work:work-runtime-ktx:2.9.0") // Test newest model }
Sync Gradle information.
- Process Knowledge Class: Create
knowledge/Process.kt
.
knowledge/Process.ktbundle com.yourcompany.advancedconceptsapp.knowledge import com.google.firebase.firestore.DocumentId knowledge class Process( @DocumentId val id: String = "", val description: String = "", val timestamp: Lengthy = System.currentTimeMillis() ) { constructor() : this("", "", 0L) // Firestore requires a no-arg constructor }
- ViewModel: Create
ui/TaskViewModel.kt
. (We’ll replace the gathering identify later).
ui/TaskViewModel.ktbundle 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.knowledge.Process // Import BuildConfig later when wanted import kotlinx.coroutines.move.MutableStateFlow import kotlinx.coroutines.move.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.duties.await // Short-term placeholder - might be changed by BuildConfig discipline const val TEMPORARY_TASKS_COLLECTION = "duties" class TaskViewModel : ViewModel() { non-public val db = Firebase.firestore // Use short-term fixed for now non-public val tasksCollection = db.assortment(TEMPORARY_TASKS_COLLECTION) non-public val _tasks = MutableStateFlow
- >(emptyList())
val duties: StateFlow
- > = _tasks
non-public val _error = MutableStateFlow
(null) val error: StateFlow = _error init { loadTasks() } enjoyable loadTasks() { viewModelScope.launch { attempt { tasksCollection.orderBy("timestamp", com.google.firebase.firestore.Question.Path.DESCENDING) .addSnapshotListener { snapshots, e -> if (e != null) { _error.worth = "Error listening: ${e.localizedMessage}" return@addSnapshotListener } _tasks.worth = snapshots?.toObjects () ?: emptyList() _error.worth = null } } catch (e: Exception) { _error.worth = "Error loading: ${e.localizedMessage}" } } } enjoyable addTask(description: String) { if (description.isBlank()) { _error.worth = "Process description can't be empty." return } viewModelScope.launch { attempt { val job = Process(description = description, timestamp = System.currentTimeMillis()) tasksCollection.add(job).await() _error.worth = null } catch (e: Exception) { _error.worth = "Error including: ${e.localizedMessage}" } } } } - Major Display screen Composable: Create
ui/TaskScreen.kt
.
ui/TaskScreen.ktbundle com.yourcompany.advancedconceptsapp.ui // Imports: androidx.compose.*, androidx.lifecycle.viewmodel.compose.viewModel, java.textual content.SimpleDateFormat, and so on. import androidx.compose.basis.format.* import androidx.compose.basis.lazy.LazyColumn import androidx.compose.basis.lazy.gadgets 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.knowledge.Process import java.textual content.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 enjoyable TaskScreen(taskViewModel: TaskViewModel = viewModel()) { val duties by taskViewModel.duties.collectAsState() val errorMessage by taskViewModel.error.collectAsState() var taskDescription by bear in mind { mutableStateOf("") } Scaffold( topBar = { TopAppBar(title = { Textual content(stringResource(id = R.string.app_name)) }) // Use useful resource for taste modifications } ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues).padding(16.dp).fillMaxSize()) { // Enter Row Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { OutlinedTextField( worth = taskDescription, onValueChange = { taskDescription = it }, label = { Textual content("New Process Description") }, modifier = Modifier.weight(1f), singleLine = true ) Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { taskViewModel.addTask(taskDescription) taskDescription = "" }) { Textual content("Add") } } Spacer(modifier = Modifier.top(16.dp)) // Error Message errorMessage?.let { Textual content(it, colour = MaterialTheme.colorScheme.error, modifier = Modifier.padding(backside = 8.dp)) } // Process Record if (duties.isEmpty() && errorMessage == null) { Textual content("No duties but. Add one!") } else { LazyColumn(modifier = Modifier.weight(1f)) { gadgets(duties, key = { it.id }) { job -> TaskItem(job) Divider() } } } } } } @Composable enjoyable TaskItem(job: Process) { val dateFormat = bear in mind { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) } Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Textual content(job.description, model = MaterialTheme.typography.bodyLarge) Textual content("Added: ${dateFormat.format(Date(job.timestamp))}", model = MaterialTheme.typography.bodySmall) } } }
- Replace
MainActivity.kt
: Set the content material toTaskScreen
.
MainActivity.ktbundle com.yourcompany.advancedconceptsapp import android.os.Bundle import androidx.exercise.ComponentActivity import androidx.exercise.compose.setContent import androidx.compose.basis.format.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Floor import androidx.compose.ui.Modifier import com.yourcompany.advancedconceptsapp.ui.TaskScreen import com.yourcompany.advancedconceptsapp.ui.theme.AdvancedConceptsAppTheme // Imports for WorkManager scheduling might be added in Step 3 class MainActivity : ComponentActivity() { override enjoyable onCreate(savedInstanceState: Bundle?) { tremendous.onCreate(savedInstanceState) setContent { AdvancedConceptsAppTheme { Floor(modifier = Modifier.fillMaxSize(), colour = MaterialTheme.colorScheme.background) { TaskScreen() } } } // TODO: Schedule WorkManager job in Step 3 } }
- Run the App: Take a look at primary performance. Duties ought to seem and persist in Firestore’s `duties` assortment (initially).
Step 3: WorkManager Implementation
Create a background employee for periodic reporting.
- Create the Employee: Create
employee/ReportingWorker.kt
. (Assortment identify might be up to date later).
employee/ReportingWorker.ktbundle com.yourcompany.advancedconceptsapp.employee import android.content material.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 wanted import kotlinx.coroutines.duties.await // Short-term placeholder - might be changed by BuildConfig discipline const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs" class ReportingWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { companion object { const val TAG = "ReportingWorker" } non-public val db = Firebase.firestore override droop enjoyable doWork(): End result { Log.d(TAG, "Employee began: Reporting utilization.") return attempt { val logEntry = hashMapOf( "timestamp" to System.currentTimeMillis(), "message" to "App utilization report.", "worker_run_id" to id.toString() ) // Use short-term fixed for now db.assortment(TEMPORARY_USAGE_LOG_COLLECTION).add(logEntry).await() Log.d(TAG, "Employee completed efficiently.") End result.success() } catch (e: Exception) { Log.e(TAG, "Employee failed", e) End result.failure() } } }
- Schedule the Employee: Replace
MainActivity.kt
‘sonCreate
technique.
MainActivity.kt additions// Add these imports to MainActivity.kt import android.content material.Context import android.util.Log import androidx.work.* import com.yourcompany.advancedconceptsapp.employee.ReportingWorker import java.util.concurrent.TimeUnit // Inside MainActivity class, after setContent { ... } block in onCreate override enjoyable onCreate(savedInstanceState: Bundle?) { tremendous.onCreate(savedInstanceState) setContent { // ... present code ... } // Schedule the employee schedulePeriodicUsageReport(this) } // Add this operate to MainActivity class non-public enjoyable schedulePeriodicUsageReport(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .construct() val reportingWorkRequest = PeriodicWorkRequestBuilder
( 1, TimeUnit.HOURS // ~ each hour ) .setConstraints(constraints) .addTag(ReportingWorker.TAG) .construct() WorkManager.getInstance(context).enqueueUniquePeriodicWork( ReportingWorker.TAG, ExistingPeriodicWorkPolicy.KEEP, reportingWorkRequest ) Log.d("MainActivity", "Periodic reporting work scheduled.") } - Take a look at WorkManager:
- Run the app. Test Logcat for messages from
ReportingWorker
andMainActivity
about scheduling. - WorkManager duties don’t run instantly, particularly periodic ones. You should use ADB instructions to power execution for testing:
- Discover your bundle identify:
com.yourcompany.advancedconceptsapp
- Pressure run jobs:
adb shell cmd jobscheduler run -f com.yourcompany.advancedconceptsapp 999
(The 999 is often enough, it’s a job ID). - Or use Android Studio’s App Inspection tab -> Background Process Inspector to view and set off employees.
- Discover your bundle identify:
- Test your Firestore Console for the
usage_logs
assortment.
- Run the app. Test Logcat for messages from
Step 4: Construct Flavors (dev vs. prod)
Create dev
and prod
flavors for various environments.
- Configure
app/construct.gradle.kts
:
app/construct.gradle.ktsandroid { // ... namespace, compileSdk, defaultConfig ... // ****** Allow BuildConfig technology ****** buildFeatures { buildConfig = true } // ******************************************* flavorDimensions += "setting" productFlavors { create("dev") { dimension = "setting" applicationIdSuffix = ".dev" // CRITICAL: Adjustments bundle identify for dev builds versionNameSuffix = "-dev" resValue("string", "app_name", "Process Reporter (Dev)") buildConfigField("String", "TASKS_COLLECTION", ""tasks_dev"") buildConfigField("String", "USAGE_LOG_COLLECTION", ""usage_logs_dev"") } create("prod") { dimension = "setting" resValue("string", "app_name", "Process Reporter") buildConfigField("String", "TASKS_COLLECTION", ""duties"") buildConfigField("String", "USAGE_LOG_COLLECTION", ""usage_logs"") } } // ... buildTypes, compileOptions, and so on ... }
Sync Gradle information.
Necessary: We added
applicationIdSuffix = ".dev"
. This implies the precise bundle identify in your improvement builds will change into one thing likecom.yourcompany.advancedconceptsapp.dev
. This requires an replace to your Firebase challenge setup, defined subsequent. Additionally be aware thebuildFeatures { buildConfig = true }
block which is required to make use ofbuildConfigField
. -
Dealing with Firebase for Suffixed Utility IDs
As a result of the `dev` taste now has a special software ID (`…advancedconceptsapp.dev`), the unique `google-services.json` file (downloaded in Step 1) won’t work for `dev` builds, inflicting a “No matching consumer discovered” error throughout construct.
It’s essential to add this new Utility ID to your Firebase challenge:
- Go to Firebase Console: Open your challenge settings (gear icon).
- Your apps: Scroll all the way down to the “Your apps” card.
- Add app: Click on “Add app” and choose the Android icon (>).
- Register dev app:
- Package deal identify: Enter the actual suffixed ID:
com.yourcompany.advancedconceptsapp.dev
(exchange `com.yourcompany.advancedconceptsapp` together with your precise base bundle identify). - Nickname (Elective): “Process Reporter Dev”.
- SHA-1 (Elective however Really helpful): Add the debug SHA-1 key from `./gradlew signingReport`.
- Package deal identify: Enter the actual suffixed ID:
- Register and Obtain: Click on “Register app”. Crucially, obtain the brand new
google-services.json
file supplied. This file now accommodates configurations for BOTH your base ID and the `.dev` suffixed ID. - Exchange File: In Android Studio (Venture view), delete the previous
google-services.json
from theapp/
listing and exchange it with the **newly downloaded** one. - Skip SDK steps: You possibly can skip the remaining steps within the Firebase console for including the SDK.
- Clear & Rebuild: Again in Android Studio, carry out a Construct -> Clear Venture after which Construct -> Rebuild Venture.
Now your challenge is accurately configured in Firebase for each `dev` (with the `.dev` suffix) and `prod` (base bundle identify) variants utilizing a single `google-services.json`.
- Create Taste-Particular Supply Units:
- Change to Venture view in Android Studio.
- Proper-click on
app/src
-> New -> Listing. Identify itdev
. - Inside
dev
, createres/values/
directories. - Proper-click on
app/src
-> New -> Listing. Identify itprod
. - Inside
prod
, createres/values/
directories. - (Elective however good observe): Now you can transfer the default
app_name
string definition fromapp/src/essential/res/values/strings.xml
into eachapp/src/dev/res/values/strings.xml
andapp/src/prod/res/values/strings.xml
. Or, you may rely solely on theresValue
definitions in Gradle (as accomplished above). UtilizingresValue
is usually easier for single strings likeapp_name
. In the event you had many alternative sources (layouts, drawables), you’d put them within the respectivedev/res
orprod/res
folders.
- Use Construct Config Fields in Code:
-
- Replace
TaskViewModel.kt
andReportingWorker.kt
to make use ofBuildConfig
as an alternative of short-term constants.
- Replace
TaskViewModel.kt change
// Add this import import com.yourcompany.advancedconceptsapp.BuildConfig // Exchange the short-term fixed utilization // const val TEMPORARY_TASKS_COLLECTION = "duties" // Take away this line non-public val tasksCollection = db.assortment(BuildConfig.TASKS_COLLECTION) // Use construct config discipline
ReportingWorker.kt change
// Add this import import com.yourcompany.advancedconceptsapp.BuildConfig // Exchange the short-term fixed utilization // const val TEMPORARY_USAGE_LOG_COLLECTION = "usage_logs" // Take away this line // ... inside doWork() ... db.assortment(BuildConfig.USAGE_LOG_COLLECTION).add(logEntry).await() // Use construct config discipline
Modify
TaskScreen.kt
to doubtlessly use the flavor-specific app identify (althoughresValue
handles this mechanically in the event you referenced@string/app_name
accurately, whichTopAppBar
often does). In the event you set the title instantly, you’d load it from sources:// In TaskScreen.kt (if wanted)
import androidx.compose.ui.res.stringResource
import com.yourcompany.advancedconceptsapp.R // Import R class
// Inside Scaffold -> topBarTopAppBar(title = { Textual content(stringResource(id = R.string.app_name)) }) // Use string useful resource
-
- Choose Construct Variant & Take a look at:
- In Android Studio, go to Construct -> Choose Construct Variant… (or use the “Construct Variants” panel often docked on the left).
- Now you can select between
devDebug
,devRelease
,prodDebug
, andprodRelease
. - Choose
devDebug
. Run the app. The title ought to say “Process Reporter (Dev)”. Knowledge ought to go totasks_dev
andusage_logs_dev
in Firestore. - Choose
prodDebug
. Run the app. The title must be “Process Reporter”. Knowledge ought to go toduties
andusage_logs
.
Step 5: Proguard/R8 Configuration (for Launch Builds)
R8 is the default code shrinker and obfuscator in Android Studio (successor to Proguard). It’s enabled by default for launch
construct sorts. We have to guarantee it doesn’t break our app, particularly Firestore knowledge mapping.
-
- Evaluate
app/construct.gradle.kts
Launch Construct Sort:
app/construct.gradle.ktsandroid { // ... buildTypes { launch { isMinifyEnabled = true // Must be true by default for launch isShrinkResources = true // R8 handles each proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.professional" // Our customized guidelines file ) } debug { isMinifyEnabled = false // Often false for debug proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.professional" ) } // ... debug construct sort ... } // ... }
isMinifyEnabled = true
permits R8 for thelaunch
construct sort. - Configure
app/proguard-rules.professional
:- Firestore makes use of reflection to serialize/deserialize knowledge lessons. R8 may take away or rename lessons/fields wanted for this course of. We have to add “maintain” guidelines.
- Open (or create) the
app/proguard-rules.professional
file. Add the next:
# Hold Process knowledge class and its members for Firestore serialization -keep class com.yourcompany.advancedconceptsapp.knowledge.Process { (...); *; } # Hold some other knowledge lessons used with Firestore equally # -keep class com.yourcompany.advancedconceptsapp.knowledge.AnotherFirestoreModel { (...); *; } # Hold Coroutine builders and intrinsics (typically wanted, although AGP/R8 deal with some mechanically) -keepnames class kotlinx.coroutines.intrinsics.** { *; } # Hold companion objects for Employees if wanted (generally R8 removes them) -keepclassmembers class * extends androidx.work.Employee { public static ** Companion; } # Hold particular fields/strategies if utilizing reflection elsewhere # -keepclassmembers class com.instance.SomeClass { # non-public java.lang.String someField; # public void someMethod(); # } # Add guidelines for some other libraries that require them (e.g., Retrofit, Gson, and so on.) # Seek the advice of library documentation for vital Proguard/R8 guidelines.
- Evaluate
-
- Rationalization:
-keep class ... {
: Retains the(...); *; } Process
class, its constructors (
), and all its fields/strategies (*
) from being eliminated or renamed. That is essential for Firestore.-keepnames
: Prevents renaming however permits elimination if unused.-keepclassmembers
: Retains particular members inside a category.
- Rationalization:
3. Take a look at the Launch Construct:
-
- Choose the
prodRelease
construct variant. - Go to Construct -> Generate Signed Bundle / APK…. Select APK.
- Create a brand new keystore or use an present one (comply with the prompts). Bear in mind the passwords!
- Choose
prodRelease
because the variant. Click on End. - Android Studio will construct the discharge APK. Discover it (often in
app/prod/launch/
). - Set up this APK manually on a tool:
adb set up app-prod-release.apk
. - Take a look at totally. Are you able to add duties? Do they seem? Does the background employee nonetheless log to Firestore (examine
usage_logs
)? If it crashes or knowledge doesn’t save/load accurately, R8 seemingly eliminated one thing essential. Test Logcat for errors (typicallyClassNotFoundException
orNoSuchMethodError
) and regulate yourproguard-rules.professional
file accordingly.
- Choose the
Step 6: Firebase App Distribution (for Dev Builds)
Configure Gradle to add improvement builds to testers through Firebase App Distribution.
- Obtain non-public key: on Firebase console go to Venture Overview at left high nook -> Service accounts -> Firebase Admin SDK -> Click on on “Generate new non-public key” button ->
api-project-xxx-yyy.json
transfer this file to root challenge on the similar stage of app folder *Make sure that this file be in your native app, don't push it to the distant repository as a result of it accommodates smart knowledge and might be rejected later - Configure App Distribution Plugin in
app/construct.gradle.kts
:
app/construct.gradle.kts// Apply the plugin on the high plugins { // ... different plugins id("com.android.software"), id("kotlin-android"), and so on. alias(libs.plugins.google.firebase.appdistribution) } android { // ... buildFeatures, flavorDimensions, productFlavors ... buildTypes { getByName("launch") { isMinifyEnabled = true // Must be true by default for launch isShrinkResources = true // R8 handles each proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.professional" // Our customized guidelines file ) } getByName("debug") { isMinifyEnabled = false // Often false for debug proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.professional" ) } firebaseAppDistribution { artifactType = "APK" releaseNotes = "Newest construct with fixes/options" testers = "briew@instance.com, bri@instance.com, cal@instance.com" serviceCredentialsFile="$rootDir/
api-project-xxx-yyy.json
"//don't push this line to the distant repository or stablish as native variable } } }Add library model to libs.model.toml
[versions] googleFirebaseAppdistribution = "5.1.1" [plugins] google-firebase-appdistribution = { id = "com.google.firebase.appdistribution", model.ref = "googleFirebaseAppdistribution" } Make sure the plugin classpath is within the
project-level
construct.gradle.kts
:challenge construct.gradle.kts
plugins { // ... alias(libs.plugins.google.firebase.appdistribution) apply false }
Sync Gradle information.
- Add a Construct Manually:
- Choose the specified variant (e.g.,
devDebug
,devRelease
,prodDebug
,prodRelease
). - In Android Studio Terminal run every commmand to generate apk model for every setting:
./gradlew assembleRelease appDistributionUploadProdRelease
./gradlew assembleRelease appDistributionUploadDevRelease
./gradlew assembleDebug appDistributionUploadProdDebug
./gradlew assembleDebug appDistributionUploadDevDebug
- Test Firebase Console -> App Distribution -> Choose .dev challenge . Add testers or use the configured group (`android-testers`).
- Choose the specified variant (e.g.,
Step 7: CI/CD with GitHub Actions
Automate constructing and distributing the `dev` construct on push to a particular department.
- Create GitHub Repository. Create a brand new repository on GitHub and push your challenge code to it.
-
- Generate FIREBASE_APP_ID:
- on Firebase App Distribution go to Venture Overview -> Common -> App ID for com.yourcompany.advancedconceptsapp.dev setting (1:xxxxxxxxx:android:yyyyyyyyyy)
- In GitHub repository go to Settings -> Secrets and techniques and variables -> Actions -> New repository secret
- Set the identify: FIREBASE_APP_ID and worth: paste the App ID generated
- Add FIREBASE_SERVICE_ACCOUNT_KEY_JSON:
- open
api-project-xxx-yyy.json
positioned at root challenge and replica the content material - In GitHub repository go to Settings -> Secrets and techniques and variables -> Actions -> New repository secret
- Set the identify: FIREBASE_SERVICE_ACCOUNT_KEY_JSON and worth: paste the json content material
- open
- Create GitHub Actions Workflow File:
- In your challenge root, create the directories
.github/workflows/
. - Inside
.github/workflows/
, create a brand new file namedandroid_build_distribute.yml
. - Paste the next content material:
- In your challenge root, create the directories
-
identify: Android CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: construct: runs-on: ubuntu-latest steps: - makes use of: actions/checkout@v3 - identify: arrange JDK 17 makes use of: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle - identify: Grant execute permission for gradlew run: chmod +x ./gradlew - identify: Construct devRelease APK run: ./gradlew assembleRelease - identify: add artifact to Firebase App Distribution makes use of: wzieba/Firebase-Distribution-Github-Motion@v1 with: appId: ${{ secrets and techniques.FIREBASE_APP_ID }} serviceCredentialsFileContent: ${{ secrets and techniques.FIREBASE_SERVICE_ACCOUNT_KEY_JSON }} teams: testers file: app/construct/outputs/apk/dev/launch/app-dev-release-unsigned.apk
- Generate FIREBASE_APP_ID:
-
- Commit and Push: Commit the
.github/workflows/android_build_distribute.yml
file and push it to youressential
department on GitHub.
- Commit and Push: Commit the
-
- Confirm: Go to the “Actions” tab in your GitHub repository. You must see the workflow working. If it succeeds, examine Firebase App Distribution for the brand new construct. Your testers ought to get notified.
Step 8: Testing and Verification Abstract
-
- Flavors: Change between
devDebug
andprodDebug
in Android Studio. Confirm the app identify modifications and knowledge goes to the proper Firestore collections (tasks_dev
/duties
,usage_logs_dev
/usage_logs
).
- Flavors: Change between
-
- WorkManager: Use the App Inspection -> Background Process Inspector or ADB instructions to confirm the
ReportingWorker
runs periodically and logs knowledge to the appropriate Firestore assortment based mostly on the chosen taste.
- WorkManager: Use the App Inspection -> Background Process Inspector or ADB instructions to confirm the
-
- R8/Proguard: Set up and check the
prodRelease
APK manually. Guarantee all options work, particularly including/viewing duties (Firestore interplay). Test Logcat for crashes associated to lacking lessons/strategies.
- R8/Proguard: Set up and check the
-
- App Distribution: Ensure testers obtain invitations for the
devDebug
(ordevRelease
) builds uploaded manually or through CI/CD. Guarantee they will set up and run the app.
- App Distribution: Ensure testers obtain invitations for the
-
- CI/CD: Test the GitHub Actions logs for profitable builds and uploads after pushing to the
develop
department. Confirm the construct seems in Firebase App Distribution.
- CI/CD: Test the GitHub Actions logs for profitable builds and uploads after pushing to the
Conclusion
Congratulations! You’ve navigated complicated Android subjects together with Firestore, WorkManager, Compose, Flavors (with appropriate Firebase setup), R8, App Distribution, and CI/CD.
This challenge supplies a stable basis. From right here, you may discover:
-
- Extra complicated WorkManager chains or constraints.
-
- Deeper R8/Proguard rule optimization.
-
- Extra subtle CI/CD pipelines (deploy signed apks/bundles, working checks, deploying to Google Play).
-
- Utilizing totally different NoSQL databases or native caching with Room.
-
- Superior Compose UI patterns and state administration.
-
- Firebase Authentication, Cloud Features, and so on.
If you wish to have entry to the complete code in my GitHub repository, contact me within the feedback.
Venture Folder Construction (Conceptual)
AdvancedConceptsApp/
├── .git/
├── .github/workflows/android_build_distribute.yml
├── .gradle/
├── app/
│ ├── construct/
│ ├── libs/
│ ├── src/
│ │ ├── essential/ # Frequent code, res, AndroidManifest.xml
│ │ │ └── java/com/yourcompany/advancedconceptsapp/
│ │ │ ├── knowledge/Process.kt
│ │ │ ├── ui/TaskScreen.kt, TaskViewModel.kt, theme/
│ │ │ ├── employee/ReportingWorker.kt
│ │ │ └── MainActivity.kt
│ │ ├── dev/ # Dev taste supply set (optionally available overrides)
│ │ ├── prod/ # Prod taste supply set (optionally available overrides)
│ │ ├── check/ # Unit checks
│ │ └── androidTest/ # Instrumentation checks
│ ├── google-services.json # *** IMPORTANT: Comprises configs for BOTH bundle names ***
│ ├── construct.gradle.kts # App-level construct script
│ └── proguard-rules.professional # R8/Proguard guidelines
├── api-project-xxx-yyy.json # Firebase service account key json
├── gradle/wrapper/
├── construct.gradle.kts # Venture-level construct script
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts