Amol Pawar

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.

What Is Liquibase

What Is Liquibase? A Complete Guide to Database Change Management (2025)

Let me guess. You’ve nailed your application code, CI/CD pipelines are humming, deployments are smooth… until it comes to the database. Suddenly, things get messy. Manual SQL scripts, environment inconsistencies, and mystery errors that only appear in prod. 

Sound familiar..?

That’s where Liquibase comes in.

What Is Liquibase?

Liquibase is an open-source database change management tool. Think of it like version control for your database. Just like Git tracks changes to your code, Liquibase tracks changes to your database schema and ensures those changes are applied safely, consistently, and automatically across environments.

It’s used by developers, DBAs, and DevOps teams to make database changes as agile, traceable, and reliable as code deployments.

Why You Should Care About Database Change Management

If you’re still shipping database changes by emailing SQL files around or copy-pasting commands into a terminal, it’s time for an upgrade.

Database change management matters because:

  • Manual scripts are error-prone
  • Rollback is painful or non-existent
  • Deployments become brittle and unpredictable
  • Audit and compliance? Forget about it

Liquibase solves all of this by bringing structure, automation, and traceability.

The Basics (How It Works)

Liquibase uses changelogs, which are XML, YAML, JSON, or SQL files that define what changes should happen to the database. Each change is a changeset.

Here’s a simple YAML changelog example:

Bash
# db-changelog.yaml

databaseChangeLog:
  - changeSet:
      id: 1
      author: amoljp19
      changes:
        - createTable:
            tableName: user
            columns:
              - column:
                  name: id
                  type: int
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: username
                  type: varchar(50)
              - column:
                  name: email
                  type: varchar(100)

Here,

  • Creates a table named user
  • Adds id, username, and email columns
  • Sets id as the primary key

You run it with a command like:

Bash
liquibase --changeLogFile=db-changelog.yaml update

Liquibase will:

  1. Check which changesets have already been run (via a tracking table in your DB)
  2. Run only the new changes
  3. Mark them as completed

Boom. Your DB schema evolves, with no guesswork.

A Real-World Example

Let’s say you want to add a new created_at timestamp column to the user table. Here’s how you’d do it:

YAML
- changeSet:
    id: 2
    author: amoljp19
    changes:
      - addColumn:
          tableName: user
          columns:
            - column:
                name: created_at
                type: timestamp
                defaultValueComputed: CURRENT_TIMESTAMP

Rerun the update command and Liquibase will apply just this new changeset. It’s smart enough to skip anything already applied.

Supported Databases and Formats

Liquibase supports all major relational databases:

  • PostgreSQL
  • MySQL
  • Oracle
  • SQL Server
  • SQLite
  • H2 (for testing)

And you can write changelogs in:

  • YAML (clean and human-readable)
  • XML (verbose but flexible)
  • JSON (for programmatic use)
  • SQL (if you prefer writing raw SQL with comments)

Integration with CI/CD Pipelines

Liquibase plays nicely with Jenkins, GitHub Actions, GitLab CI, Azure DevOps, and other automation tools. You can run it as part of your deployment pipeline to ensure database changes are always in sync with your application code.

Here’s a basic example using GitHub Actions:

YAML
jobs:
  db-update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Liquibase
        run: |
          liquibase --changeLogFile=db-changelog.yaml \
                    --url=jdbc:postgresql://dbhost:5432/mydb \
                    --username=amoljp \
                    --password=amoljp \
                    update

Rollbacks? Handled.

Every changeset can include a rollback section. Here’s an example:

YAML
- changeSet:
    id: 3
    author: amoljp19
    changes:
      - dropColumn:
          columnName: created_at
          tableName: user
    rollback:
      - addColumn:
          tableName: user
          columns:
            - column:
                name: created_at
                type: timestamp

Want to undo the last change? Run:

Bash
liquibase rollbackCount 1

And just like that, it rolls back one changeset.

Best Practices (2025 Edition)

  1. One change per changeset — Easier to track and rollback.
  2. Use YAML or XML — Cleaner than SQL for most cases.
  3. Version your changelogs in Git — Keep DB and code in sync.
  4. Automate in CI/CD — Manual updates are error magnets.
  5. Test migrations locally — Don’t push straight to prod.

Conclusion

If your database changes are becoming a bottleneck or source of bugs, it’s time to look at Liquibase. It brings the discipline of version control, the safety of rollbacks, and the power of automation to your database.

It’s not just for big teams or enterprises. Even solo developers can benefit from Liquibase by avoiding “it works on my machine” database issues.

