Showing posts with label Kotlin. Show all posts
Showing posts with label Kotlin. Show all posts

Android App Startup Performance Guide

“First impressions happen in milliseconds — your app’s startup time is the handshake between the user and your code.”

As Android engineers, we often obsess over features, UI polish, and API integration. Yet, one of the most critical (and overlooked) aspects of user experience is how fast your app launches. Users won’t wait long to experience brilliance — a slow start is often a dead start.

This article explores modern strategies and best practices for improving startup performance using the latest Android tools and techniques.


Understanding Startup Phases

Before optimizing, let’s define the types of app starts:

  • Cold Start: App launches fresh — no process in memory.

  • Warm Start: Process exists, but app is not in foreground.

  • Hot Start: App resumes instantly from background.

Cold starts are the most expensive and the main target for optimization.


Measure Before You Optimize

Optimization without metrics is guesswork.
Start by measuring startup time with precision using these tools:

  • Android Studio Profiler → Startup Profiler Tab

  • Macrobenchmark & Baseline Profiles

  • Perfetto and Systrace

  • Firebase Performance Monitoring

Quick CLI check:

adb shell am start -W com.example/.MainActivity

It displays total launch time directly.


Use Baseline Profiles (Game Changer)

A Baseline Profile precompiles critical code paths, reducing startup time by up to 50% on real devices.

Steps to implement:

  1. Add macrobenchmark and baselineprofile dependencies.

  2. Record startup behavior via test.

  3. Bundle baseline-prof.txt with your release build.

Example:

@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @get:Rule val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startup() = benchmarkRule.measureRepeated(
        packageName = "com.example",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        setupBlock = { pressHome() }
    ) {
        startActivityAndWait()
    }
}

Lightweight Application Initialization

Avoid

  • Heavy SDKs in Application.onCreate().

  • Synchronous analytics or DI initialization.

Use

  • Lazy initialization

  • Coroutines on Dispatchers.IO

  • WorkManager for deferred setup

Example:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
            initAnalytics()
            initCrashlytics()
        }
    }
}

This keeps your main thread clean and your splash screen quick.


Controlled Initialization with App Startup

Modern Android provides App Startup (AndroidX) for structured initialization.

class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.initialize(context)
    }
    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

You can control initialization order using:

android:initOrder="2"

Use this to load non-critical SDKs later.


Optimize Dependency Injection (Hilt/Dagger)

DI is powerful but can slow startup if used carelessly.

Tips:

  • Keep eager singletons minimal.

  • Use lazy injection for heavy objects.

  • Avoid large modules initializing in Application scope.

Example:

@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
    @Provides
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder().baseUrl(BASE_URL).build()
}

Compose UI and Layout Optimization

  • Use Jetpack Compose for faster inflation and less overhead.

  • Keep composition scopes small.

  • Use the SplashScreen API to manage the pre-draw phase gracefully.

Example:

installSplashScreen().setKeepOnScreenCondition {
    viewModel.isLoading.value
}

Preload Smartly

Load just enough data to make the first screen functional:

  • Use Room for cached data.

  • Use DataStore instead of SharedPreferences.

  • Fetch fresh data asynchronously after rendering.


Defer Non-Critical Tasks

Use WorkManager or delayed coroutines for:

  • Crashlytics

  • Analytics

  • Remote Config

  • SDK initializations

Example:

Handler(Looper.getMainLooper()).postDelayed({
    initRemoteConfig()
}, 3000)

Resource & Build Optimization

  • Use R8 + ProGuard for code shrinking.

  • Convert images to WebP.

  • Use VectorDrawables over PNGs.

  • Enable resource shrinking:

    shrinkResources true
    minifyEnabled true
    
  • Deliver via Android App Bundle (AAB) for smaller installs.


Post-Launch Monitoring

After deployment, continuously track:

  • Cold/Warm start times

  • ANR rates

  • Frozen frames

  • Startup crashes

Use Firebase Performance Monitoring or Play Console metrics.


Example Startup Timeline Strategy

Component Strategy Timing
Core DI Graph Lazy load On first need
Analytics Deferred init After 3s
Remote Config Background coroutine After splash
Compose UI Minimal recomposition First frame
Cached Data Room + Coroutine Async load
Baseline Profile Pre-compiled Pre-release

My View as a Sr. Android Engineer

Startup optimization is more than trimming milliseconds — it’s about engineering trust.
When your app launches instantly, it signals quality, reliability, and care.
By combining Baseline Profiles, lazy initialization, and controlled startup sequencing, you ensure that your code performs as thoughtfully as it’s designed.


Analogy

“Startup optimization is like preparing for a long flight — pack essentials in your carry-on and check the rest in later.”


 ๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

Building Custom Reusable UI Components in Jetpack Compose

The Modern Android Way

“Reusable UI is not just a coding habit — it’s a design philosophy that scales apps, teams, and ideas.”



As Android engineers, we often find ourselves solving the same UI problem over and over — a stylized button, a progress loader, a card with actions. In traditional XML days, we would duplicate layouts or inflate custom views.

But with Jetpack Compose, UI development is declarative, modular, and reactive — a perfect environment for reusable, scalable, and testable components.

In this article, I’ll walk through how to design custom reusable UI components using modern Compose patterns aligned with Clean Architecture and feature modularization — the same structure that powers enterprise-grade fintech apps.


Why Reusable UI Components Matter

Reusable UI is more than DRY (Don’t Repeat Yourself). It enables:

  • Consistency across the app’s look and feel.

  • Scalability, so new features integrate faster.

  • Testability, since each UI piece is independent.

  • Theming flexibility, to support brand-level customization.

  • Faster CI/CD, since UI updates are isolated to one module.


