Android

Why Device State Matters for Resource Limits

How Android Smartly Manages App Resources Based on Device State

When it comes to balancing app performance, user experience, and battery life, Android uses a sophisticated system of resource management that adapts dynamically based on the device’s current state. Understanding how these resource limits work is crucial for developers aiming to build efficient, battery-friendly, and responsive apps. 

In this blog, we’ll dive deep into how Android enforces or exempts resource limits depending on whether the device is charging, the screen is on, or the device is in a low-power mode like Doze.

Why Device State Matters for Resource Limits

Modern smartphones juggle dozens of apps and background processes at once. Without some form of resource control, one rogue app could drain your battery, clog the network, or degrade user experience for everything else running on your phone. To combat this, Android classifies device state into several modes — charging, screen on, and screen off with Doze active — and applies or lifts resource limits accordingly.

Resource Controls by Device State: The Big Picture

Let’s break down how resource limits change based on the device state, focusing on four key areas every developer and advanced user should know: Jobs, Alarms, Network Access, and Firebase Cloud Messaging (FCM).

1. Device Charging

Charging is the most permissive state.

  • Jobs: When your device is plugged in, most job execution limits are lifted, with the exception of apps in the restricted standby bucket (these are apps the user or system has placed under heavy restriction for background activity).
  • Alarms: There are essentially no limits to alarm scheduling, unless you have manually restricted an app’s battery usage.
  • Network Access: Apps can access the network freely. No special throttling or restrictions are applied.
  • FCM: Firebase Cloud Messaging enjoys unrestricted delivery of both high and normal priority messages while charging.

If your app needs to perform heavy background work, leveraging the charging state is best practice for both user satisfaction and energy efficiency.

2. Screen On

Active usage, but with smart checks in place.

  • Jobs: Execution of background jobs is allowed, but still subject to limits based on the app’s standby bucket. Apps the system deems “frequent” get more leeway than those rarely used.
  • Alarms: Alarm limits are enforced based on both process (foreground or background) and standby bucket.
  • Network Access: Access to network resources is permitted, but could be throttled depending on standby bucket or app process state.
  • FCM: No restrictions. Both high and normal priority FCM messages are delivered without delay.

Even with the screen on, background execution is managed to prevent resource hogging but without compromising the user’s foreground tasks.

3. Screen Off & Doze Mode

Aggressive conservation to preserve battery.

  • Jobs: Execution is heavily restricted. Jobs are only permitted to run during periodic “maintenance windows” triggered by Doze. The standby bucket further dictates how much background work an app can do — a rarely used app may only get a 10-minute quota every 24 hours.
  • Alarms: Most alarms, especially regular alarms, are deferred until these maintenance windows. “While-idle” alarms are strictly limited (e.g., 7 per hour). This ensures that the device is not frequently awakened from deep sleep, maximizing battery savings.
  • Network Access: Network requests are typically deferred, especially for background apps. Only the most essential tasks will get through during Doze.
  • FCM: High priority messages are delivered immediately, bypassing Doze. Normal priority messages are deferred until the next maintenance window, so non-urgent notifications may experience some delay.

Doze mode is designed to maximize standby time without missing critical notifications or updates. Writing efficient background code means understanding and respecting these constraints.

Device State vs. Resource Limits

Device StateJobs ExecutionAlarmsNetwork AccessFirebase Cloud Messaging
ChargingNo limits (except restricted standby bucket)No execution limits (unless manually restricted)No restrictionsNo restrictions (both priorities)
Screen OnBased on standby bucketBased on process & bucketBased on process or bucketNo restrictions
Screen Off & Doze ActiveEnforced by bucket; deferred to maintenance windowDeferred; while-idle alarms: 7/hrRestricted; usually deferredHigh priority: immediate, Normal: deferred

Developer Takeaways and Best Practices

  • Schedule background-intensive work during charging: Use job scheduling APIs to detect charging state and defer heavy tasks until then.
  • Respect Doze and App Standby Buckets: Design your background operations to be efficient and infrequent, using WorkManager or JobScheduler for compatibility.
  • Use High Priority FCM judiciously: Only essential and time-sensitive notifications should be sent with high priority to respect users’ battery life.
  • User control matters: Remember, users can manually restrict battery usage for specific apps, which overrides nearly all exemptions.

Conclusion

Android’s adaptive resource limits are a cornerstone of its battery and performance management strategy. By understanding how device state influences background jobs, alarms, network access, and cloud messaging, developers can craft apps that play nicely with the system, keeping users happy and devices running longer

Building Resilient Android Apps

Building Resilient Android Apps: Surviving Doze, App Standby, and Resource Restrictions

Android power management has evolved significantly over the years. As developers, we need to design apps that are not only functional but also battery-friendly. Google introduced Doze Mode, App Standby, and various resource restrictions to extend battery life. While these features improve user experience, they can cause unexpected issues if apps aren’t built with resilience in mind.

In this guide, we’ll break down Android power management features, why they matter, and how you can build resilient Android apps that survive and thrive under these restrictions.

Why You Should Care About Android Power Management

Modern Android devices aggressively manage background processes to save battery. If your app misbehaves — draining battery or waking up the device too often — it can be throttled, delayed, or even killed. Worse case, when you might see user complaints about missed notifications or slow updates.

By understanding how Doze Mode, App Standby, and background restrictions work, you can ensure your app remains responsive while respecting battery life.

Doze Mode

Doze Mode activates when a device is idle for a while — screen off, unplugged, and stationary. Android periodically defers background CPU and network activity to preserve battery.

