Holographic dark blue tech banner featuring floating code panels with Python and Kotlin syntax compared side-by-side, clean database schema diagrams, a glowing green Android logo silhouette, and neon teal gradient background lighting.
Master modern Android app engineering from the ground up: transition from foundational Python coding to production-ready Kotlin, Jetpack Compose UI, MVVM Clean Architecture, and fully automated Fastlane CI/CD pipelines.
Edunxt Tech Learning Android App Development Masterclass & SOP | EDUNXT TECH LEARNING
Edunxt Tech Learning Android App Development Presentation

Mastering Android App Engineering
A Comprehensive Curriculum & Standard Operating Procedure

An exhaustive engineering playbook detailing the transition from foundational logic (Python) to professional mobile systems design in Kotlin, Jetpack Compose, Clean Architecture, Security, background operations, and CI/CD automation.

Author: EDUNXT TECH LEARNING 6 Comprehensive Learning Phases 14 Detailed SOP Modules Production-Ready Code Implementation

Roadmap

The Standard Android Development Curriculum

An end-to-end timeline mapped out across 40 weeks, starting from programming logic foundations, progressing through core layouts, backends, security, testing, and concluding with CI/CD deployment.

Phase 1: Foundation Weeks 1 – 6

Master Kotlin, structured concurrency, coroutines, reactive flows, and set up a standardized team dev environment containing Android Studio, Gradle dependencies, Git branching strategies, and AVD emulators.

Phase 2: Android Core Weeks 7 – 18

Transition completely to declarative UI with Jetpack Compose. Implement clean MVVM pattern, dependency injection with Hilt, type-safe Jetpack Navigation, and database schema management with Room DB and Datastore.

Phase 3: Backend Integration & Security Weeks 19 – 26

Connect with REST APIs via Retrofit, manage network interception with OkHttp, design secure client applications with SSL Pinning, manage user authentication through Firebase, and obfuscate layouts using ProGuard/R8 rules.

Phase 4: Advanced Services & Testing Weeks 27 – 34

Establish background synchronization via WorkManager, push notifications via FCM, and implement comprehensive test suites (unit, UI Compose, and instrumentation tests) alongside memory profiling and LeakCanary diagnostics.

Phase 5: Release & Monetisation Weeks 35 – 38

Prepare builds using Android App Bundles (AAB), set up Google Play Console, implement Google Play Billing for subscriptions, and configure Google AdMob for target banner and interstitial ads.

Phase 6: Maintenance & CI/CD Ongoing

Integrate Firebase Crashlytics and ANR logging, set up CI/CD automation pipelines using GitHub Actions, configure Fastlane for automated deployments, and run controlled staged rollouts.

Module 0

Transitioning from Python to Kotlin: Coding Logic Foundation

A side-by-side comparison establishing core programming patterns. Transition your algorithms from dynamic scripting to static, type-safe, compiled software engineering.

For developers beginning their coding journey, Python is often the gateway. Python’s simplicity allows learners to quickly grasp variables, loops, control flow, functions, and object-oriented programming (OOP) principles. However, dynamic typing, interpreted execution, and runtime errors in Python present challenges when scaling to complex enterprise software. Developing high-performance, crash-free Android applications requires transitioning from Python’s flexibility to Kotlin’s static typing, compile-time safety checks, and garbage-collected JVM environment.

Why Kotlin for Android App Development?

Kotlin offers complete interoperability with legacy Java codebases while delivering modern language features that eliminate common runtime crashes. The compiler enforces strict check-points on type compatibility, null safety, and interface declarations, transforming runtime bugs into compile-time errors. Let’s compare basic programming elements side-by-side to understand how to map your Python knowledge directly to Kotlin.

Python (Dynamic & Interpreted)

  • Dynamic Typing: Variables can change their types dynamically at runtime. For example, data = "hello" can later be reassigned as data = 42 without compile-time errors.
  • Indentation-Based: Logical blocks are defined strictly by whitespace indentations. A single missing space triggers an IndentationError.
  • Late Binding: Attributes and method calls are resolved at execution time, which can trigger runtime attribute errors.
  • No Strict Nullability: Variables can hold None at any time, leading to unexpected runtime AttributeError or TypeError exceptions if not carefully checked.