Modular Architecture Overview

Compose works beautifully with modularization.
Here’s a recommended structure for large apps:

app/
 ├── MainActivity.kt
 ├── navigation/
core/
 ├── designsystem/
 │    ├── components/
 │    │    ├── CustomButton.kt
 │    │    ├── CustomProgress.kt
 │    │    ├── CustomCard.kt
 │    ├── theme/
 │    ├── typography/
 ├── utils/
feature/
 ├── dashboard/
 │    ├── ui/
 │    ├── viewmodel/
 │    ├── domain/

Rule of thumb:

  • core/designsystem → Your reusable UI kit (like Material 3 but customized).

  • feature/* → Screens that consume those components.

  • app → Wires navigation and dependency injection.


Step 1: Creating a Reusable Custom Button

Let’s start simple — a custom button that can be reused across modules.

@Composable
fun AppButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colorScheme.primary,
    textColor: Color = Color.White,
    enabled: Boolean = true
) {
    Button(
        onClick = onClick,
        modifier = modifier
            .fillMaxWidth()
            .height(56.dp),
        enabled = enabled,
        colors = ButtonDefaults.buttonColors(
            containerColor = backgroundColor,
            contentColor = textColor
        ),
        shape = RoundedCornerShape(12.dp)
    ) {
        Text(
            text = text,
            style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold)
        )
    }
}

What makes this reusable

  • Parameterized: text, onClick, color, enabled.

  • Styled centrally using your theme.

  • No hardcoded logic — just a pure, stateless composable.


Step 2: Making It Themed & Adaptive

Your core/designsystem/theme defines your app’s brand look:

@Composable
fun AppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = lightColorScheme(
            primary = Color(0xFF1565C0),
            secondary = Color(0xFF64B5F6)
        ),
        typography = Typography,
        content = content
    )
}

Now, every AppButton automatically aligns with the theme colors and typography across your app.


Step 3: Building a Custom Progress Indicator

Let’s add a reusable loading animation with Compose’s Canvas:

@Composable
fun AppProgressBar(
    progress: Float,
    modifier: Modifier = Modifier,
    color: Color = MaterialTheme.colorScheme.primary,
    strokeWidth: Dp = 6.dp
) {
    Canvas(modifier = modifier.size(100.dp)) {
        val sweepAngle = 360 * progress
        drawArc(
            color = color,
            startAngle = -90f,
            sweepAngle = sweepAngle,
            useCenter = false,
            style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)
        )
    }
}

Reusable Advantage:
This progress bar can be dropped into any screen — dashboard loading, biometric scanning, or file uploads — without extra setup.


Step 4: Adding Motion and State

Composable components are state-driven. Let’s animate progress reactively:

@Composable
fun AnimatedProgress(targetValue: Float) {
    val progress by animateFloatAsState(
        targetValue = targetValue,
        animationSpec = tween(1500)
    )
    AppProgressBar(progress = progress)
}

Every change to targetValue triggers an animation — clean, declarative, and side-effect-safe.


Step 5: Composable Composition

Compose encourages composition over inheritance. You can easily compose smaller reusable elements:

@Composable
fun AppCard(
    title: String,
    subtitle: String,
    icon: ImageVector,
    onClick: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(16.dp))
            .background(MaterialTheme.colorScheme.surfaceVariant)
            .clickable { onClick() }
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
        Spacer(Modifier.width(12.dp))
        Column {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Text(subtitle, style = MaterialTheme.typography.bodySmall)
        }
    }
}

Now your feature modules can combine AppCard, AppButton, and AppProgressBar to build complex UIs effortlessly.


Step 6: Integrating Reusable Components in Features

In your feature module (e.g., Dashboard):

@Composable
fun DashboardScreen(viewModel: DashboardViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    when (uiState) {
        is UiState.Loading -> AnimatedProgress(0.7f)
        is UiState.Success -> AppCard(
            title = "Balance",
            subtitle = "$12,450.00",
            icon = Icons.Default.AttachMoney,
            onClick = { /* Navigate */ }
        )
        is UiState.Error -> AppButton("Retry", onClick = viewModel::fetchData)
    }
}

The result: clean, modular, theme-aware UIs that require no repetitive logic.


Step 7: Accessibility and Testing

Accessibility should never be an afterthought. Compose makes it simple:

Modifier.semantics {
    contentDescription = "Loading ${progress * 100}%"
}

For testing:

@get:Rule val composeTestRule = createComposeRule()

@Test
fun testButtonDisplaysText() {
    composeTestRule.setContent { AppButton("Submit", onClick = {}) }
    composeTestRule.onNodeWithText("Submit").assertExists()
}

Design System Philosophy

Think of your core/designsystem as a mini Material library for your brand.
When a designer updates the theme or typography, every screen reflects it automatically.

That’s how large teams scale UI with confidence — Compose turns UI consistency into an architectural feature.


Key Takeaways

Principle Description
Stateless Composables Avoid internal logic; take data via parameters.
Theming Define global color, shape, and typography systems.
Composition > Inheritance Build larger components from smaller ones.
Accessibility Always use semantics for TalkBack support.
Testing Use ComposeTestRule for UI validation.
Modularity Keep your design system separate from features.

My Thoughts

As Android engineers, our UI should evolve as fast as user expectations.
Jetpack Compose gives us the freedom to innovate — while modular architecture keeps our codebase structured and scalable.

Reusable UI isn’t just a best practice; it’s a strategy for sustainable growth in modern Android apps.

“Compose taught us that great UIs aren’t built — they’re composed.”



๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

Functional vs Non-Functional Requirements in Modern Android App Development

A Senior Android Engineer’s Guide to Building Reliable, Scalable, and Delightful Apps