Key Points:

  • Your app’s background tasks get paused.
  • Network access is restricted.
  • Alarms (except AlarmManager.setExactAndAllowWhileIdle()) are deferred.

How to Handle Doze Mode Correctly:

Java
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (pm.isIgnoringBatteryOptimizations(getPackageName())) {
    // Your app is exempted from Doze (rarely recommended)
} else {
    // Use WorkManager or Firebase JobDispatcher for background tasks
}

Instead of fighting Doze, work with it. Use WorkManager for deferrable background tasks. It automatically handles Doze and other restrictions.

App Standby: What Developers Must Know

App Standby identifies apps that aren’t used frequently and restricts their background activity.

Behavior:

  • Background network access is blocked.
  • Jobs and alarms are deferred.
  • High-priority notifications still work.

Detecting App Standby Bucket:

Java
UsageStatsManager usageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
int appStandbyBucket = usageStatsManager.getAppStandbyBucket();

switch (appStandbyBucket) {
    case UsageStatsManager.STANDBY_BUCKET_ACTIVE:
        // App is active
        break;
    case UsageStatsManager.STANDBY_BUCKET_RARE:
        // App is rarely used
        break;
}

Encourage user engagement with meaningful notifications to avoid landing in the “rare” bucket.

Background Execution Limits

Starting from Android 8.0 (Oreo), background execution limits make Android power management stricter:

  • Background services can’t run freely.
  • Implicit broadcasts are restricted.

Solution: WorkManager to the Rescue

Java
WorkManager workManager = WorkManager.getInstance(context);
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MyWorker.class).build();
workManager.enqueue(workRequest);

Replace IntentService with JobIntentService or WorkManager to ensure reliability.

Optimizing Notifications Under Power Management

Notifications are crucial for engagement, but Android power management policies may delay them if improperly handled.

Best Practices:

  • Use Firebase Cloud Messaging (FCM) with high-priority messages sparingly.
  • Avoid unnecessary wake-ups; reserve high-priority FCM for time-critical updates.
  • Use NotificationManager correctly to deliver timely, non-intrusive notifications.
Java
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("Update Available")
    .setContentText("New data ready to view!")
    .setPriority(NotificationCompat.PRIORITY_HIGH);

High-priority FCM bypasses Doze but excessive usage can get your app flagged.

Avoid Common Pitfalls

Don’t abuse foreground services. They drain battery and annoy users if misused.

Don’t request battery optimization exemptions unless absolutely necessary. Google Play has strict policies and most requests get denied.

Do leverage JobScheduler, WorkManager, and FCM effectively.

Do test under real conditions. Use adb shell dumpsys deviceidle to simulate Doze Mode and check your app’s behavior.

Conclusion

Building resilient Android apps means respecting Android power management rather than working around it. Focus on:

  • Using WorkManager for background tasks.
  • Optimizing notifications.
  • Monitoring app standby behavior.

By designing apps that adapt to Android’s power-saving mechanisms, you’ll deliver reliable experiences without draining users’ batteries. 

Android Standby Bucket

Mastering Android Standby Bucket: How It Impacts Your App’s Background Activity

If you’ve noticed your Android app getting delayed push notifications, or background tasks not running as expected, the culprit could be Android Standby Bucket.

This isn’t some hidden developer setting — it’s a key part of Android’s power management system. And if you want your app to work smoothly in the background without draining battery, you need to understand how the Standby Bucket works, how it categorizes apps, and what you can do to stay on Android’s good side.

Let’s break it all down in simple way.

What Is Android Standby Bucket?

The Android Standby Bucket is a power management feature introduced in Android 9 (Pie). It groups apps into “buckets” based on how frequently the user interacts with them.

Why..? 

Because Android wants to optimize battery life. And background activity — like location updates, network calls, or jobs running silently — can suck up power fast.

So Android created a smart system that limits background access for apps the user rarely uses.

The Five Standby Buckets Explained

Here are the five standby buckets an app can fall into:

Active

  • The user is actively using the app.
  • No background restrictions.

Working Set

  • Used recently but not in the foreground now.
  • Minor restrictions apply.

Frequent

  • Used regularly but not daily.
  • Background access is more limited.

Rare

  • Used occasionally.
  • Significant background restrictions.

Restricted

  • Manually restricted or flagged by the system for battery drain.
  • Heavily limited in all background access.

Your app moves between these buckets dynamically based on user behavior — and that impacts what you can do in the background.

Why Should Developers Care?

If your app needs to do anything in the background — sync data, send reminders, update location — you must understand where your app stands in the Standby Bucket hierarchy.

Failing to adapt could mean:

  • Missed push notifications.
  • Jobs not running on time.
  • Background tasks being throttled or killed.

And ultimately, frustrated users.

How to Check Your App’s Bucket With Code

You can check which bucket your app is currently in using UsageStatsManager

Kotlin
val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val standbyBucket = usageStatsManager.appStandbyBucket

when (standbyBucket) {
    UsageStatsManager.STANDBY_BUCKET_ACTIVE -> Log.d("Bucket", "App is Active")
    UsageStatsManager.STANDBY_BUCKET_WORKING_SET -> Log.d("Bucket", "App is in Working Set")
    UsageStatsManager.STANDBY_BUCKET_FREQUENT -> Log.d("Bucket", "App is Frequent")
    UsageStatsManager.STANDBY_BUCKET_RARE -> Log.d("Bucket", "App is Rare")
    UsageStatsManager.STANDBY_BUCKET_RESTRICTED -> Log.d("Bucket", "App is Restricted")
    else -> Log.d("Bucket", "Unknown Bucket")
}