Kotlin (Static & Compiled to JVM)

  • Static Type Inference: Types are checked at compile time; you cannot assign an integer to a string variable once declared. The compiler guarantees type integrity.
  • Brace-Based: Code blocks are bounded using curly brackets {}. Semicolons are optional, and code style is structured and uniform.
  • Compile-time Resolution: The compiler resolves signatures, interfaces, and methods before generating bytecode, catching errors before deployment.
  • Null Safety: The compiler distinguishes nullable types from non-nullable types, preventing the famous NullPointerException at compile time.

Syntax Mapping Comparison

The following structural segments show how standard logic blocks map between Python and Kotlin. Notice how Kotlin introduces variable declarations using val (read-only immutable) and var (mutable), explicit type annotations, and strict function signatures.

Example 0.1: Basic Syntax Comparison
# ==========================================
# Python Code: Variable, Logic, and Loop
# ==========================================
user_name = "Alex"      # Type inferred as string
age = 25                # Type inferred as integer
is_active = True        # Boolean flag

if age >= 18:
    print(f"User {user_name} is an adult.")
else:
    print(f"User {user_name} is a minor.")

# Iterating over lists
items = ["Apples", "Bananas", "Cherries"]
for index, item in enumerate(items):
    print(f"Item {index}: {item}")Python
// ==========================================
// Kotlin Code: Variable, Logic, and Loop
// ==========================================
val userName: String = "Alex"  // val = immutable read-only
var age: Int = 25              // var = mutable variable
val isActive: Boolean = true

if (age >= 18) {
    println("User $userName is an adult.") // String interpolation using $
} else {
    println("User $userName is a minor.")
}

// Iterating over collections
val items = listOf("Apples", "Bananas", "Cherries")
for ((index, item) in items.withIndex()) {
    println("Item $index: $item")
}Kotlin

Functions & Object-Oriented Logic

Let’s look at defining reusable functions and custom data classes. In Python, class structures are flexible and can accept runtime changes. In Kotlin, classes require strict constructor configurations, properties, and access modifiers (such as private, internal, or public).

# ==========================================
# Python Function and Class Definition
# ==========================================
def calculate_price(quantity: int, unit_price: float) -> float:
    return quantity * unit_price

class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price

    def get_display_name(self) -> str:
        return f"{self.name} (${self.price:.2f})"

p = Product("Smartphone", 899.99)
print(p.get_display_name())Python
// ==========================================
// Kotlin Function and Class Definition
// ==========================================
fun calculatePrice(quantity: Int, unitPrice: Double): Double {
    return quantity * unitPrice
}

// Kotlin data classes automatically generate toString(), equals(), hashCode(), and copy()
data class Product(
    val name: String,
    val price: Double
) {
    fun getDisplayName(): String {
        return "$name ($${String.format("%.2f", price)})"
    }
}

fun main() {
    val p = Product("Smartphone", 899.99)
    println(p.getDisplayName())
}Kotlin

Collection Transformations: Comprehensions vs. Higher-Order Functions

Python utilizes list comprehensions to perform quick transformations on data sets. In Kotlin, we achieve this through higher-order collection functions such as map, filter, and flatMap. This aligns with functional programming principles.

# Python list comprehension filtering and squaring numbers
numbers = [1, 2, 3, 4, 5, 6]
squared_evens = [x**2 for x in numbers if x % 2 == 0]
# Result: [4, 16, 36]Python Comprehension
// Kotlin collection processing pipeline
val numbers = listOf(1, 2, 3, 4, 5, 6)
val squaredEvens = numbers
    .filter { it % 2 == 0 }
    .map { it * it }
// Result: [4, 16, 36]Kotlin Transformations

Exception Handling and Error Flows

Python captures errors through a standard try-except-finally block. Kotlin uses try-catch-finally blocks. In Kotlin, exceptions are unchecked, meaning you are not forced by the compiler to catch them, but defensive programming is still required to handle API and database failures.

# Python exception management
try:
    value = int("not_a_number")
except ValueError as e:
    print(f"Failed to parse: {e}")
finally:
    print("Process complete.")Python Exception