“Great apps don’t just work — they feel right.”



In modern Android app development, success isn’t measured only by what your app does (features), but also by how it performs, scales, and delights users.

That’s where Functional and Non-Functional Requirements (NFRs) come into play.

Functional requirements define what the system should do — the visible behaviors and actions.
Non-functional requirements define how the system should behave — the invisible qualities that separate a mediocre app from a world-class product.

Let’s explore both through real-world Android use cases, best practices, and architecture principles.


Functional Requirements — The “What”

These describe the core features and interactions users directly experience.
They define the app’s functional capabilities — tasks, data processing, business rules, and UI behaviors.

Examples in Android

  1. User Authentication

    • Sign-in with biometric, PIN, or OAuth (e.g., Google, Schwab Secure Login).

    • Use BiometricPrompt API and Jetpack Security for encryption.

  2. Data Fetching & Display

    • Fetch real-time stock prices using Retrofit + Coroutines + Flow.

    • Display data using Jetpack Compose with sealed UI states (Loading, Success, Error).

  3. Offline Mode

    • Cache the latest currency or weather data using Room or DataStore.

    • Sync changes when the device is online using WorkManager.

  4. Push Notifications

    • Implement FCM for trade alerts or balance updates.

    • Handle foreground and background states gracefully.

  5. Accessibility & Localization

    • Support TalkBack, font scaling, and dynamic color theming.

    • Provide localized strings (en, es, ne) with Android’s resources/values structure.

Best Practices

  • Follow MVVM Clean Architecture for modular, testable design.

  • Use sealed classes for predictable UI state management.

  • Use Kotlin Coroutines + Flow for structured concurrency and reactive data flow.

  • Validate business logic in the domain layer, not UI.


Non-Functional Requirements — The “How”

These define the quality attributes that make your app performant, secure, scalable, and user-friendly.

They often determine whether the app succeeds in real-world conditions.

Common Non-Functional Aspects

Category Description Android Example
Performance App speed, memory use, responsiveness Optimize recompositions in Jetpack Compose using remember and stable keys
Security Data protection, secure communication Use EncryptedSharedPreferences, TLS 1.3, certificate pinning
Scalability Ability to handle growing users/data Modular architecture + Repository pattern
Reliability Stability under various network or device conditions Use Retry strategies with Flow.retryWhen()
Usability UX clarity and accessibility Material 3 components, motion transitions, accessible color contrast
Maintainability Ease of code updates and testing Follow SOLID, Clean Architecture, Dependency Injection via Hilt
Compatibility Support for different Android API levels & devices Leverage backward compatibility libraries (AppCompat, Core KTX)
Observability Logs, crash monitoring, metrics Integrate Firebase Crashlytics, Macrobenchmark, or Perfetto

Use Case 1 — Mobile Banking App

Functional:

  • Biometric login

  • Fund transfer

  • Real-time transaction updates

Non-Functional:

  • End-to-end encryption (TLS + Keystore)

  • PCI-DSS compliance for card data

  • Low latency (under 200ms API response time)

  • Smooth UI transitions (Compose animation APIs)

Best Practice:

  • Use WorkManager for secure background synchronization.

  • Employ Hilt for dependency management across features.

  • Implement ProGuard & R8 for obfuscation.


Use Case 2 — Trading App (Thinkorswim-like Example)

Functional:

  • Real-time stock charts

  • Trade placement

  • Watchlist synchronization

Non-Functional:

  • High throughput WebSocket connections

  • Optimized rendering with custom Canvas charts

  • Security: Token encryption via Jetpack Security

  • Accessibility for visually impaired traders (TalkBack + custom semantics)

Best Practice:

  • Use Coroutines + Channels for WebSocket streams.

  • Employ Macrobenchmark to test frame drops during chart updates.

  • Build with modular architecture: core-network, core-ui, feature-trade, feature-watchlist.


Modern Best Practices for Balancing Both

Principle Description
Shift-Left Quality Consider NFRs early in the development cycle.
CI/CD Automation Integrate Lint, Detekt, Unit/UI tests in Jenkins or Bitrise.
Structured Concurrency Use viewModelScope and SupervisorJob for predictable task cancellation.
Jetpack Compose Performance Tuning Use derivedStateOf, LaunchedEffect, and smart recomposition strategies.
Monitoring & Observability Integrate Firebase Performance, ANR Watchdog, and custom metrics.

Final Thoughts

Functional requirements define your product’s purpose, while non-functional requirements define its quality and soul.

As a Senior Android Engineer, your mission is to deliver both:

  • Features that users love, and

  • Experiences that feel fast, safe, and delightful.

“An app may function well, but it only thrives when it’s performant, secure, and inclusive.”
— Dharma Kshetri


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป


Structured Concurrency in Android

“Concurrency without structure is chaos. Structured concurrency makes coroutines predictable, safe, and lifecycle-aware.”

As Android engineers, we juggle multiple tasks: fetching API data, updating UI, caching results, syncing offline data, and handling user interactions—all happening asynchronously. Without proper management, this can lead to leaks, zombie coroutines, or even crashes.

This is where Structured Concurrency in Kotlin comes in. It enforces scoped, hierarchical coroutine management that aligns perfectly with Android lifecycles.


 What is Structured Concurrency?

Structured concurrency means that every coroutine has a parent scope, and when the parent is canceled (e.g., Activity destroyed, ViewModel cleared), all its child coroutines are automatically canceled.

Instead of launching fire-and-forget coroutines (like GlobalScope.launch), structured concurrency ensures:

  • Lifecycle awareness: Coroutines cancel when their UI component dies.

  • Error propagation: Exceptions bubble up to parent scopes.

  • Predictable flow: All children complete or cancel together.


 Building Blocks in Android