In 2025, if you’re not managing your database like code, you’re asking for trouble. Liquibase is your first step toward making database deployments boring, in the best possible way.

Reverse Engineering

What Is Reverse Engineering? Explained: From Concept to Code

Have you ever looked at a finished gadget, app, or piece of code and thought, “How the heck did they build this?” That’s exactly where Reverse Engineering comes in — it’s like digital archaeology for modern tech. Whether you’re a curious developer, cybersecurity enthusiast, or just someone who loves figuring things out, reverse engineering is a fascinating skill to explore.

In this post, we’ll break it all down — from the concept of reverse engineering to real code examples — so you walk away not only knowing what it is, but how to start doing it.

What Exactly Is Reverse Engineering?

At its core, Reverse Engineering is the process of taking something apart to understand how it works — then documenting, modifying, or improving it. While it originally came from mechanical engineering (think tearing down an engine), today it’s widely used in software, cybersecurity, game modding, and even competitive hacking (CTFs).

Imagine you have a compiled program, but no access to the source code. Reverse engineering lets you peel back the layers to uncover the logic, data structures, and behavior hidden inside.

Why Is Reverse Engineering Useful?

Here are a few real-world reasons people dive into reverse engineering:

  • Security research: Find vulnerabilities in apps and systems.
  • Legacy systems: Understand undocumented software to maintain or upgrade it.
  • Malware analysis: Dissect viruses or ransomware to see how they work.
  • Compatibility: Make old software work on new platforms.
  • Learning: Understand how advanced systems are built — great for self-teaching..!

How Does Reverse Engineering Work?

Let’s look at a simplified breakdown of the process:

  1. Observation: Run the program and see what it does.
  2. Disassembly: Use tools to view the compiled binary code (machine language).
  3. Decompilation: Convert low-level code back into a higher-level approximation.
  4. Analysis: Understand data structures, logic flow, and algorithms.
  5. Modification (optional but not recommended): Patch, bypass, or improve the code, but be aware that doing so could violate legal restrictions or terms of service. Proceed with caution.

Types of Reverse Engineering

Let’s split this into two main categories: hardware and software.

Hardware Reverse Engineering

This often involves examining physical components — like circuit boards or mechanical parts. Engineers may take high-resolution images, use 3D scanning, or map out circuitry by hand.

Example: If a critical component in a legacy machine fails, and the manufacturer no longer exists, reverse engineering helps recreate or replace that part.

Software Reverse Engineering

This can be broken into two techniques:

1. Static Analysis

You inspect the software without running it. This involves:

  • Looking at the binary or compiled code
  • Using tools like Ghidra or IDA Free to decompile code into something readable
  • Understanding function names, variables, and logic flow

2. Dynamic Analysis

Here, you run the software and monitor what it does. Tools like OllyDbg, x64dbg, or Wireshark let you:

  • Set breakpoints
  • Watch memory changes
  • Analyze system calls or network activity

Common Tools for Reverse Engineering

Before we jump into code, here are a few tools you’ll often see in reverse engineering:

  • IDA Pro / Ghidra — Disassemblers that help you analyze binaries.
  • x64dbg / OllyDbg — Debuggers for Windows.
  • Radare2 / Cutter — Open-source reverse engineering frameworks.
  • Wireshark — For network traffic inspection.
  • Hex-Rays Decompiler — Converts assembly to pseudocode.

Real-World Example: Code Deconstruction

Let’s say you find a mysterious binary function. After decompiling, you see this assembly code:

ASM
push ebp
mov  ebp, esp
mov  eax, [ebp+8]
add  eax, 5
pop  ebp
ret

Even if you’re not a pro, this pattern is pretty straightforward. Here’s how it works:

  • push ebp / mov ebp, esp: standard setup for a function
  • mov eax, [ebp+8]: grabs the first argument passed to the function
  • add eax, 5: adds 5 to it
  • ret: returns the result

This is likely the compiled version of:

C
int addFive(int x) {
    return x + 5;
}

That’s reverse engineering — working backwards from machine instructions to human-readable logic.

Is Reverse Engineering Legal?

Good question..! The answer isn’t black and white — it largely depends on what you’re doing and where you’re doing it.

If you’re reverse engineering for educational purposes or security research — and not distributing pirated software or stolen code — you’re likely in the clear.

Usually allowed:

  • Security research
  • Interoperability (e.g., making software compatible)
  • Personal use (e.g., restoring old hardware/software you own)

Usually restricted or illegal:

  • Circumventing DRM or copy protection
  • Repackaging and reselling proprietary software or designs
  • Hacking for unauthorized access