This snippet uses UsageStatsManager to get the current standby bucket. Based on that, you can log or trigger actions to adjust your app’s behavior accordingly.

How the Standby Bucket Impacts Background Activity

Here’s what each bucket means for your app’s background capabilities:

BucketBackground ExecutionJob SchedulingNetwork Access
ActiveNo restrictionsImmediateUnrestricted
Working SetMinor delaysSlight delaySlight delay
FrequentModerate limitsScheduled with delayDelayed
RareSevere limitsDeferred heavilyHeavily delayed
RestrictedBlockedBlockedBlocked

This directly impacts APIs like:

  • WorkManager
  • AlarmManager
  • JobScheduler
  • Firebase Cloud Messaging (FCM)

If you’re wondering why your background sync isn’t firing, check your bucket first.

How to Keep Your App in a Good Bucket

You can’t directly set the bucket, but you can influence it by keeping users engaged:

1. Encourage Regular Use

Design for stickiness. The more users interact with your app, the better your bucket position.

2. Send Relevant Notifications

Make sure your notifications lead to real engagement. Avoid spamming or your app could get demoted.

3. Use Foreground Services Wisely

For important tasks (like location tracking or media playback), run them in a foreground service with a visible notification.

4. Follow Background Execution Limits

Stick to Android’s guidelines. Use WorkManager for deferred tasks and ForegroundService for immediate ones.

Best Practices for Dealing With Standby Buckets

  • Test under all bucket conditions: Simulate lower buckets using ADB (see below).
  • Use JobScheduler.setRequiresDeviceIdle() carefully: It might never trigger if your app is in a low bucket.
  • Monitor your background task success rate: Adjust logic depending on current restrictions.

Simulating Buckets with ADB

You can force your app into a specific bucket for testing:

Kotlin
adb shell am set-standby-bucket com.yourapp.package rare

To reset:

Kotlin
adb shell am reset-standby-bucket com.yourapp.package

This is incredibly useful for QA and debugging.

Real-World Examples

  • Social Media Apps: Stay in Active/Working Set buckets due to frequent use, keeping messages and updates timely.
  • Fitness App Used Weekly: Dropped to Frequent or Rare, background syncs may be delayed, so design your UI to handle missing updates gracefully.
  • Single-Purpose Utility: Used once after installation, then falls to Rare or even Restricted. Background operations almost always deferred.

Conclusion

The Android Standby Bucket system is here to stay. It’s designed to protect user battery life while still allowing well-behaved apps to run efficiently.

By understanding how it works and adapting your app’s background behavior accordingly, you’ll build a better, more battery-friendly experience for your users.

Remember: apps that respect user attention and system resources will always win in the long run.

FAQs

What is Android Standby Bucket?
 It’s a power-saving feature that groups apps based on usage to limit background activity. Apps are bucketed as Active, Working Set, Frequent, Rare, or Restricted.

How does it impact apps?
 The lower the bucket, the more Android restricts background tasks, job scheduling, and network access.

How to check your app’s bucket?
 Use UsageStatsManager.appStandbyBucket to programmatically find the current bucket.

How to stay in a good bucket?
 Encourage engagement, follow background limits, and use foreground services wisely.

Android Back Stack

Android Back Stack: A Complete Guide for Modern App Navigation

Let’s talk about something that trips up a lot of Android developers — especially when building apps with complex navigation: the Android Back Stack.

You know that moment when you hit the back button and your app behaves like it has a mind of its own? Yeah, we’ve all been there. The Android Back Stack can be tricky, but once you get a handle on it, your app’s navigation feels intuitive, snappy, and exactly how users expect it to behave.

This guide breaks it down step by step, with real-world code examples, clear explanations, and some personal tips from my own development experience.

What Is the Android Back Stack, Really?

The Android Back Stack is just a managed stack (think: a vertical pile) of activities or fragments that tracks the user’s navigation history. When the user presses the back button, Android pops the top of the stack and returns to the previous screen.

Simple in theory. In practice? It gets more interesting.

Let’s start with an example.

Activities and the Back Stack

When you start a new activity using:

Kotlin
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)

Android pushes SecondActivity onto the back stack. Now, pressing the back button pops it off and returns you to MainActivity.

So far, so good.

By default, each call to startActivity() adds the new activity to the task’s back stack, unless you explicitly modify this behavior using intent flags or manifest attributes.

Customizing Back Stack Behavior with Intent Flags

You can tweak the back stack behavior with intent flags. Here are a few you’ll use often:

1. FLAG_ACTIVITY_CLEAR_TOP

Let’s say your stack looks like this: A → B → C → D

If you call startActivity() from D to go back to B with FLAG_ACTIVITY_CLEAR_TOP, Android will pop off C and D and bring B to the top.

Kotlin
val intent = Intent(this, B::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)

It’s like saying: “Hey Android, I want B on top again — clear anything above it.”

2. FLAG_ACTIVITY_NEW_TASK

This one creates a new task entirely. It’s mostly used in system-level or launcher-related contexts.

Kotlin
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)

3. FLAG_ACTIVITY_SINGLE_TOP

If the activity you’re trying to start is already at the top of the stack, don’t create a new instance — just reuse the existing one.

Kotlin
val intent = Intent(this, ProfileActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)

You’ll also need to handle this in onNewIntent() in the target activity.

Fragments and the Back Stack: The Modern Way

These days, many apps use fragments instead of spinning up new activities. That’s where things get more nuanced.