1. CoroutineScope

Defines the lifecycle boundary for coroutines.

  • viewModelScope → Tied to ViewModel lifecycle.

  • lifecycleScope → Tied to Activity/Fragment lifecycle.

  • rememberCoroutineScope → For Composables.

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val data = repository.getData()
            _uiState.value = data
        }
    }
}

2. Job & SupervisorJob

  • Job: Cancels all child coroutines when canceled.

  • SupervisorJob: Allows siblings to fail independently.

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

3. Coroutine Builders

  • launch { } → Fire-and-forget.

  • async { } → Returns Deferred<T> result, use await().

  • withContext { } → Switch dispatcher safely.

  • coroutineScope { } → Ensures children complete or cancel as a group.


 Real-World Use Cases in Android

1. Safe API Calls in ViewModel

class UserViewModel(private val repo: UserRepository) : ViewModel() {
    val userState = MutableStateFlow<User?>(null)

    fun loadUser() {
        viewModelScope.launch {
            try {
                val user = repo.fetchUser()
                userState.value = user
            } catch (e: IOException) {
                // Handle error gracefully
            }
        }
    }
}

 When the ViewModel clears, viewModelScope cancels the coroutine, preventing memory leaks.


2. Parallel Requests with async

suspend fun fetchProfileAndPosts() = coroutineScope {
    val profile = async { api.getProfile() }
    val posts = async { api.getPosts() }
    profile.await() to posts.await()
}

 If one request fails, both are canceled, avoiding inconsistent UI states.


3. With Timeout

withTimeout(5000) {
    repository.syncData()
}

 Cancels automatically if it takes longer than 5 seconds.


 4. In Jetpack Compose

@Composable
fun UserScreen(repo: UserRepository) {
    val scope = rememberCoroutineScope()
    var name by remember { mutableStateOf("") }

    Button(onClick = {
        scope.launch {
            name = repo.getUser().name
        }
    }) {
        Text("Load User")
    }
}

When the Composable leaves the composition, rememberCoroutineScope is canceled.


 Error Handling in Structured Concurrency

Centralized Exception Handling

val handler = CoroutineExceptionHandler { _, exception ->
    Log.e("CoroutineError", "Caught: $exception")
}

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + handler)

Supervisor Scope Example

suspend fun safeParallelCalls() = supervisorScope {
    val first = launch { riskyCall1() }
    val second = launch { riskyCall2() }
    // If one fails, the other keeps running
}

 Best Practices for Android Engineers

  1. Never use GlobalScope → Causes leaks and unmanaged coroutines.

  2. Always tie to lifecycle → Use viewModelScope, lifecycleScope, or rememberCoroutineScope.

  3. Use coroutineScope or supervisorScope for child tasks.

  4. Use Dispatchers wisely:

    • Main → UI updates.

    • IO → Network/database.

    • Default → CPU-intensive tasks.

  5. Write tests with runTest or TestCoroutineDispatcher.

  6. Handle cancellation properly (isActive, ensureActive()).


Analogy: Family Trip 


Think of structured concurrency like a family trip:
  • The parent (scope) starts the trip.

  • All kids (child coroutines) must stick together.

  • If the parent says "Trip’s over," everyone goes home.

  • No child keeps wandering alone (no leaks).


Closing Thoughts

Structured concurrency is not just a Kotlin feature—it’s a mindset. It brings discipline to concurrency, making apps safer, cleaner, and more maintainable.

As Android engineers, embracing structured concurrency ensures our coroutines don’t outlive their purpose, keeping apps fast, stable, and memory-leak free.


- By following these practices, you’ll write modern, production-ready Android code with Kotlin coroutines that scales across features, modules, and teams.


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป




Token Management in Android App Development: Best Practices for Engineers

As Android engineers, we’re constantly building apps that need to securely authenticate users and interact with backend APIs. One of the most critical parts of this puzzle is token management — making sure authentication is safe, seamless, and user-friendly.


 What is a Token?

A token is a digital key that grants access to protected resources (like APIs). Instead of repeatedly sending usernames and passwords (insecure and inefficient), the client app uses tokens to prove identity and permissions.

Tokens usually come in two flavors:

  • Access Token – short-lived, used for every API request.

  • Refresh Token – longer-lived, used to obtain a new access token once it expires.

Think of an access token as a hotel room key card and the refresh token as your hotel booking record. If your key card stops working, the receptionist (auth server) issues a new one, as long as your booking is valid.


Token Lifecycle in Android

Here’s the typical flow:

  1. Login – User authenticates with email/password/biometric → server issues access + refresh token.

  2. Store Securely – Tokens are stored securely on-device.

  3. Use in Requests – Access token is attached to each API call.

  4. Refresh When Expired – On 401 Unauthorized, refresh token is used to fetch a new access token.

  5. Logout – Tokens are cleared.

 Visual Flow:


 Best Practices for Token Management

  1. Use Access + Refresh Token Strategy

    • Access tokens: short-lived.

    • Refresh tokens: long-lived but securely stored.

  2. Secure Storage

    • Use EncryptedSharedPreferences / Encrypted DataStore.

    • Use Android Keystore for refresh tokens (hardware-backed security).

    • Keep access token in memory only to reduce exposure.

  3. Automatic Token Handling

    • Use OkHttp Interceptor to attach tokens.

    • Use OkHttp Authenticator to refresh tokens when expired.

    • Never manually add tokens in API calls.

  4. Follow Clean Architecture

    • Keep token logic in Data layer.

    • Expose login/refresh/logout via Use Cases (Domain layer).

    • UI observes AuthState from ViewModel.

  5. Security Best Practices

    • Always use HTTPS (TLS).

    • Never log tokens.

    • Force logout if refresh fails.

    • Test token expiration scenarios.


 Example Implementation in Android