Always read license agreements and check local laws carefully before diving in.

 Tips for Getting Started

  • Start small: Pick tiny programs you wrote yourself to disassemble.
  • Practice with CTFs: Platforms like Hack The Box and picoCTF are great.
  • Read reverse engineering write-ups: Learn from real-world examples.
  • Keep learning assembly: It’s the backbone of all binary analysis.
  • Don’t get discouraged: It’s tough at first, but insanely rewarding.

Conclusion

Reverse Engineering isn’t just for hackers in hoodies — it’s a powerful way to understand, learn, and even protect software systems. Whether you’re analyzing malware, figuring out a legacy application, or just learning how binaries work, this skill puts you in control of what’s normally a black box.

By starting small, using the right tools, and staying curious, you can turn the mysterious world of compiled code into something you can read, modify, and even improve.

So next time you encounter an executable and wonder what’s inside, fire up your debugger and take a peek — you might just discover something amazing.

TL;DR: What Is Reverse Engineering?

  • Reverse engineering is the process of analyzing software (or hardware) to understand how it works.
  • It’s widely used in security research, malware analysis, and legacy software support.
  • You can start with simple tools like strings, objdump, and Ghidra.
  • It’s legal in many cases — especially for educational or research purposes.
  • Start small, stay curious, and practice often.

Happy reversing..! 🕵️‍♂️💻

Ransomware 101

Ransomware 101: Everything You Need to Know to Stay Protected

Let’s talk about something that’s become way too common: ransomware. If you’ve never heard of it before, or if you’ve heard the word but aren’t exactly sure what it means, don’t worry — you’re not alone. I wrote this guide to give you the real-world, no-BS breakdown of what ransomware is, how it spreads, and what you can do to protect yourself. Whether you’re running a business or just trying to keep your personal laptop safe, this is for you.

What Is Ransomware, Really?

Ransomware is a type of malicious software (malware) that locks you out of your files or entire system until you pay a ransom. It’s like a digital hostage situation. The attacker usually demands payment in cryptocurrency (like Bitcoin) because it’s harder to trace.

Once it gets into your system, it starts encrypting your files — basically scrambling them so you can’t open anything. Then it flashes a message on your screen saying something like, 

Your files are locked. Pay us $500 in Bitcoin or lose everything.

And here’s the kicker: even if you pay, there’s no guarantee you’ll get your files back.

How Does It Spread?

Ransomware doesn’t just fall from the sky. It usually sneaks in through one of these methods:

  • Phishing Emails: You get an email that looks legit — maybe from your bank or a coworker — with a link or attachment. One click, and boom, you’re infected.
  • Malicious Websites: Sometimes just visiting a shady site can trigger a download in the background.
  • Software Vulnerabilities: Outdated software (especially operating systems or web browsers) can have security holes that ransomware exploits.
  • Compromised USB Drives: Yes, even plugging in an infected USB can do the trick.

Real Talk: Why Ransomware Is a Big Deal

This isn’t just a problem for big companies. Ransomware hits schools, hospitals, local governments, and regular people every day. Some folks lose precious family photos, years of work, or personal records. For businesses, downtime can cost thousands — or millions.

What’s worse, some newer strains of ransomware not only encrypt your files but also threaten to leak them online if you don’t pay. That’s a double whammy.

How to Protect Yourself from Ransomware

Alright, now that we’ve covered the scary part, here’s the good news: you can protect yourself. Here are the essentials:

1. Backup Everything. Regularly.

Make backing up your files a habit. Use an external hard drive or cloud storage (ideally both). If ransomware hits and you have clean backups, you can just wipe your system and restore your stuff.

2. Keep Your Software Updated

Updates aren’t just annoying popups — they fix vulnerabilities that attackers exploit. Turn on automatic updates for your operating system, antivirus, browsers, and any other key software.

3. Use Strong Antivirus & Anti-Malware Tools

Make sure you have a solid antivirus program running. Windows Defender is decent, but for extra peace of mind, consider additional tools like Malwarebytes.

4. Learn to Spot Phishing Emails

If an email seems off, don’t click anything. Look for misspellings, weird addresses, and urgent language. Hover over links before clicking to see where they actually lead.

5. Enable Ransomware Protection (Windows 10/11)

Did you know Windows has built-in ransomware protection?

Windows 10/11 Protection

Here’s how to enable it:

Kotlin
1. Open "Windows Security"
2. Click on "Virus & threat protection"
3. Scroll down to "Ransomware protection"
4. Click "Manage ransomware protection"
5. Turn on "Controlled folder access"