When using fragments, you manage the back stack yourself with FragmentManager. Here’s how you can add a fragment to the back stack:

Kotlin
supportFragmentManager.beginTransaction()
    .replace(R.id.fragment_container, SecondFragment())
    .addToBackStack(null)
    .commit()

Calling addToBackStack() (with either null or a string tag) adds the fragment transaction to the back stack, which means the system will remember it and can reverse the transaction—i.e., remove the newly added fragment and restore the previous one—when the back button is pressed.

If addToBackStack() is not called, the transaction is not saved to the back stack, so pressing the back button does not reverse that specific transaction.

In that case, if there are no other entries in the back stack and no other UI elements to pop, pressing the back button will exit the activity (if you’re at the top of the stack).

Pro Tip: Naming Your Back Stack Entries

You can pass a string tag to addToBackStack("tag_name") to track what’s on the stack. This helps with debugging or popping specific entries.

Kotlin
.addToBackStack("SecondFragment")

Then later:

Kotlin
supportFragmentManager.popBackStack("SecondFragment", 0)

You now have surgical control over your navigation history.

Jetpack Navigation Component: A Modern Solution

If you’re using the Jetpack Navigation Component (and you probably should be), it abstracts much of this back stack management while still giving you hooks when needed.

Kotlin
findNavController().navigate(R.id.action_home_to_detail)

And to go back:

Kotlin
findNavController().popBackStack()

The Navigation Component maintains a back stack internally and works seamlessly with the system back button and deep links. It also integrates nicely with BottomNavigationView, DrawerLayout, and more.

Common Mistakes and How to Avoid Them

Here are a few Android Back Stack missteps I’ve seen:

Mistake #1: Forgetting addToBackStack()

If you don’t add your fragment transaction to the back stack, pressing back won’t return to the previous fragment — it might just exit the app.

Mistake #2: Overusing Activities

Switching activities too often can make your app feel clunky. Stick to fragments when screens are tightly related.

Mistake #3: Ignoring Task Affinities

Sometimes, your app can get into weird states when launched from a notification or deep link. Always check if your task and back stack behave as expected.

Handling the System Back Button

You can override the back button behavior in both activities and fragments:

In an activity:

Kotlin
override fun onBackPressed() {
    // Custom logic
    super.onBackPressed()
}

In a fragment (Jetpack way):

Kotlin
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
    // Your custom back logic here
}

This gives you full control while still respecting Android’s expected navigation model.

Conclusion

A smooth, predictable back navigation experience is one of the most critical parts of mobile UX. Users expect it to “just work.”

Understanding how the Android Back Stack works — and how to tame it — gives you a major edge as a developer. Whether you’re working with activities, fragments, or the Navigation Component, mastering this system ensures your app feels polished and professional.

Paparazzi Testing Library

Paparazzi Testing Library: The Secret Weapon for Flawless UI Snapshots in Android

You build a clean UI, test it on your emulator, and everything looks solid. But once QA or users get their hands on it, layout bugs pop up — text overflowing, views misaligned, odd paddings. It happens more often than we like to admit.

That’s where the Paparazzi Testing Library proves its worth. It’s a powerful snapshot testing tool that helps you catch UI issues early — without spinning up an emulator or device. It’s fast, reliable, and runs directly on the JVM.

Here’s what makes Paparazzi a smart addition to your toolkit — and how to get started using it today.

What Is the Paparazzi Testing Library?

The Paparazzi Testing Library is a screenshot testing tool developed by Cash App (Square). It renders Android views directly on your local JVM, meaning no more waiting for Gradle to launch an emulator. You write tests that take UI snapshots and compare them to previously approved images. If something changes, you’ll know — instantly.

It’s like version control for your UI.

Why Snapshot Testing Matters

Snapshot testing is about locking down the visual representation of your UI. When your layout changes unexpectedly — due to a rogue commit, a bumped font size, or some sneaky theme refactor — snapshot tests fail, and you get a diff of what changed.

This catches visual regressions without needing a manual check.

“But isn’t this what Espresso is for?”

Nope. Espresso is great for behavior tests — clicks, scrolls, text input — but it’s not built to capture static snapshots or catch pixel-level layout issues. That’s exactly what Paparazzi is made for.

In essence:

  • Espresso = “Does the app behave correctly when a user interacts with it?”
  • Paparazzi = “Does the app look correct across different states and configurations?”

They complement each other, addressing different aspects of UI testing.

Getting Started with Paparazzi

First, you need to add the dependency to your build.gradle.kts:

Kotlin
// build.gradle.kts (module)
dependencies {
    testImplementation("app.cash.paparazzi:paparazzi:1.3.0")
}

Then, make sure your project uses AGP 7.0+. Paparazzi integrates seamlessly with Compose and traditional XML views.

Your First Snapshot Test

Let’s say you have a simple composable:

Kotlin
@Composable
fun GreetingCard(name: String) {
    Surface(color = Color.White) {
        Text(
            text = "Hello, $name..!",
            modifier = Modifier.padding(16.dp),
            fontSize = 18.sp
        )
    }
}

Here’s how you’d test it with Paparazzi:

Kotlin
class GreetingCardTest {

@get:Rule
    val paparazzi = Paparazzi()
    @Test
    fun greetingCardSnapshot() {
        paparazzi.snapshot {
            GreetingCard(name = "Shakuntala Devi")
        }
    }
}

Run the test, and boom — you get a PNG snapshot of the rendered composable. If you change the layout later and rerun the test, Paparazzi compares the new image to the baseline. Any difference? You’ll get a visual diff and a failing test.