// Kotlin exception management
try {
    val value = "not_a_number".toInt()
} catch (e: NumberFormatException) {
    println("Failed to parse: ${e.message}")
} finally {
    println("Process complete.")
}Kotlin Exception
๐Ÿ’ก
Transition Tip: Immutable by Default

In Python, variables are reassigned freely. When programming in Kotlin, adopt the rule: always declare variables using val by default. Only change val to var if you must change the stored reference. This minimizes state mutations, leading to highly predictable code and preventing thread-unsafe actions inside your apps.

Phase 1

Phase 1: Foundation โ€” Kotlin Language & Development Tooling

Building an industry-standard base. Master Kotlin’s type safety patterns, concurrent coroutine architecture, and the complete Android Studio setup.

Module 1: Kotlin Deep Dive โ€” Null Safety, Coroutines & Flows

Writing stable, bug-free Android applications requires a complete understanding of Kotlin’s advanced language specifications. We focus on three core pillars: Null Safety, Coroutines, and Flows.

Null Safety โ€” Eliminating the Billion-Dollar Mistake

In Kotlin, types are non-nullable by default. Attempting to assign null to a standard variable will cause a compile-time crash. To declare a variable that can hold a null reference, append a question mark (?) to the type definition. Safe-call operators (?.) and the Elvis operator (?:) allow developers to safely resolve objects without causing runtime crashes. This compile-time check completely removes the most common cause of mobile app crashes: the NullPointerException.

val name: String = "John" // Non-nullable. Cannot be assigned null.
var nullableName: String? = null // Nullable type.

// Safe check using ?. and Elvis fallback
val length: Int = nullableName?.length ?: 0
println("String length is: $length") // Prints 0

// Safe cast smart type casting
val obj: Any = "Hello"
val str: String? = obj as? StringKotlin Null Safety

Coroutines โ€” Lightweight Concurrency

Android UI threads run strictly on the Main thread. Heavy tasks (like network calls, database processing, or file I/O) executed directly on the Main thread freeze the interface, resulting in an “App Not Responding” (ANR) warning. Kotlin Coroutines solve this using cooperative multitasking, suspending execution without blocking the main thread.

Coroutines are managed using Scopes and Dispatchers. While a thread represents a physical resource managed by the OS, a coroutine is a logical state that can be scheduled across different thread pools. This means thousands of coroutines can run concurrently on a single thread pool without the memory overhead of spawning new physical threads.

Coroutine Component Dispatcher Selection Standard Use Case Thread Allocation
Dispatchers.MainUI Interaction / UI ThreadUpdating Compose components, lightweight logic, state updates.Single Main Thread
Dispatchers.IOI/O Thread PoolReading/writing databases, executing network requests, disk operations.Elastic Thread Pool (max 64)
Dispatchers.DefaultCPU-bound Thread PoolComplex calculations, rendering images, parsing JSON data structures.Fixed Thread Pool (equal to CPU cores)
Dispatchers.UnconfinedNot confined to specific threadsStarts execution in the current call-stack and suspends dynamically.Runs on current execution thread

Coroutine Scope Builders and Lifecycle Scopes

Never run coroutines inside GlobalScope, as this can leak resources if the host screen or ViewModel is destroyed. In Android, always use lifecycle-aware scopes:

  • viewModelScope: Automatically cancels all active coroutines when the parent ViewModel is cleared.
  • lifecycleScope: Automatically cancels when the host Activity or Fragment reaches the destroyed state.
  • rememberCoroutineScope(): Creates a local Compose-aware scope that cancels when the Composable leaves the composition.

import kotlinx.coroutines.*

fun fetchUserData() {
    // Launching a coroutine scoped to UI operations
    CoroutineScope(Dispatchers.Main).launch {
        // Suspend function runs asynchronously on background Thread
        val result = withContext(Dispatchers.IO) {
            executeSlowNetworkRequest() 
        }
        // Suspended execution resumes here back on Main thread
        updateUIWithResult(result)
    }
}

suspend fun executeSlowNetworkRequest(): String {
    delay(2000) // Simulating network latency
    return "User Profile Payload"
}Coroutine Concurrency

Kotlin Flows โ€” Asynchronous Data Streams

While standard functions return a single value, Kotlin Flow<T> is a cold stream that emits multiple sequentially calculated values asynchronously. StateFlow and SharedFlow provide hot streams that broadcast states directly to the UI layers.

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*