Retrofit API

interface AuthApi {
    @POST("auth/login")
    suspend fun login(@Body request: LoginRequest): TokenResponse

    @POST("auth/refresh")
    suspend fun refreshToken(@Body request: RefreshRequest): TokenResponse
}

Token Storage

class TokenStorage(private val context: Context) {
    private val Context.dataStore by preferencesDataStore("secure_prefs")
    private val ACCESS_TOKEN = stringPreferencesKey("access_token")
    private val REFRESH_TOKEN = stringPreferencesKey("refresh_token")

    suspend fun saveTokens(access: String, refresh: String) {
        context.dataStore.edit {
            it[ACCESS_TOKEN] = access
            it[REFRESH_TOKEN] = refresh
        }
    }

    suspend fun getAccessToken() = context.dataStore.data.first()[ACCESS_TOKEN]
    suspend fun getRefreshToken() = context.dataStore.data.first()[REFRESH_TOKEN]
    suspend fun clearTokens() = context.dataStore.edit { it.clear() }
}

OkHttp Interceptor

class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider.getAccessToken()
        val newRequest = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(newRequest)
    }
}

OkHttp Authenticator (Auto Refresh)

class TokenAuthenticator(private val repo: AuthRepository) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        return runBlocking {
            if (repo.refreshToken()) {
                repo.getAccessToken()?.let { newToken ->
                    response.request.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                }
            } else null
        }
    }
}

Key Takeaways

  • Access tokens should be short-lived → reduces risk.

  • Refresh tokens should be secured in Keystore → prevents theft.

  • Automatic handling via Interceptor + Authenticator → reduces bugs.

  • Clean architecture separation → makes token logic testable + maintainable.

By following these best practices, you’ll deliver apps that are secure, reliable, and seamless for users.


Common Mistakes to Avoid

Even experienced developers sometimes fall into these pitfalls:

  1. Storing tokens in plain SharedPreferences

    • ❌ Vulnerable to root access and backup extraction.

    • ✅ Use Encrypted DataStore or Keystore.

  2. Using long-lived access tokens

    • ❌ If stolen, attackers can access APIs indefinitely.

    • ✅ Keep access tokens short-lived, rely on refresh tokens.

  3. Not handling 401 Unauthorized properly

    • ❌ App just crashes or shows an error.

    • ✅ Automatically refresh the token using Authenticator.

  4. Storing refresh tokens in memory only

    • ❌ Refresh token will be lost when the app restarts, forcing re-login.

    • ✅ Store refresh token securely on disk (Keystore-backed).

  5. Logging tokens in Crashlytics or Logcat

    • ❌ Attackers can retrieve sensitive tokens from logs.

    • ✅ Never log tokens, redact them if necessary.

  6. Not clearing tokens on logout

    • ❌ User data can remain exposed.

    • ✅ Always clear tokens from secure storage on logout.

  7. Refreshing tokens too eagerly

    • ❌ Wastes server resources and battery.

    • ✅ Refresh only when expired (lazy refresh).


 Token Lifecycle Management

  • On login → store both access + refresh token securely.

  • On every request → add access token.

  • On 401 → refresh token automatically.

  • On app start → check if refresh token exists & is valid.

  • On logout → clear all tokens from storage.




Final Words

Token management might sound like a low-level detail, but in reality, it’s the backbone of secure Android apps. Whether you’re building fintech, healthcare, or large-scale consumer apps, these practices ensure your app is both safe and user-friendly.


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

UI Design Principle or Paradigm or Pattern with Best and latest approach

When designing UI for Android app development, it's crucial to follow modern design principles, paradigms, and patterns to ensure the app is user-friendly, maintainable, and aligned with best practices. Below is a structured overview of each category with the best and latest approach as of 2025.


1. UI Design Principles (Timeless + Google-backed)

These are foundational ideas that guide good UI/UX:

Principle Description
Material Design 3 (Material You) Google’s latest design system with dynamic theming, accessibility, and better adaptability across devices.
Clarity Interface should communicate clearly; avoid ambiguity. Use labels, icons, and empty states well.
Consistency Components and interactions should behave the same throughout the app.
Feedback Every user action should get an immediate visual or haptic response.
Affordance Users should instinctively understand what an element does (e.g., tappable cards, clickable icons).
Minimalism Remove unnecessary elements and focus on core user tasks.
Accessibility-first Ensure all UI elements are usable by screen readers, have proper contrast, and support font scaling.

2. UI Paradigms (Approach to thinking about UI)

Paradigm Description Example in Android
Declarative UI (Latest) UI is a function of state. No need to imperatively update the UI. Jetpack Compose
Reactive Programming  UI reacts to data/state changes. Kotlin Flows + Compose
Unidirectional Data Flow (UDF)  Data flows from a single source to UI; UI sends events up. MVI / MVVM
Responsive Design UI adapts to screen sizes and device capabilities. WindowSizeClass, Foldables, Tablets
Theme-Aware / Adaptive UI  UI adapts to user's theme (dark/light) and preferences. Material You + Dynamic Colors

3. UI Design Patterns (Architectural + UX)

A. Architecture Patterns

Pattern Status Tool
MVVM (Model-View-ViewModel)  - Standard Jetpack ViewModel, Compose, StateFlow
MVI (Model-View-Intent) - Trending Orbit MVI, Decompose
Clean Architecture - Recommended Layers: UI → UseCases → Repository → Data Source
Hexagonal / Ports & Adapters - Advanced Enterprise-level separation

B. UI Interaction Patterns