Reviewing Changes

Let’s say your team modifies the GreetingCard to bump the padding. When you commit, the snapshot test will fail (as expected). Here’s the best part: Paparazzi gives you a side-by-side diff view of the change.

If the change is intentional, just approve the new snapshot. If not, you just caught a bug before it shipped.

Tips and Best Practices

Here are a few lessons I’ve learned the hard way:

  • Use stable fonts and themes: Dynamic styling can trigger unnecessary diffs.
  • Avoid timestamps or dynamic content in your snapshots.
  • Organize snapshots by feature to keep your test suite tidy.
  • Pair with pull requests: Have your CI pipeline run snapshot tests on every PR.

How It Works Under the Hood

Paparazzi uses RenderScript and Skia to render Android views headlessly. It builds your composables or views in a JVM environment and captures the exact pixels they produce. 

No emulator, no instrumentation, no flakiness.

When Not to Use Paparazzi

Paparazzi is not meant to test animations, gestures, or interactive flows. It shines in static UI verification. If your test involves user interaction, Espresso or Compose UI testing is your better bet.

Conclusion

If you’re serious about UI quality, the Paparazzi Testing Library is a must-have. It’s fast, consistent, and ridiculously easy to use once you get the hang of it. Plus, catching layout regressions early saves your team countless hours of QA churn and bug fixes.

Give it a try on your next UI feature. You’ll wonder how you ever lived without it.

TL;DR

  • Paparazzi = snapshot testing for Android UIs on the JVM
  • No emulator needed
  • Great for catching unexpected layout changes
  • Easy to set up and integrates with Compose
  • Ideal for static UI components
android sdk tools

Android SDK Tools: The Brains Behind Your App Development

When building Android apps, it’s easy to focus only on the code, UI, and features. But behind every successful app lies a set of powerful, low-level tools that keep the whole process moving. These are the Android SDK Tools — the unsung heroes of the Android development ecosystem.

In this post, we’ll break down what they are, how they work, and why every Android developer should understand them — even in the age of Android Studio.

What Are Android SDK Tools?

Android SDK Tools are a collection of command-line utilities that help you build, test, and debug Android apps. They used to be packaged together as “SDK Tools,” but over time, they’ve been split into modular components like:

  • platform-tools — includes ADB, fastboot, and other core tools.
  • build-tools — includes utilities like aapt, zipalign, etc.

Even though Android Studio handles much of this behind the scenes, these tools are still critical — especially when things go wrong, or when you want to automate development tasks.

Essential SDK Tools Every Android Developer Should Know

Let’s take a closer look at the key tools that power Android development under the hood:

1. ADB (Android Debug Bridge)

ADB is a command-line tool that lets your computer communicate with an Android device or emulator.

Think of it as a remote control for your Android environment. You can install apps, copy files, debug logs, and even run shell commands directly on the device.

Common ADB Commands:

ASM
adb devices             # Lists connected devices
adb install myapp.apk   # Installs an APK
adb logcat              # Displays real-time device logs

This is one of the most valuable tools in your toolbox, especially for real-time debugging.

2. fastboot

When your device is in bootloader mode, fastboot lets you flash images to the device, unlock the bootloader, and perform other low-level operations.

It’s typically used for:

  • Flashing custom recoveries or ROMs
  • Unlocking or locking bootloaders
  • Recovering bricked devices

While not every developer uses fastboot regularly, it’s indispensable for anyone working near the hardware layer or with custom builds.

3. R8 and ProGuard

Originally, ProGuard was used to shrink and obfuscate Java code in Android apps. Today, R8 has replaced it as the default tool for most modern Android projects.

R8 performs:

  • Code shrinking
  • Dead code elimination
  • Resource optimization
  • Obfuscation (to make reverse engineering harder)

R8 is built into the Android Gradle plugin, so you don’t typically run it manually — but understanding what it does can help you configure it properly in proguard-rules.pro.

Why These Tools Still Matter in 2025

Even though Android Studio and Gradle handle most of the heavy lifting today, knowing how SDK tools work gives you:

  • More control over your builds and deployments
  • Better debugging capabilities, especially on real devices
  • The ability to automate testing, CI/CD, and device management
  • Deeper insights into how Android actually works

When something breaks outside the Studio UI, these tools are often your first (and best) line of defense.

Conclusion

The Android SDK Tools may not be glamorous, but they are the engine under the hood of Android development. Whether you’re pushing your first APK or debugging a complex issue, understanding ADB, fastboot, and R8 will make you a more capable — and confident — developer.

Bundles in libs.versions.toml

Say Goodbye to Repetition: How Bundles in libs.versions.toml Simplify Android Projects

If you’re tired of repeating the same dependencies across different modules in your Android project, you’re not alone. Managing dependencies manually is error-prone, messy, and not scalable. Fortunately, Bundles in libs.versions.toml offer a clean and modern solution that saves time and reduces duplication. Let’s break it down, step by step, in a simple way. What...

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Proto DataStore

Proto DataStore in Android: How to Store Complex Objects with Protocol Buffers

Managing data on Android has evolved significantly over the years. From SharedPreferences to Room, we’ve seen the full spectrum. But when it comes to storing structured, complex data in a lightweight and efficient way, Proto DataStore steps in as a game-changer.

In this blog, we’ll walk through Proto DataStore, how it works under the hood, and how to use it with Protocol Buffers to store complex objects. We’ll also look at how it stacks up against the older SharedPreferences and why it’s the better modern choice.