fun getLiveStockTickers(): Flow<Double> = flow {
    var price = 150.0
    while(true) {
        delay(1000)
        price += (Math.random() - 0.5) * 5
        emit(price) // Emitting values sequentially to collectors
    }
}

// Collector usage inside viewModelScope
fun collectPrices() {
    viewModelScope.launch {
        getLiveStockTickers()
            .filter { it > 152.0 } // Intermediate transformation
            .map { "Price is: $it" }
            .collect { displayString ->
                println(displayString) // Terminal operation
            }
    }
}Asynchronous Flow Stream

Module 2: Dev Environment Setup โ€” Android Studio, Gradle & Git

A professional development environment ensures consistent builds across a team. Setting up a standardized system prevents environmental discrepancies.

IDE

Android Studio Config

Use the latest stable build of Android Studio (such as Ladybug or newer). Configure JDK 17 or JDK 21 as the system boot SDK for compatibility with modern Gradle platforms.

G

Gradle Build Tool

Understand the build automation tool. Configure settings across `build.gradle.kts` modules utilizing Kotlin DSL scripts and explicit Version Catalogs (`libs.versions.toml`).

AVD

AVD Emulator System

Set up hardware-accelerated Android Virtual Devices using x86_64 Google Play Services system images for high-speed debug cycles and multi-screen testing configurations.

Standard Gradle Version Catalog Setup (libs.versions.toml)

Modern Android pipelines maintain dependency libraries inside a single centralized location: the Version Catalog. This ensures that all submodules reference matching libraries.

[versions]
ktx = "1.12.0"
composeCompiler = "1.5.8"
hilt = "2.50"
retrofit = "2.9.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }

[plugins]
android-application = { id = "com.android.application", version = "8.2.2" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "1.9.22" }libs.versions.toml

Git Workflow SOP

To coordinate development, team members must adhere to strict branch conventions:

  • main: Houses production-stable packages. Direct commits are restricted. All code changes require signed Pull Requests (PRs).
  • develop: Integration branch containing staging features. All automated builds run unit test checks against this branch.
  • feature/jira-id-description: Dedicated workspace branches created for specific JIRA tasks (e.g., feature/AND-102-login-screen).
Rule of Action: Git Conflict Avoidance

Before merging code into the shared integration branch, developers must rebase their branches locally against the latest remote `origin/develop` to resolve merge conflicts in isolation before triggering a remote PR: git pull --rebase origin develop. This maintains a clean commit history and simplifies release tagging.

Phase 2

Phase 2: Android Core โ€” Modern UI, Architecture & Local Storage

Deep dive into UI design, component dependencies, and local databases. Build responsive apps using Jetpack Compose, clean architectures, and Room databases.

Module 3: Jetpack Compose โ€” Modern Declarative UI

Jetpack Compose completely replaces the legacy XML system. Instead of maintaining XML files and linking them via findViewById, Compose uses Kotlin functions to describe your UI declaratively. The framework automatically updates your UI components when state values change.

Legacy XML Layouts

  • Verbose syntax using nested elements. Hard to maintain and refactor.
  • State is decoupled from layouts, leading to UI/code synchronization bugs.
  • Complex transitions require layout transitions, animation resources, and verbose code.
  • Fragment lifecycles and views increase complexity. Double memory representation of views.

Jetpack Compose UI

  • Clean UI code written entirely in Kotlin. Easier to share and refactor.
  • Single Source of Truth: Declarative layouts redraw automatically when states change.
  • Animation APIs enable smooth, state-driven UI transitions natively.
  • Modular Composable functions make it easy to reuse UI elements. Single-pass layout engine.

Recomposition and State Management

Recomposition is the process of executing Composable functions again with updated parameters. To optimize performance, Compose skips Composables whose parameters have not changed. Use remember to cache values across recompositions, and wrap values in mutableStateOf to trigger UI updates.

Understanding the Three Phases of Compose Layout

Jetpack Compose converts code into visible pixels through three distinct phases:

  1. Composition: Determines what UI components should be displayed. Compose executes Composable functions and builds a tree structure of UI nodes.
  2. Layout: Determines where components should be placed. During this single-pass phase, Compose measures children nodes and decides their coordinates.
  3. Drawing: Draws components on the screen canvas based on their bounds and coordinates.