Pattern Usage
Scaffold Layout Provides topBar, bottomBar, FAB, drawer in Compose.
Navigation Component (Jetpack) For in-app navigation and deep linking.
LazyColumn / LazyGrid Efficient lists/grids in Compose.
BottomSheet / ModalBottomSheet For additional layered content.
Pull to Refresh Common in feed-style UIs.
Gesture Detection Compose has pointerInput, Modifier.clickable, etc.
Animation & Motion Use Compose’s animate*AsState, MotionLayout, Transitions.

4. Best Practices & Modern Tools (2025)

Area Tool/Approach
UI Toolkit Jetpack Compose (replacing XML gradually)
State Management Kotlin Flow + StateFlow or MVI with Orbit/Dagger
Design System Material 3 (Material You)
Navigation Navigation-Compose or Decompose for MVI
Themes Dynamic Theming with MaterialTheme and color schemes
Multiplatform KMP with Compose Multiplatform
Accessibility semantics { }, TalkBack testing, font scaling, haptics
Testing Compose UI Test, Robolectric, Espresso (for hybrid apps)

UI Example in Compose (Material 3)

@Composable
fun ProductCard(product: Product) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        shape = RoundedCornerShape(16.dp),
        elevation = CardDefaults.cardElevation()
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(product.name, style = MaterialTheme.typography.titleMedium)
            Text("$${product.price}", style = MaterialTheme.typography.bodyMedium)
        }
    }
}

Summary: Latest & Best Approach (2025)

Use Jetpack Compose
Follow Material 3 (Material You)
Use StateFlow/Flow for state
Apply MVVM or MVI with Clean Architecture
Design for Accessibility + Responsive UI
Leverage Compose Preview, Theming, and Composable testing
Add animations via animate*, LaunchedEffect, or MotionLayout


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

Detekt : static code analysis tool for Kotlin

Detekt is a static code analysis tool for Kotlin that inspects your Android/Kotlin codebase for:

  •  Code smells
  •  Performance issues
  •  Potential memory/context leaks
  •  Style violations
  • Complexity, nesting, duplication, and more

Think of it as Kotlin’s equivalent of Lint or SonarQube, but tailored for Kotlin best practices — and very extendable.



 Why Use Detekt in Android App Development?

Benefit Description
 Catch bugs early Find context leaks, unclosed resources, and anti-patterns
 Maintain clean code Auto-check complexity, style, naming
CI-ready Works in GitHub Actions, GitLab, Bitrise, etc.
Customizable Add or disable rules, write your own
Kotlin-first No Java compatibility hacks needed

 Step-by-Step Integration in Android Project

 Step 1: Apply Detekt Plugin

In your root build.gradle.kts or build.gradle:

plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.6"
}

Or use classpath in older plugin DSLs.

 Step 2: Add Configuration (Optional)

Generate a default configuration file:

./gradlew detektGenerateConfig

This creates:
config/detekt/detekt.yml
You can customize rules, thresholds, and disabled checks here.

 Step 3: Run Detekt

Use:

./gradlew detekt

Optional with config:

./gradlew detekt --config config/detekt/detekt.yml

It will output findings in the console and optionally HTML/MD/XML reports.


 Useful Detekt Rules (Leak & Jetpack Compose Focused)

1. UseApplicationContext

Flags dangerous use of Activity context that may lead to leaks.

class Repo(val context: Context) // ๐Ÿšซ BAD

 Use context.applicationContext.


2. TooManyFunctions, LargeClass, LongMethod

Compose-friendly apps with many lambdas often need these enabled to track complexity.


3. UnsafeCallOnNullableType, CastToNullableType

Helps avoid unexpected crashes from nulls or unsafe casts.


4. ComplexCondition, NestedBlockDepth

Useful for detecting overly nested logic in state-handling Compose UIs.


5. MagicNumber, ForbiddenComment, MaxLineLength

Maintain clean and readable Compose files.


 Optional: Use Detekt with Compose + Custom Rules

You can even write custom Detekt rules to:

  • Detect misuse of remember or LaunchedEffect

  • Prevent ViewModel holding reference to Context

  • Flag mutable state misuse like:

var state by mutableStateOf(...) // without snapshot awareness? ๐Ÿ”ฅ Flag it

You’d implement these with Detekt's Rule API.


 CI Integration Example (GitHub Actions)

- name: Run Detekt
  run: ./gradlew detekt

You can configure it to fail PRs on violation, ensuring team-wide quality.


 Best Practices When Using Detekt

Practice Benefit
 Customize detekt.yml Tailor rules to team/style guidelines
 Run in CI Prevent regression and enforce code health
 Use for Context Leak Rules Prevent common Android lifecycle bugs
 Track complexity & function size Useful for Compose UI layers
 Extend for custom Compose rules Detect architecture violations

 Output Formats

  • Console (default)

  • HTMLbuild/reports/detekt/detekt.html

  • XML – for tools like SonarQube

  • SARIF – for GitHub Code Scanning


 Summary

Feature Value
Kotlin-first Yes 
Jetpack Compose friendly  Can flag misuse and complexity
Leak prevention  Context, state misuse, null safety
Configurable Fully customizable rules
Extendable Supports custom rule sets
CI Ready Easy to integrate in pipelines

๐Ÿ”— Useful Links


๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

Sealed Classes and Data Classes for State Management in Android

 In Android development, managing the state of an application effectively is critical for providing a smooth user experience. Sealed classes and data classes are two Kotlin features that work exceptionally well together to model and manage UI states in a clean and robust way, particularly when using Jetpack Compose or any other modern Android architecture like MVVM.

1. Sealed Classes Overview