Let’s break it down step by step.

What is Proto DataStore?

Proto DataStore is a Jetpack library from Google that helps you store typed objects persistently using Protocol Buffers (protobuf), a fast and efficient serialization format.

It’s:

  • Type-safe
  • Asynchronous
  • Corruption-resistant
  • Better than SharedPreferences

Unlike Preferences DataStore, which stores data in key-value pairs (similar to SharedPreferences), Proto DataStore is ideal for storing structured data models.

Why Use Proto DataStore?

Here’s why developers love Proto DataStore:

  • Strong typing — Your data models are generated and compiled, reducing runtime errors.
  • Speed — Protocol Buffers are faster and more compact than JSON or XML.
  • Safe and robust — Built-in corruption handling and data migration support.
  • Asynchronous API — Uses Kotlin coroutines and Flow, keeping your UI smooth.

Store Complex Objects with Proto DataStore

Let’s go hands-on. Suppose you want to save a user profile with fields like name, email, age, and preferences.

Step 1: Add the Dependencies

Add these to your build.gradle (app-level):

Kotlin
dependencies {
    implementation "androidx.datastore:datastore:1.1.0"
    implementation "androidx.datastore:datastore-core:1.1.0"
    implementation "com.google.protobuf:protobuf-javalite:3.25.1"
}

In your build.gradle (project-level), enable Protobuf:

Kotlin
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.1"
    }

    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java { }
            }
        }
    }
}

Also apply plugins at the top:

Kotlin
plugins {
    id 'com.google.protobuf' version '0.9.4'
    id 'kotlin-kapt'
}

Step 2: Define Your .proto File

Create a file named user.proto inside src/main/proto/:

Kotlin
syntax = "proto3";

option java_package = "com.softaai.datastore";
option java_multiple_files = true;

message UserProfile {
  string name = 1;
  string email = 2;
  int32 age = 3;
  bool isDarkMode = 4;
}

This defines a structured data model for the user profile.

Step 3: Create the Serializer

Create a Kotlin class that implements Serializer<UserProfile>:

Kotlin
object UserProfileSerializer : Serializer<UserProfile> {
    override val defaultValue: UserProfile = UserProfile.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserProfile {
        return UserProfile.parseFrom(input)
    }

    override suspend fun writeTo(t: UserProfile, output: OutputStream) {
        t.writeTo(output)
    }
}

This handles how the data is read and written to disk using protobuf.

Step 4: Initialize the Proto DataStore

Create a DataStore instance in your repository or a singleton:

Kotlin
val Context.userProfileDataStore: DataStore<UserProfile> by dataStore(
    fileName = "user_profile.pb",
    serializer = UserProfileSerializer
)

Now you can access this instance using context.userProfileDataStore.

Step 5: Read and Write Data

Here’s how you read the stored profile using Kotlin Flow:

Kotlin
val userProfileFlow: Flow<UserProfile> = context.userProfileDataStore.data

To update the profile:

Kotlin
suspend fun updateUserProfile(context: Context) {
    context.userProfileDataStore.updateData { currentProfile ->
        currentProfile.toBuilder()
            .setName("Amol Pawar")
            .setEmail("[email protected]")
            .setAge(28)
            .setIsDarkMode(true)
            .build()
    }
}

Easy, clean, and fully type-safe.

Bonus: Handling Corruption and Migration

Handle Corruption Gracefully

You can customize the corruption handler if needed:

Kotlin
val Context.safeUserProfileStore: DataStore<UserProfile> by dataStore(
    fileName = "user_profile.pb",
    serializer = UserProfileSerializer,
    corruptionHandler = ReplaceFileCorruptionHandler {
        UserProfile.getDefaultInstance()
    }
)

Migrate from SharedPreferences

If you’re switching from SharedPreferences:

Kotlin
val Context.migratedUserProfileStore: DataStore<UserProfile> by dataStore(
    fileName = "user_profile.pb",
    serializer = UserProfileSerializer,
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, "old_prefs_name"))
    }
)

When to Use Proto DataStore

Use Proto DataStore when:

  • You need to persist complex, structured data.
  • You care about performance and file size.
  • You want a modern, coroutine-based data solution.

Avoid it for relational data (instead use Room) or for simple flags (Preferences DataStore may suffice).

Conclusion

Proto DataStore is the future-forward way to store structured data in Android apps. With Protocol Buffers at its core, it combines speed, safety, and type-safety into one clean package.

Whether you’re building a user profile system, app settings, or configuration storage, Proto DataStore helps you stay efficient and future-ready.

TL;DR

Q: What is Proto DataStore in Android?
 A: Proto DataStore is a modern Jetpack library that uses Protocol Buffers to store structured, type-safe data asynchronously and persistently.

Q: How do I store complex objects using Proto DataStore?
 A: Define a .proto schema, set up a serializer, initialize the DataStore, and read/write using Flow and coroutines.

Q: Why is Proto DataStore better than SharedPreferences?
 A: It’s type-safe, faster, handles corruption, and integrates with Kotlin coroutines.

Jetpack DataStore in Android

Mastering Jetpack DataStore in Android: The Modern Replacement for SharedPreferences

If you’re still using SharedPreferences in your Android app, it’s time to move forward. Google introduced Jetpack DataStore as a modern, efficient, and fully asynchronous solution for storing key-value pairs and typed objects. In this blog, we’ll break down what Jetpack DataStore is, why it’s better than SharedPreferences, and how you can use it effectively in your Android projects.

What Is Jetpack DataStore?