State Hoisting and Custom Modifiers

State hoisting is a pattern of moving state to a Composable’s caller to make the Composable stateless. This improves testability and reusability. Modifiers decorate Composable elements, allowing you to add behavior, layout adjustments, click listeners, and padding. The order of Modifiers is critical: chaining modifier.padding(16.dp).background(Color.Blue) produces a different visual result than modifier.background(Color.Blue).padding(16.dp).

import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun CounterWidget() {
    // remember keeps state preserved across recomposition passes
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(text = "Current Click Count: $count", style = MaterialTheme.typography.bodyLarge)
        Button(onClick = { count++ }) {
            Text(text = "Increment Counter")
        }
    }
}Compose Declarative Component

Module 4: MVVM + Clean Architecture

We use a structured architectural model based on Separation of Concerns (SoC) and Dependency Inversion. Our system splits code into three distinct layers: Presentation, Domain, and Data.

Presentation (UI/ViewModel)
โ†’
Domain (Use Cases/Entities)
โ†’
Data (Repository/Database/Network)
  • Presentation Layer: Contains UI layout components (Jetpack Compose) and ViewModels. ViewModels observe data streams, update UI state, and survive configuration changes like screen rotations.
  • Domain Layer: Holds the core business logic of the application. It operates independently of databases or network layers, using Use Cases that act as single-purpose execution classes. This is the most stable layer and contains no framework-specific dependencies.
  • Data Layer: Handles network operations (Retrofit APIs), local caching (Room Databases), and repository implementations that coordinate database and network updates. It abstracts data sources from the rest of the app.

Dependency Injection with Hilt

Hilt simplifies dependency injection by managing object scopes and lifecycles automatically. The following example demonstrates injecting a Data Repository into a Domain Use Case, which is then provided to the ViewModel.

import javax.inject.Inject
import dagger.hilt.android.lifecycle.HiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

// 1. Domain Layer Definition
class GetUserUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): User {
        return userRepository.fetchUserProfile(userId)
    }
}

// 2. Presentation Layer ViewModel
@HiltViewModel
class UserProfileViewModel @Inject constructor(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _uiState = MutableStateFlow<UserState>(UserState.Loading)
    val uiState: StateFlow<UserState> = _uiState.asStateFlow()

    fun loadProfile(id: String) {
        viewModelScope.launch {
            try {
                val profile = getUserUseCase(id)
                _uiState.value = UserState.Success(profile)
            } catch (ex: Exception) {
                _uiState.value = UserState.Error(ex.message ?: "Unknown Error")
            }
        }
    }
}

sealed interface UserState {
    object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}Hilt DI Architecture Flow

Module 5: Local Storage โ€” Room DB & DataStore

Android provides different APIs for different types of local storage. Relational datasets should go in a SQLite database via Room, while key-value settings should use Jetpack DataStore.

Storage Type Technology Used Ideal Data Payload Thread Model
Relational DatabaseRoom DatabaseComplex profiles, offline transaction lists, nested objects.Async (Coroutines/Flows)
Preferences / Key-ValueJetpack DataStore (Preferences)User preferences, auth tokens, light boolean configuration flags.Coroutines (Flow)
Secure Key-ValueEncryptedSharedPreferencesAuthentication tokens, highly sensitive security credentials.Synchronous / Blocking

Room Implementation Code Structure

Room abstracts SQLite operations into type-safe Kotlin interfaces using annotations. It verifies SQL queries at compile time, reducing database errors.

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Entity(tableName = "cached_users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM cached_users WHERE id = :userId")
    fun observeUser(userId: String): Flow<UserEntity?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: UserEntity)

    @Delete
    suspend fun removeUser(user: UserEntity)
}

@Database(entities = [UserEntity::class], version = 2, exportSchema = true)
abstract class AppRoomDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}Room Database Architecture

Handling Database Migrations

When you update entity structures (such as adding a column to UserEntity), you must write a database migration to prevent application crashes for existing users.

import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // SQL statement to alter table and add new column
        db.execSQL("ALTER TABLE cached_users ADD COLUMN phone_number TEXT DEFAULT '' NOT NULL")
    }
}Room Migration Setup
SOP: Database Schema Changes