This feature blocks unauthorized apps from accessing important folders.

6. Use Multi-Factor Authentication (MFA)

If someone steals your password, MFA can still block them. It’s a simple way to add a serious layer of protection.

What to Do If You Get Hit

First: Don’t pay the ransom. Paying doesn’t guarantee your files will be restored, and it just funds more attacks.

Here’s what to do:

  • Disconnect from the internet to stop the ransomware from spreading.
  • Scan your system with antivirus/malware tools to identify and remove the infection.
  • Restore from backups if you have them.
  • Report the incident to local authorities or a cybercrime unit.

If you’re stuck and need help, look into organizations like No More Ransom (nomoreransom.org). They offer free decryption tools for certain types of ransomware.

Conclusion

Ransomware isn’t going away anytime soon, but that doesn’t mean you have to live in fear. By understanding how it works and taking some basic steps, you can avoid becoming a victim.

If there’s one takeaway from this post, it’s this: Backup your data today. Seriously. Do it now.

Stay safe out there..! 💻🔒

What Is Selenium

What Is Selenium? A Beginner’s Guide to the #1 Web Testing Tool

What Is Selenium?

Selenium is a free, open-source framework used to automate web browsers. Developers and QA engineers rely on it to:

  • Automate testing of web applications
  • Simulate real user interactions (clicks, typing, navigation)
  • Support multiple languages (Python, Java, JavaScript, C#, Ruby)

Originally created in 2004, Selenium has evolved into the industry-standard tool for browser-based testing. It’s flexible, powerful, and backed by a strong community.

Why Use Selenium?

  1. Cross‑Browser Testing
     Run tests on Chrome, Firefox, Safari, Edge, and more, ensuring consistent behavior across platforms.
  2. Supports Multiple Languages
     Write your tests in the language you love — be it Python, JavaScript, or others.
  3. Community & Ecosystem
     Rich support from blogs, plugins, tutorials, and extensions.
  4. Scalability
     Use Selenium Grid or cloud platforms like Sauce Labs to run tests in parallel.

Core Components of Selenium

Selenium consists of several key parts:

  • Selenium WebDriver: Main tool for controlling browsers.
  • Selenium IDE: Chrome/Firefox extension for record-and-playback testing.
  • Selenium Grid: Enables remote and parallel test execution.

The primary focus here is WebDriver, which interacts with the browser by simulating mouse movements, clicks, form entries, and more. Let’s explore a basic example.

Getting Started with Selenium in Python

Step 1: Install Selenium

Python
pip install selenium

You also need a WebDriver executable for your browser (e.g., chromedriver for Chrome).

Step 2: Write Your First Test

Create a file named test_google_search.py:

Python
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

# 1. Launch browser
driver = webdriver.Chrome()

try:
    # 2. Go to Google
    driver.get('https://www.google.com')

    # 3. Locate search box
    search_box = driver.find_element('name', 'q') 

    # 4. Type and press Enter
    search_box.send_keys('Selenium testing')
    search_box.send_keys(Keys.RETURN)

    # 5. Print title
    print("Page title is:", driver.title)

finally:
    # 6. Close browser
    driver.quit()

What’s happening?

  1. Importing modules — we bring in webdriver and Keys for browser control and keyboard interaction.
  2. driver = webdriver.Chrome() — opens a Chrome session via the WebDriver executable.
  3. .get() — navigates to the target URL.
  4. find_element — locates the search input using its name attribute.
  5. send_keys() — simulates typing and pressing Enter.
  6. driver.title — fetches the current page title.
  7. finally: driver.quit() — guarantees the browser closes even if errors occur.

Expanding the Example: Assertion & Cleanup

Let’s assert that the title contains “Selenium”:

Python
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome()
driver.get('https://www.google.com')
search_box = driver.find_element('name', 'q')
search_box.send_keys('Selenium testing')
search_box.send_keys(Keys.RETURN)

assert 'Selenium' in driver.title, "Selenium not found in title"
print("Test passed. Title contains 'Selenium'")

driver.quit()
  • assert statement verifies expected behavior.
  • Cleaner flow without try/finally, but you’ll want try/finally in real-world tests for safety.

Tips for Clean Selenium Code

1. Use explicit waits, not time.sleep, to wait for page elements:

Python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.NAME, 'q')))

2. Use Page Object Model (POM) to organize locators and actions into classes.

3. Parameterize tests (e.g., search terms) to reuse code.

4. Log actions and screenshots for easier debugging.