A sealed class in Kotlin is a special class that restricts class inheritance to a limited set of types. It can have a fixed set of subclasses, which allows the compiler to know all possible types. This is particularly useful for state management because it enables exhaustive when checks and helps ensure that all possible states are covered.

Sealed classes are typically used to represent different states or outcomes (like success, error, or loading) in the app, such as when handling network responses, UI states, or other processes.

sealed class UIState {
    object Loading : UIState()
    data class Success(val data: String) : UIState()
    data class Error(val message: String) : UIState()
}

2. Data Classes Overview

A data class is a class in Kotlin that is used to hold data. It automatically generates useful methods such as toString(), equals(), hashCode(), and copy(). It's mainly used to represent immutable data, which is ideal for handling states that involve encapsulating information (like success or error data) without altering the state itself.

For example, in the sealed class example above, the Success and Error states are modeled using data classes, allowing them to hold and manage state-specific data.

3. How They Work Together

Sealed classes and data classes work together to encapsulate various states in a clean, type-safe manner. Here's how they work together in state management:

  • Sealed Class for Type Safety: Sealed classes are used to restrict and control the possible states of the system. The compiler knows all subclasses, so if a new state is added, it forces a code update to ensure that all states are handled properly.

  • Data Class for Holding Data: Data classes are used within sealed classes to hold and represent state-specific data, such as the result of an API call or any other data-driven UI state.

4. Use Cases in Android Apps

Here are some practical use cases where sealed classes and data classes are often used together:

Use Case 1: Network Request Handling

Consider a scenario where you need to display the state of a network request (loading, success, or error). You can use a sealed class to represent the possible states and data classes to carry the data in the success and error states.

sealed class UIState {
    object Loading : UIState()
    data class Success(val data: List<User>) : UIState()
    data class Error(val message: String) : UIState()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UIState>()
    val uiState: LiveData<UIState> get() = _uiState

    fun loadUsers() {
        _uiState.value = UIState.Loading
        viewModelScope.launch {
            try {
                val users = api.getUsers()  // network request
                _uiState.value = UIState.Success(users)
            } catch (e: Exception) {
                _uiState.value = UIState.Error(e.message ?: "An unknown error occurred")
            }
        }
    }
}

In this example:

  • UIState is a sealed class with three possible states: Loading, Success, and Error.

  • Success and Error are data classes used to hold specific data related to each state (list of users for success and an error message for failure).

  • The loadUsers() function simulates a network request and updates the state accordingly.

Use Case 2: Form Validation

Another common use case is managing the state of a form (e.g., checking if input is valid, showing errors, or displaying success).

sealed class ValidationState {
    object Valid : ValidationState()
    data class Invalid(val errorMessage: String) : ValidationState()
}

class FormViewModel : ViewModel() {
    private val _validationState = MutableLiveData<ValidationState>()
    val validationState: LiveData<ValidationState> get() = _validationState

    fun validateInput(input: String) {
        if (input.isNotEmpty() && input.length > 5) {
            _validationState.value = ValidationState.Valid
        } else {
            _validationState.value = ValidationState.Invalid("Input must be at least 6 characters long")
        }
    }
}

In this example:

  • ValidationState is a sealed class with two possible states: Valid and Invalid.

  • Invalid is a data class that holds the error message when the form input is invalid.

Use Case 3: UI State Management with Jetpack Compose

In Jetpack Compose, you can use sealed classes to manage different UI states such as loading, displaying content, or handling errors in a declarative way.

sealed class UIState {
    object Loading : UIState()
    data class Content(val message: String) : UIState()
    data class Error(val error: String) : UIState()
}

@Composable
fun MyScreen(viewModel: MyViewModel) {
    val uiState by viewModel.uiState.observeAsState(UIState.Loading)

    when (uiState) {
        is UIState.Loading -> {
            CircularProgressIndicator()
        }
        is UIState.Content -> {
            Text((uiState as UIState.Content).message)
        }
        is UIState.Error -> {
            Text("Error: ${(uiState as UIState.Error).error}")
        }
    }
}

In this case:

  • UIState is a sealed class used to handle different states in the UI.

  • The Content and Error data classes hold the actual data that is rendered in the UI.

  • Jetpack Compose will update the UI reactively based on the current state.

5. Benefits of Using Sealed and Data Classes Together

  • Exhaustiveness Checking: With sealed classes, the compiler ensures that you handle every possible state, reducing the chances of unhandled states or bugs.

  • Type Safety: Data classes encapsulate data in a structured way, while sealed classes ensure that the states are known and finite, making the system more predictable and less prone to errors.

  • Easy Debugging and Error Handling: By using data classes to represent different states, especially errors, it's easier to capture and display the exact error message or data related to a specific state.

 Thoughts

Sealed classes and data classes complement each other perfectly for state management in Android development, providing a robust, type-safe, and maintainable way to represent various states. Sealed classes give you control over state variation, while data classes store relevant data for each state. Together, they are an excellent choice for managing UI states, network responses, form validation, and other scenarios where different outcomes need to be handled in a clean and predictable manner.

๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป

Building a Scalable Architecture for an eCommerce App with Jetpack Compose

Design a scalable architecture for an eCommerce app built with Jetpack Compose. This architecture will support key features like offline product caching, real-time inventory updates, paginated product listings, and modular UI with feature separation. We’ll focus on best practices for scalability, maintainability, and modularity, ensuring the app can handle future growth efficiently.

Overview of the App Architecture

The architecture for this app will be based on Clean Architecture, separating concerns into Presentation, Domain, and Data layers. We will also modularize the app to ensure flexibility, and each feature (e.g., Product, Cart, Inventory) will be handled in a separate module.

We'll incorporate Jetpack Compose for UI, Room for offline caching, Paging 3 for efficient product listing, and Firebase/Realtime Database or WebSocket for real-time inventory updates.