When updating database structures, write a migration script and update the database version. Do not set fallbackToDestructiveMigration() in production environments, as this deletes all local user database instances during updates.

Phase 3

Phase 3: Backend Integration, Security & Data Integration

Connect your app to external services. Configure network routing pipelines, secure API communication, and safeguard user details.

Module 6: APIs & Networking โ€” Retrofit & REST

Retrofit is the industry standard for network integration in Android. It converts REST endpoints into type-safe Kotlin interfaces. OkHttp handles the underlying connection pooling, logging, and request interception.

Retrofit Interface & OkHttp Config

To implement network requests, declare interface routes with annotations, configure an OkHttp client with a logging interceptor, and handle serialization using `kotlinx.serialization` or `Gson` converters.

import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path

// 1. Define Retrofit API endpoints
interface UserApiService {
    @GET("users/{id}/profile")
    suspend fun getUserProfile(@Path("id") userId: String): UserNetworkDto
}

// Data Transfer Object
data class UserNetworkDto(
    val id: String,
    val name: String,
    val email: String
)

// 2. Auth Interceptor to add Headers dynamically
class AuthInterceptor(private val token: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(request)
    }
}

// 3. Build OkHttp Client with interceptors
fun createNetworkClient(authToken: String): UserApiService {
    val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .addInterceptor(AuthInterceptor(authToken))
        .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
        .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
        .build()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.edunxt.com/v1/")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    return retrofit.create(UserApiService::class.java)
}Retrofit Network Client

Module 7: Auth & Security โ€” SSL Pinning & Code Obfuscation

Security is critical for keeping user accounts safe and protecting app data. You should secure communication using SSL Pinning and encrypt credentials locally with the Android Keystore.

๐Ÿ›ก๏ธ

SSL Pinning Validation

Prevent Man-in-the-Middle (MitM) attacks by pinning the server’s public key fingerprint inside the app. This ensures the app only trusts certificates that match the pinned keys, blocking proxy tools like Charles or Fiddler.

๐Ÿ”‘

Android Keystore System

Encrypt sensitive tokens and data locally using cryptographic keys stored securely in the hardware-backed Android Keystore. Key extraction is blocked at the hardware level.

OkHttp SSL Pinning Implementation

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

fun secureHttpClient(): OkHttpClient {
    // Restricting connections strictly to servers containing matching public keys
    val certificatePinner = CertificatePinner.Builder()
        .add("api.edunxt.com", "sha256/afwKpfq1gf01pfs0pfas8dfhasu0fasjdf=" )
        .build()

    return OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build()
}SSL Pinning configuration

Network Security Configuration File

Android 9 (API level 28) and higher disable cleartext HTTP traffic by default. Use a Network Security Config XML file to customize certificate pinning configurations and secure network routing.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">edunxt.com</domain>
        <pin-set expiration="2027-01-01">
            <pin digest="SHA-256">afwKpfq1gf01pfs0pfas8dfhasu0fasjdf=</pin>
        </pin-set>
    </domain-config>
</network-security-config>network_security_config.xml

Code Obfuscation via ProGuard & R8

Obfuscation shrinks your code and renames classes, fields, and methods to make it harder to reverse engineer. Configure optimization rules in `proguard-rules.pro` to keep critical classes (such as database entities or JSON serialization DTOs) from being stripped or broken by renaming.

# Keep network models from renaming to prevent JSON parsing errors
-keepclassmembers class com.edunxt.data.network.dto.** { *; }

# Keep database entities safe from schema modification
-keep class * extends androidx.room.RoomDatabase { *; }

# Remove logging statements in production builds
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int d(...);
}proguard-rules.pro
Phase 4

Phase 4: Advanced Services, Testing & Diagnostics

Deliver real-time events and build production-ready test suites. Master background operations, unit/UI testing, and memory leak analysis.

Module 8: Background Tasks โ€” FCM & WorkManager

To conserve battery, modern Android releases restrict background execution. Background tasks should be managed using Firebase Cloud Messaging (FCM) for remote push events and WorkManager for local tasks.