Advanced Features & Ecosystem

  • Selenium Grid: Run tests in parallel on multiple browsers/OS combos.
  • Headless Mode: Use headless browsers to save resources.
  • Cloud Integration: Services like BrowserStack and Sauce Labs support Selenium out of the box.
  • Extensions: Community libraries like pytest-selenium and selenium-page-factory help structure and scale tests.

Is Selenium Right for You?

Use selenium if you need to:

  • Automate browser tasks
  • Test cross-browser web apps
  • Use code-based testing with real user actions

Avoid or complement selenium if:

  • You need non-UI tests (unit tests, API tests) → use pytest, requests, etc.
  • You need visual regression testing → use tools like Applitools.
  • You’re testing mobile apps exclusively → Appium would be better.

Your Next Steps

  1. Install WebDriver for your browser and run the sample script.
  2. Add assertions and waits to make tests robust.
  3. Explore pytest or unittest integration for test suites.
  4. Try out Selenium Grid or cloud services for large-scale testing.

Conclusion

Selenium is a powerful, established tool that helps you automate web browsers exactly how a user would interact with them. With support for multiple languages, browsers, and platforms, it’s an essential component in web test automation. By following clean code practices, using waits, and organizing your tests, you’ll master selenium quickly — and write reliable, maintainable tests.

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 Is libs.versions.toml?

Starting with Gradle 7 and Android Gradle Plugin 7+, Google introduced Version Catalogs — a new way to centralize and manage dependencies. Instead of scattering dependency strings across multiple build.gradle files, you can now define everything in a single place: libs.versions.toml.

This TOML (Tom’s Obvious Minimal Language) file lives in your project’s gradle folder and acts as your master dependency list.

Here’s what a basic libs.versions.toml file looks like:

Kotlin
[versions]
kotlin = "1.9.0"
coroutines = "1.7.1"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

That’s great — but what if you’re using the same group of libraries in every module? Writing them out repeatedly is a waste of time. That’s where Bundles in libs.versions.toml come to the rescue.

What Are Bundles?

Bundles are a feature of version catalogs that let you group related dependencies under a single name. Think of them like playlists for your libraries. Instead of referencing each dependency one by one, you just call the bundle, and you’re done.

Why Use Bundles?

  • Clean, organized code
  • No repeated dependencies
  • Easy updates across modules
  • Better modularization

How to Create a Bundle in libs.versions.toml

Let’s say you’re using multiple Jetpack Compose libraries across several modules. Without bundles, you’d need to add each one like this:

Kotlin
implementation(libs.compose.ui)
implementation(libs.compose.material)
implementation(libs.compose.tooling)

With Bundles in libs.versions.toml, you can simplify it like this:

Step 1: Define the Libraries

Kotlin
[versions]
compose = "1.5.0"

[libraries]
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }

Step 2: Create a Bundle

Kotlin
[bundles]
compose = ["compose-ui", "compose-material", "compose-tooling"]

How to Use Bundles in build.gradle.kts

In your module’s build.gradle.kts file, just add:

Kotlin
implementation(libs.bundles.compose)

That one-liner brings in all the Compose dependencies you need. Clean, right?

Real-World Use Case: Networking Stack

Let’s say you always use Retrofit, Moshi, and OkHttp in your data modules. Define a bundle like this:

Kotlin
[versions]
retrofit = "2.9.0"
moshi = "1.13.0"
okhttp = "4.10.0"

[libraries]
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

[bundles]
networking = ["retrofit-core", "moshi-core", "okhttp-core"]

Then in your module:

Kotlin
implementation(libs.bundles.networking)

You’ve just replaced three lines with one — and centralized version control in the process.

Common Mistakes to Avoid

  • Wrong syntax: The bundle array must reference exact keys from the [libraries] block.
  • Missing versions: Always define versions under [versions] and refer using version.ref.
  • Not reusing bundles: If two modules share the same libraries, don’t duplicate — bundle them.

Why Bundles in libs.versions.toml Matter for Android Developers

Bundles in libs.versions.toml are more than just a convenience—they’re a best practice. They improve your project structure, reduce maintenance overhead, and make scaling a breeze. Whether you’re working solo or on a large team, bundling dependencies is the smart way to manage complexity.

If you’re building modular Android apps (and let’s face it, who isn’t in 2025?), adopting bundles is a no-brainer.

Conclusion

The old way of managing dependencies is clunky and outdated. With Bundles in libs.versions.toml, you can streamline your workflow, stay DRY (Don’t Repeat Yourself), and future-proof your project.

Say goodbye to repetitive implementation lines and hello to clean, maintainable build scripts.

Start bundling today — and give your Android project the structure it deserves.

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.

error: Content is protected !!