Layered Architecture Breakdown

1. Presentation Layer (UI)

The Presentation Layer is responsible for the user interface and user interactions. With Jetpack Compose, we can easily build reactive and dynamic UIs. The UI will be composed of Composables, while ViewModels will handle the UI state and interact with the Domain layer.

Key Components:

  • Jetpack Compose: For building the user interface in a declarative way.

  • ViewModel: Handles state management and communicates with the Domain layer.

  • StateFlow/LiveData: For managing UI state like loading, success, and error states.

  • Navigation: Jetpack Navigation Compose to manage the app's navigation.

Example Composables:

@Composable
fun ProductListScreen(viewModel: ProductListViewModel) {
    val products by viewModel.products.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()
    val isError by viewModel.isError.collectAsState()

    if (isLoading) {
        CircularProgressIndicator()
    } else if (isError) {
        Text("Error fetching products")
    } else {
        LazyColumn {
            items(products) { product ->
                ProductItem(product = product)
            }
        }
    }
}

2. Domain Layer

The Domain Layer holds the business logic and use cases. This layer abstracts the data layer and provides clean interfaces for the Presentation layer to interact with. The domain layer consists of Use Cases and Repository interfaces.

Key Components:

  • Use Cases: Define business logic, such as fetching products, pagination, and handling inventory.

  • Repositories: Interface that defines data-fetching operations like fetching products, updating inventory, and more.

Example Use Case:

class GetProductListUseCase(private val productRepository: ProductRepository) {
    suspend operator fun invoke(page: Int): Result<List<Product>> {
        return productRepository.getPaginatedProducts(page)
    }
}

3. Data Layer

The Data Layer handles data fetching, caching, and the communication with external services (like APIs and Firebase). This layer includes repositories for both remote data (API calls) and local data (Room Database). We’ll use Room for offline caching and Paging 3 for efficient data loading.

Key Components:

  • Room: Used for offline caching of products and inventory data.

  • API Services: Retrofit or Ktor for interacting with remote APIs for products and real-time updates.

  • Firebase/Realtime Database: Used for real-time inventory updates.

  • Paging 3: Efficiently handles pagination for product lists.

Offline Caching Example with Room:

@Entity(tableName = "product")
data class ProductEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val price: Double,
    val stockQuantity: Int
)

@Dao
interface ProductDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProducts(products: List<ProductEntity>)

    @Query("SELECT * FROM product")
    suspend fun getAllProducts(): List<ProductEntity>
}

Repository Example:

class ProductRepositoryImpl(
    private val apiService: ApiService,
    private val productDao: ProductDao
) : ProductRepository {

    override suspend fun getPaginatedProducts(page: Int): Result<List<Product>> {
        val productsFromCache = productDao.getAllProducts()
        if (productsFromCache.isNotEmpty()) {
            return Result.success(productsFromCache.map { it.toDomain() })
        }

        try {
            val response = apiService.getProducts(page)
            productDao.insertProducts(response.products.map { it.toEntity() })
            return Result.success(response.products.map { it.toDomain() })
        } catch (e: Exception) {
            return Result.failure(e)
        }
    }
}

4. Real-Time Inventory Updates

For real-time inventory updates, we can use Firebase Realtime Database or WebSocket. When the stock quantity of a product changes, the app will update the product's data in real time, and the UI will reflect the updated information.

Firebase Example:

class FirebaseInventoryRepository {
    private val database = FirebaseDatabase.getInstance().getReference("inventory")

    fun observeInventoryUpdates(productId: Int, callback: (Int) -> Unit) {
        database.child("products").child(productId.toString()).child("stockQuantity")
            .addValueEventListener(object : ValueEventListener {
                override fun onDataChange(snapshot: DataSnapshot) {
                    val stockQuantity = snapshot.getValue(Int::class.java) ?: 0
                    callback(stockQuantity)
                }

                override fun onCancelled(error: DatabaseError) {
                    // Handle error
                }
            })
    }
}

5. Modularization

To ensure that the app remains maintainable as it grows, we will modularize the codebase. Each feature, such as the Product module, Cart module, and Inventory module, will be developed in separate modules.

This separation ensures that each module is responsible for one feature and can be developed and tested independently. It also improves build times and allows for easier team collaboration.

Modularization Example:

// In build.gradle for 'product' module
dependencies {
    implementation project(":core")
    implementation "androidx.compose.ui:ui:$compose_version"
}

6. Offline Handling and Connectivity

The app should handle offline scenarios gracefully, providing users with cached data when they are not connected to the internet. We can use the ConnectivityManager to check the network status and display cached products when offline. When the network is available, the app should fetch real-time data.

Offline Strategy:

  • Room Database: Cache products and inventory locally.

  • Network Status: Use ConnectivityManager to determine if the app is online or offline.

7. Real-Time Sync with Firebase

Firebase can be used for real-time syncing of inventory data. Using Firebase Realtime Database, the app can listen for changes to inventory quantities and update the UI instantly. Alternatively, WebSocket can be used to get real-time updates from the backend.

My thoughts

This architecture leverages modern Android tools like Jetpack Compose, Room, Paging 3, Firebase, and Clean Architecture to build a scalable and maintainable eCommerce app. The use of modularization ensures that each feature is self-contained, while the domain-driven design keeps the business logic separated from the UI.

By incorporating offline caching, real-time updates, and pagination, this architecture provides a robust foundation for building a seamless, scalable eCommerce experience that performs well even in scenarios with slow or no network connectivity.

๐Ÿ“ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! Please leave a comment below. I’d love to hear from you! ๐Ÿ‘‡

Happy coding! ๐Ÿ’ป