Technology Scheduling Type Guarantees Best For
WorkManagerOne-off or Periodic DeferredPersistent execution, runs even after system restarts.Uploading logs, local syncs, database cleanups.
Firebase Cloud MessagingPush Server NotificationReal-time messaging, wakes app from background.Chat messages, order updates, promotions.
Foreground ServiceImmediate ExecutionVisible task with a persistent notification.Audio playback, active GPS navigation.

Scheduling Tasks with WorkManager

import android.content.Context
import androidx.work.*

// 1. Declare Worker execution logic
class DataSyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            performSync()
            Result.success()
        } catch (e: Exception) {
            // Automatic retry based on BackoffPolicy if execution fails
            if (runAttemptCount < 3) Result.retry() else Result.failure()
        }
    }

    private suspend fun performSync() {
        // Perform synchronization logic
    }
}

// 2. Schedule Worker with Constraints
fun triggerSync(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi only
        .setRequiresBatteryNotLow(true)
        .build()

    val syncRequest = OneTimeWorkRequestBuilder<DataSyncWorker>()
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
        .build()

    WorkManager.getInstance(context).enqueue(syncRequest)
}WorkManager Execution

Module 9: Testing โ€” Unit, UI & Integration Tests

A reliable test suite gives developers the confidence to refactor code without introducing regressions. We use JUnit and MockK to test business logic, and the Compose Test Rule to verify UI components.

Unit Testing Business Logic

import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test

class GetUserUseCaseTest {
    // Mocking the data dependency
    private val repository = mockk<UserRepository>()
    private val getUserUseCase = GetUserUseCase(repository)

    @Test
    fun `when load profile gets user, return correct profile`() = runTest {
        val mockUser = User(id = "12", name = "Dan", email = "dan@edunxt.com")
        coEvery { repository.fetchUserProfile("12") } returns mockUser

        val result = getUserUseCase("12")

        assertEquals("Dan", result.name)
        coVerify(exactly = 1) { repository.fetchUserProfile("12") }
    }
}Unit Test Case

UI Testing with Compose

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test

class CounterWidgetUiTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun incrementCounter_updatesDisplayValue() {
        // Start the UI component in the test container
        composeTestRule.setContent {
            CounterWidget()
        }

        // Verify initial state
        composeTestRule.onNodeWithText("Current Click Count: 0").assertExists()

        // Perform interaction
        composeTestRule.onNodeWithText("Increment Counter").performClick()

        // Verify updated state
        composeTestRule.onNodeWithText("Current Click Count: 1").assertExists()
    }
}Compose UI Test

Module 10: Performance Profiling โ€” LeakCanary & App Optimization

Performance problems and resource bottlenecks can make apps feel sluggish. Use the Profilers built into Android Studio alongside diagnostic libraries to optimize performance.

โš ๏ธ

Memory Leak Detection

Memory leaks occur when the app retains references to objects that are no longer needed, preventing the Garbage Collector from freeing their memory. Use LeakCanary in debug builds to automatically detect and analyze leaks.

๐Ÿš€

Startup Time Optimization

Slow startup times can frustrate users. Optimize startup performance by generating Baseline Profiles to pre-compile critical code paths and class structures before execution.

Common Leak Cause & Mitigation

Memory leaks are often caused by capturing Context inside long-running coroutines, holding references to Views inside Fragments, or registering event listeners without unregistering them. Avoid passing Activity Context to long-lived singleton instances.

// LEAKING CODE EXAMPLE
class LocationTracker private constructor(private val context: Context) {
    // Passing an Activity Context here leaks the entire Activity when it is destroyed.
}

// SECURE IMPLEMENTATION
class LocationTracker private constructor(private val context: Context) {
    // Extracting applicationContext keeps the reference scoped globally to the app lifecycle
    private val appContext = context.applicationContext
}Context Memory Management
Phase 5

Phase 5: Launch Preparation, Store Release & Monetisation

Release your app to the public. Generate optimized production packages and implement secure payment integrations.

Module 11: Play Store Publishing โ€” App Bundles (AAB) & Policies

Google Play requires developers to submit releases using the Android App Bundle (AAB) format instead of traditional APKs. App Bundles allow Google Play to generate and serve optimized APKs tailored to each user’s device configuration (screen density, CPU architecture, and language assets), reducing download sizes by up to 35%.