Jetpack DataStore is part of Android Jetpack and is designed to store small amounts of data. It comes in two flavors:

  • Preferences DataStore — stores key-value pairs, similar to SharedPreferences.
  • Proto DataStore — stores typed objects using Protocol Buffers.

Unlike SharedPreferences, Jetpack DataStore is built on Kotlin coroutines and Flow, making it asynchronous and safe from potential ANRs (Application Not Responding errors).

Why Replace SharedPreferences?

SharedPreferences has been around for a long time but comes with some baggage:

  • Synchronous API — can block the main thread.
  • Lacks error handling — fails silently.
  • Not type-safe — you can run into ClassCastExceptions easily.

Jetpack DataStore solves all of these with:

  • Coroutine support for non-blocking IO.
  • Strong typing with Proto DataStore.
  • Built-in error handling.
  • Better consistency and reliability.

Setting Up Jetpack DataStore

To start using Jetpack DataStore, first add the required dependencies to your build.gradle:

Kotlin
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.datastore:datastore-core:1.0.0"

For Proto DataStore:

Kotlin
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.14.0"

Also, don’t forget to apply the protobuf plugin if using Proto:

Kotlin
id 'com.google.protobuf' version '0.8.12'

Using Preferences DataStore

Step 1: Create the DataStore instance

Jetpack DataStore is designed to be singleton-scoped. The recommended way is to create it as an extension property on Context:

Kotlin
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")

Here, preferencesDataStore creates a singleton DataStore instance. This ensures you have a single DataStore instance per file, avoiding memory leaks and data corruption.

Step 2: Define keys

Kotlin
val USER_NAME = stringPreferencesKey("user_name")
val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")

stringPreferencesKey and booleanPreferencesKey help define the keys.

Step 3: Write data

To write data, use the edit function, which is fully asynchronous and safe to call from any thread:

Kotlin
suspend fun saveUserData(context: Context, name: String, isLoggedIn: Boolean) {
    context.dataStore.edit { preferences ->
        preferences[USER_NAME] = name
        preferences[IS_LOGGED_IN] = isLoggedIn
    }
}

Here, edit suspends while the data is being written, ensuring no UI thread blocking.

Step 4: Read data

To read data, use Kotlin Flows, which emit updates whenever the data changes:

Kotlin
val userNameFlow: Flow<String> = context.dataStore.data
    .map { preferences ->
        preferences[USER_NAME] ?: ""
    }

Here, data is accessed reactively using Kotlin Flow, returns a Flow<String> that emits the username whenever it changes. You can collect this Flow in a coroutine or observe it in Jetpack Compose.

Real-World Use Case: User Login State

Let’s say you want to keep track of whether a user is logged in. Here’s how you do it:

Save login state:

Kotlin
suspend fun setLoginState(context: Context, isLoggedIn: Boolean) {
    context.dataStore.edit { prefs ->
        prefs[IS_LOGGED_IN] = isLoggedIn
    }
}

Observe login state:

Kotlin
val loginState: Flow<Boolean> = context.dataStore.data
    .map { prefs -> prefs[IS_LOGGED_IN] ?: false }

This setup lets your app reactively respond to changes in the login state, such as redirecting users to the login screen or the home screen.

Migrating from SharedPreferences

Jetpack DataStore makes migration easy with SharedPreferencesMigration:

Kotlin
import androidx.datastore.preferences.SharedPreferencesMigration

val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
)
  • Migration runs automatically before any DataStore access.
  • Once migrated, stop using the old SharedPreferences to avoid data inconsistency.

Using Proto DataStore (Typed Data)

Proto DataStore requires you to define a .proto schema file.

Step 1: Define the Proto schema

user_prefs.proto

Kotlin
syntax = "proto3";

option java_package = "com.softaai.sitless";
option java_multiple_files = true;

message UserPreferences {
  string user_name = 1;
  bool is_logged_in = 2;
}

Step 2: Create the serializer

Kotlin
object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return UserPreferences.parseFrom(input)
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        t.writeTo(output)
    }
}

Step 3: Initialize Proto DataStore

Kotlin
val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

Step 4: Update and read data

Kotlin
suspend fun updateUser(context: Context, name: String, isLoggedIn: Boolean) {
    context.userPreferencesStore.updateData { prefs ->
        prefs.toBuilder()
            .setUserName(name)
            .setIsLoggedIn(isLoggedIn)
            .build()
    }
}

val userNameFlow = context.userPreferencesStore.data
    .map { it.userName }

Best Practices

  • Use Proto DataStore when your data model is complex or needs strong typing.
  • Use Preferences DataStore for simple key-value storage.
  • Always handle exceptions using catch when collecting flows.
  • Avoid main-thread operations; DataStore is built for background execution.

Conclusion

Jetpack DataStore is not just a replacement for SharedPreferences; it’s an upgrade in every sense. With better performance, safety, and modern API design, it’s the future of local data storage in Android.

If you’re building a new Android app or refactoring an old one, now’s the perfect time to switch. By embracing Jetpack DataStore, you’re not only writing cleaner and safer code, but also aligning with best practices endorsed by Google.

Unit Testing in Kotlin

How to Do Unit Testing in Kotlin Like a Google Engineer

Unit testing in Kotlin isn’t just about making sure your code works. It’s about writing tests that prove your code works, stays reliable over time, and catches bugs before they hit production. Google engineers treat testing as a core development skill, not an afterthought. And you can do the same.

In this guide, we’ll break down unit testing in Kotlin in a simple way. We’ll show you how to write clean, maintainable tests just like a pro. Whether you’re building Android apps or server-side Kotlin applications, this blog will give you the confidence to write bulletproof unit tests.

