
This information will stroll you thru constructing a small utility step-by-step, specializing in integrating a number of highly effective instruments and ideas important for contemporary Android growth.
What We’ll Cowl:
- Jetpack Compose: Constructing the UI declaratively.
- NoSQL Database (Firestore): Storing and retrieving information within the cloud.
- WorkManager: Working dependable background duties.
- Construct Flavors: Creating completely different variations of the app (e.g., dev vs. prod).
- Proguard/R8: Shrinking and obfuscating your code for launch.
- Firebase App Distribution: Distributing take a look at builds simply.
- CI/CD (GitHub Actions): Automating the construct and distribution course of.
The Objective: Construct a “Process Reporter” app. Customers can add easy activity 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 completely different Firestore collections/information and distribute the dev
construct for testing.
Conditions:
- Android Studio (newest secure model really helpful).
- Fundamental understanding of Kotlin and Android growth 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 selection). - Package deal Identify: Your most well-liked package deal identify (e.g.,
com.yourcompany.advancedconceptsapp
). - Language: Kotlin.
- Minimal SDK: API 24 or greater.
- 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 connect with an current one.
- Click on “Add Cloud Firestore to your app”. Settle for adjustments to your
construct.gradle.kts
(orconstruct.gradle
) recordsdata. This provides the required 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 adjustments.
- Allow Providers in Firebase Console:
- Go to the Firebase Console and choose your challenge.
- Allow Firestore Database (begin in Check mode).
- Within the left menu, go to Construct -> Firestore Database. Click on “Create database”.
- Begin in Check mode for simpler preliminary growth (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 package deal 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. - Observe: We’ll probably substitute 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 needed 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 recordsdata.
- Process Knowledge Class: Create
information/Process.kt
.
information/Process.ktpackage deal com.yourcompany.advancedconceptsapp.information import com.google.firebase.firestore.DocumentId information 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.ktpackage deal 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.information.Process // Import BuildConfig later when wanted import kotlinx.coroutines.movement.MutableStateFlow import kotlinx.coroutines.movement.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.duties.await // Momentary placeholder - will likely be changed by BuildConfig subject const val TEMPORARY_TASKS_COLLECTION = "duties" class TaskViewModel : ViewModel() { non-public val db = Firebase.firestore // Use momentary 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.Course.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 activity = Process(description = description, timestamp = System.currentTimeMillis()) tasksCollection.add(activity).await() _error.worth = null } catch (e: Exception) { _error.worth = "Error including: ${e.localizedMessage}" } } } } - Major Display screen Composable: Create
ui/TaskScreen.kt
.
ui/TaskScreen.ktpackage deal com.yourcompany.advancedconceptsapp.ui // Imports: androidx.compose.*, androidx.lifecycle.viewmodel.compose.viewModel, java.textual content.SimpleDateFormat, and so forth. import androidx.compose.basis.structure.* import androidx.compose.basis.lazy.LazyColumn import androidx.compose.basis.lazy.objects 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.information.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 adjustments } ) { 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.peak(16.dp)) // Error Message errorMessage?.let { Textual content(it, colour = MaterialTheme.colorScheme.error, modifier = Modifier.padding(backside = 8.dp)) } // Process Listing if (duties.isEmpty() && errorMessage == null) { Textual content("No duties but. Add one!") } else { LazyColumn(modifier = Modifier.weight(1f)) { objects(duties, key = { it.id }) { activity -> TaskItem(activity) Divider() } } } } } } @Composable enjoyable TaskItem(activity: 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(activity.description, fashion = MaterialTheme.typography.bodyLarge) Textual content("Added: ${dateFormat.format(Date(activity.timestamp))}", fashion = MaterialTheme.typography.bodySmall) } } }
- Replace
MainActivity.kt
: Set the content material toTaskScreen
.
MainActivity.ktpackage deal com.yourcompany.advancedconceptsapp import android.os.Bundle import androidx.exercise.ComponentActivity import androidx.exercise.compose.setContent import androidx.compose.basis.structure.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 will likely 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: Check fundamental 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 will likely be up to date later).
employee/ReportingWorker.ktpackage deal 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 // Momentary placeholder - will likely be changed by BuildConfig subject 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(): Consequence { 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 momentary fixed for now db.assortment(TEMPORARY_USAGE_LOG_COLLECTION).add(logEntry).await() Log.d(TAG, "Employee completed efficiently.") Consequence.success() } catch (e: Exception) { Log.e(TAG, "Employee failed", e) Consequence.failure() } } }
- Schedule the Employee: Replace
MainActivity.kt
‘sonCreate
methodology.
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 { // ... current 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.") } - Check WorkManager:
- Run the app. Test Logcat for messages from
ReportingWorker
andMainActivity
about scheduling. - WorkManager duties don’t run instantly, particularly periodic ones. You need to use ADB instructions to pressure execution for testing:
- Discover your package deal identify:
com.yourcompany.advancedconceptsapp
- Drive run jobs:
adb shell cmd jobscheduler run -f com.yourcompany.advancedconceptsapp 999
(The 999 is normally enough, it’s a job ID). - Or use Android Studio’s App Inspection tab -> Background Process Inspector to view and set off staff.
- Discover your package deal 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 package deal 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 forth ... }
Sync Gradle recordsdata.
Vital: We added
applicationIdSuffix = ".dev"
. This implies the precise package deal identify to your growth builds will turn into one thing likecom.yourcompany.advancedconceptsapp.dev
. This requires an replace to your Firebase challenge setup, defined subsequent. Additionally word thebuildFeatures { buildConfig = true }
block which is required to make use ofbuildConfigField
. -
Dealing with Firebase for Suffixed Software IDs
As a result of the `dev` taste now has a distinct utility ID (`…advancedconceptsapp.dev`), the unique `google-services.json` file (downloaded in Step 1) is not going to work for `dev` builds, inflicting a “No matching consumer discovered” error throughout construct.
You could add this new Software ID to your Firebase challenge:
- Go to Firebase Console: Open your challenge settings (gear icon).
- Your apps: Scroll right 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 precise suffixed ID:
com.yourcompany.advancedconceptsapp.dev
(substitute `com.yourcompany.advancedconceptsapp` along with your precise base package deal 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 precise suffixed ID:
- Register and Obtain: Click on “Register app”. Crucially, obtain the brand new
google-services.json
file provided. This file now comprises configurations for BOTH your base ID and the `.dev` suffixed ID. - Substitute File: In Android Studio (Venture view), delete the outdated
google-services.json
from theapp/
listing and substitute 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 package deal 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/foremost/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 achieved above). UtilizingresValue
is usually less complicated for single strings likeapp_name
. Should you had many various assets (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 a substitute of momentary constants.
- Replace
TaskViewModel.kt change
// Add this import import com.yourcompany.advancedconceptsapp.BuildConfig // Substitute the momentary fixed utilization // const val TEMPORARY_TASKS_COLLECTION = "duties" // Take away this line non-public val tasksCollection = db.assortment(BuildConfig.TASKS_COLLECTION) // Use construct config subject
ReportingWorker.kt change
// Add this import import com.yourcompany.advancedconceptsapp.BuildConfig // Substitute the momentary 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 subject
Modify
TaskScreen.kt
to doubtlessly use the flavor-specific app identify (althoughresValue
handles this robotically in case you referenced@string/app_name
accurately, whichTopAppBar
normally does). Should you set the title immediately, you’d load it from assets:// 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 & Check:
- In Android Studio, go to Construct -> Choose Construct Variant… (or use the “Construct Variants” panel normally 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 needs to 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 information mapping.
-
- Overview
app/construct.gradle.kts
Launch Construct Kind:
app/construct.gradle.ktsandroid { // ... buildTypes { launch { isMinifyEnabled = true // Ought to 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
allows R8 for thelaunch
construct sort. - Configure
app/proguard-rules.professional
:- Firestore makes use of reflection to serialize/deserialize information lessons. R8 may take away or rename lessons/fields wanted for this course of. We have to add “preserve” guidelines.
- Open (or create) the
app/proguard-rules.professional
file. Add the next:
# Hold Process information class and its members for Firestore serialization -keep class com.yourcompany.advancedconceptsapp.information.Process { (...); *; } # Hold another information lessons used with Firestore equally # -keep class com.yourcompany.advancedconceptsapp.information.AnotherFirestoreModel { (...); *; } # Hold Coroutine builders and intrinsics (typically wanted, although AGP/R8 deal with some robotically) -keepnames class kotlinx.coroutines.intrinsics.** { *; } # Hold companion objects for Staff 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 another libraries that require them (e.g., Retrofit, Gson, and so forth.) # Seek the advice of library documentation for needed Proguard/R8 guidelines.
- Overview
-
- 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. Check 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 current 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 (normally in
app/prod/launch/
). - Set up this APK manually on a tool:
adb set up app-prod-release.apk
. - Check completely. Are you able to add duties? Do they seem? Does the background employee nonetheless log to Firestore (examine
usage_logs
)? If it crashes or information doesn’t save/load accurately, R8 probably eliminated one thing vital. Test Logcat for errors (typicallyClassNotFoundException
orNoSuchMethodError
) and modify yourproguard-rules.professional
file accordingly.
- Choose the
Step 6: Firebase App Distribution (for Dev Builds)
Configure Gradle to add growth builds to testers by way of 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 identical stage of app folder *Be sure that this file be in your native app, don't push it to the distant repository as a result of it comprises smart information and will likely 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.utility"), id("kotlin-android"), and so forth. alias(libs.plugins.google.firebase.appdistribution) } android { // ... buildFeatures, flavorDimensions, productFlavors ... buildTypes { getByName("launch") { isMinifyEnabled = true // Ought to 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 recordsdata.
- 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 -> Normal -> 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
situated 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 yourforemost
department on GitHub.
- Commit and Push: Commit the
-
- Confirm: Go to the “Actions” tab in your GitHub repository. It is best to 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 adjustments and information goes to the right 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 information 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 take a look at 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 take a look at the
-
- App Distribution: Be certain that testers obtain invitations for the
devDebug
(ordevRelease
) builds uploaded manually or by way of CI/CD. Guarantee they will set up and run the app.
- App Distribution: Be certain that 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 strong 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 completely different NoSQL databases or native caching with Room.
-
- Superior Compose UI patterns and state administration.
-
- Firebase Authentication, Cloud Capabilities, and so forth.
If you wish to have entry to the total 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/
│ │ ├── foremost/ # Widespread code, res, AndroidManifest.xml
│ │ │ └── java/com/yourcompany/advancedconceptsapp/
│ │ │ ├── information/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)
│ │ ├── take a look at/ # Unit checks
│ │ └── androidTest/ # Instrumentation checks
│ ├── google-services.json # *** IMPORTANT: Comprises configs for BOTH package deal 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