Publishing Step Required Output Verification Checklist
App SigningSigned App Bundle (AAB)Configure Play App Signing in the Google Play Console; keep release keystores stored securely.
Content RatingQuestionnaire ratingFill out the target audience and content questionnaire honestly to prevent app suspension.
Privacy PolicyHosted URL linkProvide a clear privacy policy detail explaining what user data is collected and how it is processed.

Module 12: Monetisation โ€” Billing API & AdMob

Integrate the Google Play Billing Library to process in-app purchases and subscriptions securely, and configure AdMob to serve targeted advertisements.

Integrating Google Play Billing

import android.content.Context
import com.android.billingclient.api.*

class BillingManager(context: Context) : PurchasesUpdatedListener {

    private val billingClient = BillingClient.newBuilder(context)
        .setListener(this)
        .enablePendingPurchases()
        .build()

    fun connectToGooglePlay() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    // Billing connection established. Safe to query products.
                }
            }
            override fun onBillingServiceDisconnected() {
                // Handle disconnection. Retry connection.
            }
        })
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (purchase in purchases) {
                handlePurchase(purchase)
            }
        }
    }

    private fun handlePurchase(purchase: Purchase) {
        // Validate purchase tokens on your backend and grant entitlements to users.
    }
}Google Play Billing Library
Phase 6

Phase 6: Post-Launch โ€” Monitoring, Analytics & CI/CD Pipelines

Maintain application stability and automate releases. Configure analytics tracking, error reporting, and CI/CD pipelines.

Module 13: Analytics & Crashlytics โ€” Firebase Integration

Monitor your app’s stability in real time. Use Firebase Crashlytics to capture crashes and tracking logs, and configure analytics events to understand user behavior.

import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase

object AnalyticsService {
    private val analytics: FirebaseAnalytics = Firebase.analytics
    private val crashlytics: FirebaseCrashlytics = FirebaseCrashlytics.getInstance()

    fun trackUserAction(actionName: String, userId: String) {
        // 1. Log event in Firebase Analytics
        analytics.logEvent("user_action") {
            param("action_name", actionName)
            param("user_id", userId)
        }

        // 2. Set key identifiers in Crashlytics logs
        crashlytics.setUserId(userId)
        crashlytics.log("Logged Action: $actionName for user: $userId")
    }

    fun logHandledException(throwable: Throwable) {
        // Track non-fatal exceptions in Crashlytics
        crashlytics.recordException(throwable)
    }
}Firebase Monitoring Service

Module 14: CI/CD Pipelines โ€” GitHub Actions & Fastlane

Automate your build and deployment process to ensure consistent, reliable releases. Your CI/CD pipeline should build the app, run tests, sign the bundle, and upload it to the Play Store automatically on every release branch commit.

GitHub Trigger
โ†’
Run Unit Tests
โ†’
Fastlane Build & Sign
โ†’
Google Play Staged Deployment

GitHub Actions Workflow Config (release-pipeline.yml)

name: Production Release Pipeline

on:
  push:
    branches:
      - release/*

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Source Code
        uses: actions/checkout@v3

      - name: Set up Java Environment
        uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Cache Gradle Packages
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/libs.versions.toml') }}

      - name: Execute Static Lint & Unit Tests
        run: ./gradlew lint testReleaseUnitTest

      - name: Deploy Production AAB via Fastlane
        env:
          PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
        run: bundle exec fastlane deploy_to_play_storeGitHub Actions Config

Fastlane Configuration (Fastfile)

Fastlane orchestrates packaging, signing, and uploading App Bundles directly to the Google Play Developer Console.

default_platform(:android)

platform :android do
  desc "Build AAB and deploy to Google Play Store Staging"
  lane :deploy_to_play_store do
    # Run gradle build target task
    gradle(
      task: 'clean bundleRelease'
    )

    # Upload AAB directly to Play Store internal test track
    upload_to_play_store(
      track: 'internal',
      aab: 'app/build/outputs/bundle/release/app-release.aab',
      json_key_data: ENV["PLAY_STORE_JSON_KEY"]
    )
  end
endFastlane Lane Config

Global Android App Development Masterclass & SOP

Author: EDUNXT TECH LEARNING ยฉ 2026. All rights reserved.

This presentation represents the standard operating procedures for Android App Development is for educational purpose only.