What is Unit Testing in Kotlin?

Unit testing is the process of testing individual units of code (like functions or classes) in isolation to ensure they work as expected. Unlike integration or UI tests, unit tests focus on your own logic, not external libraries or frameworks.

Unit Test is a piece of code that is not a part of your application. It can create and call all of your application’s public classes and methods… You want to verify whether application code works as you expect.

Why Google Engineers Prioritize Unit Testing

  • Fast feedback: Tests run in milliseconds. You catch bugs fast.
  • Safe refactoring: When you change code, tests confirm nothing breaks.
  • Confidence in deployment: You ship faster because you trust your code.
  • Documents behavior: Tests show how code is supposed to work.

Now let’s get to the fun part — how to actually do this in Kotlin.

Setting Up Unit Testing in Kotlin

Most Kotlin projects use JUnit as the test framework. Android Studio and IntelliJ IDEA make setup easy:

1. Add JUnit to your project dependencies (usually already included in Android projects). Use JUnit5 for unit testing in Kotlin. It’s modern, fast, and integrates well.

Kotlin
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testImplementation("org.jetbrains.kotlin:kotlin-test:1.9.0")
}

test {
    useJUnitPlatform()
}

2. Create a test class for the code you want to test.

3. Write test methods using the @Test annotation.

Basic Unit Test in Kotlin

Let’s say you have a function that adds two numbers:

Kotlin
fun add(a: Int, b: Int): Int = a + b

Here’s how you write a test for it:

Kotlin
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

class MathUtilsTest {
    @Test
    fun `add should return sum of two numbers`() {
        val result = add(3, 4)
        assertEquals(7, result)
    }
}

What’s happening here?

  • @Test marks the method as a test case.
  • assertEquals checks the expected and actual values.
  • The function name is written in backticks for clarity.

Best Practices for Unit Testing in Kotlin

Google engineers follow these principles to ensure effective unit testing in Kotlin:

1. Keep Tests Small and Focused

Each test should verify one behavior or scenario. This makes tests easy to read and maintain.

2. Use Immutable Test Data

Initialize objects as val and avoid mutating shared state between tests. This prevents flaky tests and makes debugging easier.

3. Leverage Kotlin Features

Kotlin’s concise syntax (like data classes and extension functions) makes tests more readable and expressive.

4. Test Lifecycle Annotations

  • @Before: Setup code before each test.
  • @After: Cleanup after each test.
  • @TestInstance(Lifecycle.PER_CLASS): Reuse test class instance for all tests (avoids static members).

5. Mock Dependencies

Use libraries like MockK or Mockito to replace dependencies with mock objects, so you only test your own code’s logic.

Testing with Mocks in Kotlin

Sometimes, your code depends on external systems (like APIs or databases). For true unit testing in Kotlin, you should mock those dependencies.

Use MockK — a Kotlin-first mocking library.

Add MockK to Your Project

Kotlin
dependencies {
    testImplementation("io.mockk:mockk:1.13.8")
}

Example with MockK

Kotlin
interface UserRepository {
    fun getUser(id: Int): String
}

class UserService(private val repository: UserRepository) {
    fun getUsername(id: Int): String = repository.getUser(id).uppercase()
}

class UserServiceTest {
    private val repository = mockk<UserRepository>()
    private val service = UserService(repository)
    @Test
    fun `getUsername returns uppercased username`() {
        every { repository.getUser(1) } returns "amol"
        val result = service.getUsername(1)
        assertEquals("AMOL", result)
    }
}

Key Points

  • mockk<UserRepository>() creates a mock object.
  • every { ... } returns ... defines behavior for the mock.
  • Test checks the result of the service method in isolation.

Testing Coroutines in Kotlin

Kotlin’s coroutines make asynchronous code easier, but they require special handling in tests.

Example: Testing a Coroutine Function

Suppose you have:

Kotlin
suspend fun fetchData(): String {
    delay(1000) // Simulate network call
    return "Data"
}

Test with runBlocking:

Kotlin
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.Assert.assertEquals

class DataFetchTest {
    @Test
    fun fetchDataReturnsCorrectValue() = runBlocking {
        val result = fetchData()
        assertEquals("Data", result)
    }
}

Tips:

  • Use runBlocking to execute suspending functions in tests.
  • For more advanced coroutine testing, use CoroutineTestRule and TestCoroutineDispatcher to control coroutine execution and skip delays.

Running and Maintaining Tests

  • Run tests frequently: Use your IDE or command line to run all tests after every change.
  • Fix failing tests immediately: Don’t ignore red tests.
  • Refactor tests as needed: Keep them clean and up-to-date as your code evolves.

Tips for Writing Great Unit Tests

  1. Name tests clearly: Describe what the test checks.
  2. Test one thing at a time: Keep tests focused.
  3. Keep tests fast: No real network/database.
  4. Avoid logic in tests: Use literal values.
  5. Use setup methods for repetitive boilerplate.

Common Mistakes to Avoid

  • Testing too much in one test
  • Using real APIs in unit tests
  • Not asserting outcomes
  • Ignoring failed tests
  • Skipping tests because “it works on my machine

Conclusion

Unit Testing in Kotlin isn’t just for Google engineers — it’s a superpower for every developer. By writing small, focused tests, leveraging Kotlin’s features, and using the right tools, you’ll catch bugs early and build robust applications with confidence. 

Start small, keep practicing, and soon unit testing will be second nature..!

error: Content is protected !!