Showing posts with label Interview QA. Show all posts
Showing posts with label Interview QA. Show all posts

Common Scenarios & Examples of Coroutine Memory Leaks

 What is a Memory Leak?

A memory leak occurs when memory that is no longer needed is not released because the program still holds references to it. In Android, this means the garbage collector (GC) cannot reclaim that memory, potentially leading to OutOfMemoryError and app crashes.


 How Do Memory Leaks Occur in a Coroutine?

Coroutines are lightweight threads, but if not scoped and managed correctly, they can continue running even when the component (like an Activity or ViewModel) is destroyed. This can lead to memory leaks.





 Common Scenarios & Examples of Coroutine Memory Leaks

Here are some common scenarios where memory leaks can happen with coroutines, what causes them, what happens afterward, and how to fix them:


1. Coroutine Scope Tied to a Destroyed Component

❌ Problem:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            delay(10_000) // 10-second delay
            Log.d("Coroutine", "Finished!")
        }
    }
}

 What Happens:

Even if the user rotates the screen or navigates away, the coroutine keeps running unless cancelled. If it references this, the Activity is retained in memory.

✅ Fix:

Use a lifecycle-aware scope, like lifecycleScope, and ensure the task is cancellable:

lifecycleScope.launch {
    withTimeoutOrNull(5000) {
        delay(10_000)
    }
}

2. GlobalScope Misuse

❌ Problem:

GlobalScope.launch {
    val bitmap = loadLargeBitmap()
    imageView.setImageBitmap(bitmap)
}

 What Happens:

  • GlobalScope lives for the entire app lifecycle.

  • If imageView belongs to a destroyed Activity, it is retained in memory.

✅ Fix:

Use viewModelScope, lifecycleScope, or a custom CoroutineScope that is cancelled appropriately:

class MyViewModel : ViewModel() {
    fun loadImage() {
        viewModelScope.launch {
            val bitmap = loadLargeBitmap()
            _bitmapLiveData.value = bitmap
        }
    }
}

3. ViewModel or Activity Holds Long-lived Coroutine with UI Reference

❌ Problem:

class MyViewModel : ViewModel() {
    var activity: MainActivity? = null

    fun doWork() {
        viewModelScope.launch {
            delay(10_000)
            activity?.updateUI() // Leaks MainActivity
        }
    }
}

 What Happens:

Even after MainActivity is destroyed, the coroutine keeps a reference to it via activity.

✅ Fix:

Avoid passing UI references. Use LiveData, StateFlow, or a callback interface that is weakly referenced.


4. Job not Cancelled in onDestroy()

❌ Problem:

class MyActivity : AppCompatActivity() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        scope.launch {
            fetchData()
        }
    }

    //  Missing job.cancel()
}

 What Happens:

The coroutine continues running even after the Activity is destroyed.

✅ Fix:

Cancel the job:

override fun onDestroy() {
    super.onDestroy()
    job.cancel()
}

5. Flow Collection Without Proper Scope

❌ Problem:

viewModel.dataFlow.onEach {
    textView.text = it
}.launchIn(GlobalScope) //  WRONG

 What Happens:

This will outlive the lifecycle of the view that textView belongs to.

✅ Fix:

viewModel.dataFlow
    .onEach { textView.text = it }
    .launchIn(lifecycleScope)

Or use:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.dataFlow.collect {
            textView.text = it
        }
    }
}

 Best Practices to Avoid Coroutine Memory Leaks

 Practice  Explanation
Use viewModelScope, lifecycleScope These scopes are lifecycle-aware and cancel automatically.
Avoid GlobalScope for UI tasks GlobalScope never cancels, leading to leaks.
Always cancel custom scopes Manually call job.cancel() in onDestroy() or similar.
Avoid long-lived references to Context/UI Don’t store Activities/Views inside ViewModels or background coroutines.
Use structured concurrency Always launch coroutines inside a well-defined scope.
Make coroutines cancellable Use withTimeout, isActive, and ensure long-running tasks honor cancellation.
Use repeatOnLifecycle for Flow Ensures collection only happens during active lifecycle state.

 Use Case Example: Fetch User Profile and Update UI

❌ Incorrect (Leaky)

class ProfileActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GlobalScope.launch {
            val user = fetchUserProfile()
            runOnUiThread {
                profileTextView.text = user.name
            }
        }
    }
}

✅ Correct (Leak-safe)

class ProfileActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            val user = fetchUserProfile()
            profileTextView.text = user.name
        }
    }
}

 Summary

πŸ›‘ Don't Do This ✅ Do This Instead
Launch coroutine in GlobalScope Use viewModelScope or lifecycleScope
Hold references to Activity/View Use LiveData/StateFlow for updates
Ignore coroutine cancellation Make coroutine cancellable
Launch coroutine in onCreate w/o guard Use repeatOnLifecycle or cancel scope

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

Happy coding! πŸ’»✨

Implementing an LRU Cache in Android using Kotlin and Jetpack Compose

In Android development, caching is a critical technique for storing data locally for quicker access, reducing network calls, and enhancing the user experience. One popular caching technique is the Least Recently Used (LRU) cache, which automatically evicts the least recently accessed data when the cache reaches its limit. This post explores how to implement an LRU cache in Android using Kotlin and Jetpack Compose.


What is an LRU Cache?

Least Recently Used (LRU) is a caching strategy that removes the least recently accessed items first when the cache exceeds its defined limit. It's optimal when you want to cache a limited amount of data, such as images, objects, or API responses, and ensure only frequently used items remain in memory.

Benefits:

  • Reduces memory footprint

  • Speeds up data retrieval

  • Avoids unnecessary API calls or file reads

  • Automatically manages cache eviction


Use Cases of LRU Cache in Android

Use Case Description
Image Caching Store bitmap images fetched from the network to avoid repeated downloads
Network Responses Save HTTP responses (e.g., user profile, dashboard data) to speed up load times
File or Object Caching Cache local files or data models in memory for quick reuse
Custom In-App Caching Temporary store for autocomplete suggestions, search history, etc.

 Implementing LRU Cache in Kotlin (Without External Libraries)

Step 1: Create an LRU Cache Manager

class LruCacheManager<K, V>(maxSize: Int) {
    private val cache = object : LruCache<K, V>(maxSize) {
        override fun sizeOf(key: K, value: V): Int {
            return 1 // Customize this if needed (e.g., for Bitmaps)
        }
    }

    fun put(key: K, value: V) {
        cache.put(key, value)
    }

    fun get(key: K): V? {
        return cache.get(key)
    }

    fun evictAll() {
        cache.evictAll()
    }
}

☝️ Note: This uses Android’s built-in LruCache<K, V> from android.util.LruCache.


Example: Caching User Profiles

Let’s say you’re loading user profiles from an API, and want to cache them in memory to avoid reloading them repeatedly.

Step 2: Define a simple data model

data class UserProfile(val id: Int, val name: String, val avatarUrl: String)

Step 3: Create a ViewModel using StateFlow and LruCache

class UserProfileViewModel : ViewModel() {
    private val cache = LruCacheManager<Int, UserProfile>(maxSize = 5)

    private val _userProfile = MutableStateFlow<UserProfile?>(null)
    val userProfile: StateFlow<UserProfile?> = _userProfile

    fun loadUserProfile(userId: Int) {
        cache.get(userId)?.let {
            _userProfile.value = it
            return
        }

        viewModelScope.launch {
            // Simulate network call
            delay(1000)
            val user = UserProfile(userId, "User $userId", "https://picsum.photos/200/200?random=$userId")
            cache.put(userId, user)
            _userProfile.value = user
        }
    }
}

Jetpack Compose UI

@Composable
fun UserProfileScreen(viewModel: UserProfileViewModel = viewModel()) {
    val profile by viewModel.userProfile.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { viewModel.loadUserProfile((1..10).random()) }) {
            Text("Load Random User")
        }

        Spacer(modifier = Modifier.height(20.dp))

        profile?.let {
            Text(text = it.name, style = MaterialTheme.typography.titleLarge)
            AsyncImage(
                model = it.avatarUrl,
                contentDescription = "Avatar",
                modifier = Modifier.size(120.dp).clip(CircleShape)
            )
        } ?: Text("No user loaded")
    }
}

Best Practices for Using LRU Cache

Tip Description
Use LruCache only for in-memory caching Do not use it for persistent or disk-based caching
Define a sensible maxSize Based on app usage pattern and available memory
For image caching, use Bitmap size as the unit Override sizeOf() method
Evict cache on memory warnings Use onTrimMemory() or onLowMemory() callbacks
Use libraries like Coil, Glide, or Picasso They offer built-in LRU support for image loading

πŸ”š Remember

The LRU caching mechanism is a simple yet powerful technique in Android development. It keeps your UI snappy and responsive by reducing network usage and data load time. While libraries like Glide manage LRU for images out of the box, creating your custom LRU cache gives you flexibility and control—especially when caching JSON responses or domain objects.

Pro Tip: Combine LruCache with StateFlow and Jetpack Compose for real-time UI updates and smoother UX.



#AndroidDevelopment #Kotlin #JetpackCompose #LRUCache #CachingInAndroid #StateFlow #PerformanceOptimization



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

Happy coding! πŸ’»✨



Cracking the Senior Android Engineer Interview: What to Expect from Start to Finish

Stepping into the interview process for a Senior Android Engineer role can be both exciting and challenging. Whether you’re preparing for your dream job at a big tech firm or a promising startup, understanding the structure and topics across each interview stage is crucial.

In this blog, we’ll break down the commonly asked topics from the initial recruiter round to the final bar-raiser interview, including everything from Kotlin coding to system design and behavioral assessments.


 Initial HR/Recruiter Screening

This is mostly a soft round to gauge your fit for the role and company culture.

Topics:

  • Summary of your Android development experience

  • Key projects you've worked on (apps, user base, challenges)

  • Current role, notice period, and salary expectations

  • Why you're exploring new opportunities

  • Communication skills and professional demeanor


Online Technical Coding Round

Here the focus is on core problem-solving skills using Kotlin.

Topics to Prepare:

  • Data Structures & Algorithms:

    • Arrays, LinkedLists, Trees, Graphs, HashMaps

    • Sorting, searching, recursion, backtracking

  • Kotlin-specific Concepts:

    • Coroutines (Job, SupervisorJob, Dispatchers)

    • Flow, StateFlow, and Channels

    • Lambda expressions, extension functions, and delegates

  • Concurrency & Asynchronous Programming in Android

Tools: HackerRank, Codility, or take-home assignments


System Design Interview

This round evaluates how you architect scalable, modular Android apps.

Key Focus Areas:

  • Clean Architecture (Presentation → Domain → Data layers)

  • MVVM vs MVI vs MVP — and when to choose which

  • Real-world design:

    • Offline-first apps

    • Syncing with API and caching (Room, DataStore)

    • Push notifications & background sync (WorkManager)

  • Dependency Injection (Hilt/Dagger2)

  • Multi-module project structuring

Example: “Design a banking app with authentication, balance display, and transaction history”


Android Platform & Jetpack Deep Dive

Here, expect questions on Jetpack libraries, Compose UI, and platform internals.

Topics:

  • Jetpack Compose:

    • State management

    • Recomposition and performance pitfalls

  • Lifecycle Management:

    • ViewModel, LifecycleOwner, LifecycleObserver

  • Jetpack Libraries:

    • Navigation, Room, Paging, WorkManager, DataStore

  • Security:

    • Encrypted storage, biometric authentication, Keystore

  • Accessibility:

    • Compose semantics, TalkBack support, content descriptions


Testing & Debugging

A great Senior Android Engineer writes testable and maintainable code.

What You Should Know:

  • Unit Testing with JUnit, Mockito, MockK

  • UI Testing with Espresso and Compose testing APIs

  • Integration Testing with HiltTestApplication

  • Debugging ANRs, memory leaks (LeakCanary), performance bottlenecks

  • Using tools like Crashlytics, Logcat, StrictMode


CI/CD, DevOps, and Release Management

Modern Android teams value automation and fast feedback cycles.

Topics:

  • CI/CD tools: Jenkins, GitHub Actions, Bitrise

  • Gradle optimization, build flavors, product types

  • Feature flag implementation (Gradle + Firebase Remote Config)

  • Code quality enforcement: Detekt, Lint, SonarQube

  • Secure and efficient release strategies (Play Store, Firebase App Distribution)


Behavioral & Leadership Assessment

This round checks for team collaboration, mentorship, and decision-making skills.

Example Questions:

  • Tell us about a time you led a project or mentored a junior

  • How do you resolve disagreements with product or design teams?

  • What’s your strategy for balancing tech debt vs. feature delivery?

  • How do you stay updated with evolving Android trends?

Tip: Use the STAR method (Situation, Task, Action, Result) to answer.


Bar-Raiser or VP/CTO Round

This is the make-or-break round for many companies.

Focus Areas:

  • End-to-end ownership of features and impact

  • Trade-offs made during architecture decisions

  • Innovation, optimization, or cost-saving initiatives you've led

  • Long-term vision, technical leadership, and culture fit


πŸ”š Final Thoughts

Landing a Senior Android Engineer role isn’t just about writing great Kotlin code. It’s about demonstrating architectural mastery, leadership, and a product-first mindset across every round.

Start prepping smart by:

  • Practicing DSA in Kotlin

  • Building or refactoring a multi-module Compose app

  • Designing systems (like a chat app or e-commerce app)

  • Writing testable, clean code

  • Staying up to date with Jetpack and security best practices


What Next?

Want mock interview questions, detailed Kotlin exercises, or a full Android app architecture walkthrough? Drop a comment or subscribe for more deep-dives every week.


πŸ”— Related Reads:



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

Happy coding! πŸ’»✨

Exploring Scope Functions in Kotlin Compose

Kotlin provides several powerful scope functions that make your code more readable and concise. These scope functions — let, run, apply, also, and with — are particularly useful when working with Kotlin's modern Android development framework, Jetpack Compose. 

Scope functions in Kotlin allow you to execute a block of code within the context of an object. Each of these functions has different characteristics and return values. They can be used for various purposes, like modifying an object, chaining function calls, or simplifying code readability.

1. let – Transform the Object and Return the Result

The let function executes a block of code on the object it is invoked on and returns the result of the block. It's often used when working with an object and returning a result without modifying the original object.

Syntax:

val result = object.let {
    // Do something with object
    "result"
}

Example in Compose: Suppose we want to handle an event in Jetpack Compose like a button click, where we only need to transform the result or pass it on to another function:

val buttonText = "Click me"
val result = buttonText.let {
    it.toUpperCase()
}

Text(text = result)  // Output: "CLICK ME"

In this example, the let function transforms buttonText to uppercase without modifying the original string.

2. run – Execute a Block and Return the Result

run is similar to let, but it is typically used when you want to execute a block of code and return a result. Unlike let, run doesn’t take the object as an argument — instead, it works within the object's context.

Syntax:

val result = object.run {
    // Do something with object
    "result"
}

Example in Compose: When creating a composable, you can use run to set up complex UI elements or operations:

val result = "Hello".run {
    val length = length
    "Length of text: $length"
}

Text(text = result)  // Output: "Length of text: 5"

Here, we used run to access properties and perform operations on a string. The function directly returns the result without modifying the object.

3. apply – Configure an Object and Return It

The apply function is used when you want to modify an object and return the modified object itself. It’s particularly useful for setting multiple properties on an object.

Syntax:

val modifiedObject = object.apply {
    // Modify object properties
}

Example in Compose: For instance, in Jetpack Compose, you can use apply when configuring a Modifier object to add multiple modifications:

val modifier = Modifier
    .padding(16.dp)
    .apply {
        background(Color.Blue)
        fillMaxSize()
    }

Box(modifier = modifier) {
    Text(text = "Hello World")
}

In this case, the apply function allows chaining multiple properties on the Modifier object and returns the same object after applying changes.

4. also – Perform an Action and Return Object

The also function is often used when you want to perform additional actions on an object but don’t want to change or return a new object. It’s often used for logging or debugging.

Syntax:

val objectWithAction = object.also {
    // Perform actions like logging
}

Example in Compose: Suppose you want to log a value when a user clicks a button in Compose:

val clickCount = remember { mutableStateOf(0) }

Button(onClick = {
    clickCount.value = clickCount.value.also {
        println("Button clicked: ${clickCount.value} times")
    } + 1
}) {
    Text("Click Me")
}

In this example, the also function is used to log the click count before updating the value of clickCount.

5. with – Execute a Block on an Object Without Returning It

The with function is used when you want to perform several actions on an object without modifying or returning it. It operates similarly to run, but unlike run, which operates in the context of the object, with requires the object to be passed explicitly.

Syntax:

with(object) {
    // Perform actions
}

Example in Compose: If you want to configure a composable’s properties, you can use with for better readability:

val modifier = with(Modifier) {
    padding(16.dp)
    background(Color.Green)
    fillMaxSize()
}

Box(modifier = modifier) {
    Text("Welcome to Compose!")
}

Here, with is used to apply multiple modifiers without repeatedly referencing the Modifier object.

Key Differences Between Scope Functions

  • let: Useful when you transform the object and return a result. The object is passed as an argument to the block.

  • run: This is similar to let, but the object is accessed directly within the block. It is helpful when returning a result after performing operations.

  • apply: Modifies the object and returns the object itself. Ideal for object configuration.

  • also: Similar to apply, but used primarily for performing side actions (like logging), while returning the original object.

  • with: Works like run, but requires the object to be passed explicitly and is used when you need to operate on an object without modifying it.

When to Use Each in Jetpack Compose

  • let: When you must transform or pass an object’s value.

  • run: When performing operations within the context of an object and returning the result.

  • apply: When you need to modify an object (like a Modifier) and return it after changes.

  • also: For performing additional actions (e.g., logging or debugging) without changing the object.

  • with: When you want to execute multiple operations on an object without modifying it.

Summary

Scope functions are essential to Kotlin’s functional programming style, offering a concise and readable way to work with objects. In Jetpack Compose, they help streamline UI development, manage states, and enhance overall code readability. Whether you’re configuring UI elements, performing transformations, or logging actions, scope functions can significantly reduce boilerplate and improve the efficiency of your Kotlin Compose code.

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

Happy coding! πŸ’»✨

Coin Change Problem in Kotlin: Multiple Approaches with Examples

The coin change problem is a classic leet coding challenge often encountered in technical interviews. The problem asks:

Given an array of coin denominations and a target amount, find the fewest number of coins needed to make up that amount. If it's not possible, return -1. You can use each coin denomination infinitely many times.

Here are multiple ways to solve the Coin Change problem in Kotlin, with detailed explanations and code examples. I'll present two distinct approaches:

  1. Dynamic Programming (Bottom-Up approach)
  2. Recursive Approach with Memoization (Top-Down)

Approach 1: Dynamic Programming (Bottom-Up)

Idea:

  • Build an array dp where each dp[i] indicates the minimum number of coins required for the amount i.
  • Initialize the array with a large number (representing infinity).
  • The base case is dp[0] = 0.

Steps:

  • For each amount from 1 to amount, try every coin denomination.
  • Update dp[i] if using the current coin leads to fewer coins than the current value.

Kotlin Solution:

fun coinChange(coins: IntArray, amount: Int): Int {
    val max = amount + 1
    val dp = IntArray(amount + 1) { max }
    dp[0] = 0

    for (i in 1..amount) {
        for (coin in coins) {
            if (coin <= i) {
                dp[i] = minOf(dp[i], dp[i - coin] + 1)
            }
        }
    }
    
    return if (dp[amount] > amount) -1 else dp[amount]
}

// Usage:
fun main() {
    println(coinChange(intArrayOf(1, 2, 5), 11)) // Output: 3
    println(coinChange(intArrayOf(2), 3))        // Output: -1
    println(coinChange(intArrayOf(1), 0))        // Output: 0
}

Time Complexity: O(amount * coins.length)
Space Complexity: O(amount)


Approach 2: Recursive Approach with Memoization (Top-Down)

Idea:

  • Define a recursive function solve(remainingAmount) that returns the minimum coins required.
  • Use memoization to store previously computed results, avoiding redundant calculations.

Steps:

  • For each call, explore all coin denominations and recursively find solutions.
  • Cache results to avoid recomputation.

Kotlin Solution:

fun coinChangeMemo(coins: IntArray, amount: Int): Int {
    val memo = mutableMapOf<Int, Int>()

    fun solve(rem: Int): Int {
        if (rem < 0) return -1
        if (rem == 0) return 0
        if (memo.containsKey(rem)) return memo[rem]!!

        var minCoins = Int.MAX_VALUE
        for (coin in coins) {
            val res = solve(rem - coin)
            if (res >= 0 && res < minCoins) {
                minCoins = res + 1
            }
        }

        memo[rem] = if (minCoins == Int.MAX_VALUE) -1 else minCoins
        return memo[rem]!!
    }

    return solve(amount)
}

// Usage:
fun main() {
    println(coinChangeMemo(intArrayOf(1, 2, 5), 11)) // Output: 3
    println(coinChangeMemo(intArrayOf(2), 3))        // Output: -1
    println(coinChangeMemo(intArrayOf(1), 0))        // Output: 0
}

Time Complexity: O(amount * coins.length)
Space Complexity: O(amount) (stack space + memoization map)


Quick Comparison:

Approach Time Complexity Space Complexity When to Use?
Dynamic Programming (Bottom-Up) O(amount * coins.length) O(amount) Optimal, preferred for efficiency
Recursive with Memoization O(amount * coins.length) O(amount) Easy to understand recursion

Edge Cases Handled:

  • If amount is 0, both solutions immediately return 0.
  • If the amount cannot be composed by given coins, they return -1.

Summary:

  • Dynamic Programming is the optimal, most widely used solution for this problem.
  • Recursive Approach with memoization demonstrates understanding of recursion and memoization principles.

You can select either based on clarity, readability, or efficiency needs. The DP solution is highly recommended in competitive programming or technical interviews for optimal performance. 

Thanks for reading! πŸŽ‰ I'd love to know what you think about the article. Did it resonate with you? πŸ’­ Any suggestions for improvement? I’m always open to hearing your feedback to improve my posts! πŸ‘‡πŸš€. Happy coding! πŸ’»✨

Code Challenge: Number of Islands in Kotlin

The Number of Islands problem is a common interview question that involves counting the number of islands in a 2D grid. Each island is made up of connected pieces of land (denoted as '1') surrounded by water (denoted as '0'). The challenge is to count how many separate islands exist in the grid, where an island is formed by horizontally or vertically adjacent lands.



We will discuss multiple ways to solve this problem, explaining their pros and cons. Let's dive into solving this problem using Kotlin.


Problem Definition

Given a 2D binary grid grid, return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. The grid is surrounded by water on all sides.

Example 1:

Input:

[
  ["1", "1", "1", "1", "0"],
  ["1", "1", "0", "1", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "0", "0", "0"]
]

Output:

1

Example 2:

Input:

[
  ["1", "1", "0", "0", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "1", "0", "0"],
  ["0", "0", "0", "1", "1"]
]

Output:

3

Approach 1: Depth-First Search (DFS)

The most intuitive approach is to use Depth-First Search (DFS). We start from each land cell ('1'), mark it as visited (or change it to water '0'), and recursively check its adjacent cells (up, down, left, right). Every time we find an unvisited land cell, we count it as a new island.

Algorithm:

  1. Traverse the grid.
  2. If we find a '1', increment the island count and use DFS to mark the entire island as visited.
  3. For each DFS, recursively mark the neighboring land cells.

Kotlin Implementation:

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    var count = 0

    // Define DFS function
    fun dfs(grid: Array<CharArray>, i: Int, j: Int) {
        // Return if out of bounds or at water
        if (i < 0 || i >= grid.size || j < 0 || j >= grid[0].size || grid[i][j] == '0') return
        // Mark the land as visited
        grid[i][j] = '0'
        // Visit all 4 adjacent cells
        dfs(grid, i + 1, j) // down
        dfs(grid, i - 1, j) // up
        dfs(grid, i, j + 1) // right
        dfs(grid, i, j - 1) // left
    }

    // Iterate over the grid
    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                // Found a new island
                count++
                dfs(grid, i, j)
            }
        }
    }
    return count
}

Time Complexity:

  • O(m * n), where m is the number of rows and n is the number of columns. Each cell is visited once.

Space Complexity:

  • O(m * n) in the worst case (if the entire grid is land), as we may need to store all cells in the call stack due to recursion.

Approach 2: Breadth-First Search (BFS)

We can also use Breadth-First Search (BFS). Instead of using recursion like in DFS, we use a queue to explore all adjacent cells iteratively. The process is similar, but the main difference lies in the order of exploration.

Algorithm:

  1. Start from an unvisited land cell ('1').
  2. Use a queue to explore all adjacent land cells and mark them as visited.
  3. Each BFS initiation represents a new island.

Kotlin Implementation:

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    var count = 0
    val directions = arrayOf(intArrayOf(0, 1), intArrayOf(1, 0), intArrayOf(0, -1), intArrayOf(-1, 0))

    fun bfs(i: Int, j: Int) {
        val queue: LinkedList<Pair<Int, Int>>= LinkedList()
        queue.offer(Pair(i, j))
        grid[i][j] = '0' // Mark the starting cell as visited

        while (queue.isNotEmpty()) {
            val (x, y) = queue.poll()
            for (dir in directions) {
                val newX = x + dir[0]
                val newY = y + dir[1]
                if (newX in grid.indices && newY in grid[0].indices && grid[newX][newY] == '1') {
                    grid[newX][newY] = '0' // Mark as visited
                    queue.offer(Pair(newX, newY))
                }
            }
        }
    }

    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                count++
                bfs(i, j)
            }
        }
    }
    return count
}

Time Complexity:

  • O(m * n), where m is the number of rows and n is the number of columns. Each cell is visited once.

Space Complexity:

  • O(m * n), which is required for the queue in the worst case.

Approach 3: Union-Find (Disjoint Set)

The Union-Find (or Disjoint Set) approach is another efficient way to solve this problem. The idea is to treat each land cell as an individual component and then union adjacent land cells. Once all unions are complete, the number of islands is simply the number of disjoint sets.

Algorithm:

  1. Initialize each land cell as a separate island.
  2. For each neighboring land cell, perform a union operation.
  3. The number of islands will be the number of disjoint sets.

Kotlin Implementation:

class UnionFind(private val m: Int, private val n: Int) {
    private val parent = IntArray(m * n) { it }

    fun find(x: Int): Int {
        if (parent[x] != x) parent[x] = find(parent[x]) // Path compression
        return parent[x]
    }

    fun union(x: Int, y: Int) {
        val rootX = find(x)
        val rootY = find(y)
        if (rootX != rootY) parent[rootX] = rootY
    }

    fun getCount(): Int {
        return parent.count { it == it }
    }
}

fun numIslands(grid: Array<CharArray>): Int {
    if (grid.isEmpty()) return 0
    val m = grid.size
    val n = grid[0].size
    val uf = UnionFind(m, n)

    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                val index = i * n + j
                // Try to union with adjacent cells
                if (i + 1 &lt; m &amp;&amp; grid[i + 1][j] == '1') uf.union(index, (i + 1) * n + j)
                if (j + 1 &lt; n &amp;&amp; grid[i][j + 1] == '1') uf.union(index, i * n + (j + 1))
            }
        }
    }
    val islands = mutableSetOf&lt;Int&gt;()
    for (i in grid.indices) {
        for (j in grid[i].indices) {
            if (grid[i][j] == '1') {
                islands.add(uf.find(i * n + j))
            }
        }
    }
    return islands.size
}

Time Complexity:

  • O(m * n), as we perform a union operation for each adjacent land cell.

Space Complexity:

  • O(m * n) for the union-find parent array.

Calling in main():

fun main() {
    val grid1 = arrayOf(
        charArrayOf('1', '1', '1', '1', '0'),
        charArrayOf('1', '1', '0', '1', '0'),
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('0', '0', '0', '0', '0')
    )
    println("Number of Islands : ${numIslands(grid1)}")  // Output: 1
    
    val grid2 = arrayOf(
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('1', '1', '0', '0', '0'),
        charArrayOf('0', '0', '1', '0', '0'),
        charArrayOf('0', '0', '0', '1', '1')
    )
    println("Number of Islands : ${numIslands(grid2)}")  // Output: 3
}



Which Solution is Best?

  1. DFS/BFS (Approaches 1 & 2): These are the simplest and most intuitive solutions. Both have a time complexity of O(m * n), which is optimal for this problem. DFS uses recursion, which might run into issues with large grids due to stack overflow, but BFS avoids this problem by using an iterative approach. If you want simplicity and reliability, BFS is preferred.

  2. Union-Find (Approach 3): This approach is more advanced and has a similar time complexity of O(m * n). However, it can be more difficult to understand and implement. It also performs well with path compression and union by rank, but for this problem, the DFS/BFS approach is usually sufficient and easier to implement.

Conclusion

For this problem, BFS is the recommended solution due to its iterative nature, which avoids recursion issues with large grids, while still being efficient and easy to understand.


Full Problem description in LeetCode


Thank you for reading my latest article! I would greatly appreciate your feedback to improve my future posts. πŸ’¬ Was the information clear and valuable? Are there any areas you think could be improved? Please share your thoughts in the comments or reach out directly. Your insights are highly valued. πŸ‘‡πŸ˜Š.  Happy coding! πŸ’»✨

Scenario-based technical questions for a Senior Android Engineer interview

Comprehensive list of scenario-based technical questions for a Senior Android Engineer interview, covering various aspects of Android development and software engineering practices:




Scenario 1: Rewriting a Legacy App

Question: How would you approach rewriting a legacy Android app to improve scalability and maintainability?
Answer:
I would adopt Clean Architecture to structure the app into independent layers:

  • Domain Layer for business logic
  • Data Layer for repository and API interactions
  • Presentation Layer for UI using MVVM

I would migrate the app incrementally to avoid disruptions, starting with small modules and ensuring extensive unit tests for the rewritten sections. Dependency Injection (e.g., Hilt) would manage dependencies, and new features would be developed using the improved architecture while the legacy sections are refactored.


Scenario 2: Navigation Crash

Question: Users report crashes when navigating between fragments. How would you fix this?
Answer:
I would investigate the crash logs to pinpoint lifecycle issues, such as using retained fragments incorrectly. Transitioning to the Android Navigation Component resolves most navigation-related lifecycle issues, ensuring safe fragment transactions. Adding null checks and default fallbacks would prevent null pointer crashes. Tools like Firebase Crashlytics help monitor the fix in production.


Scenario 3: API Integration

Question: How do you manage third-party API integrations to make the app robust and future-proof?
Answer:
I’d encapsulate the API logic in a Repository Layer, isolating the app from changes to the API. Libraries like Retrofit handle networking, while Result wrappers ensure error states are managed gracefully. To future-proof, I’d use an abstraction layer so that switching APIs later won’t require massive codebase changes.


Scenario 4: RecyclerView Performance

Question: The RecyclerView in your app lags with large datasets. How would you optimize it?
Answer:
I’d optimize the ViewHolder pattern, avoiding redundant inflation of views. Leveraging ListAdapter with DiffUtil improves performance for dynamic datasets. For heavy image loads, libraries like Glide or Picasso are used with proper caching enabled. If the dataset is very large, Paging 3 ensures efficient, paginated loading.


Scenario 5: Unresponsive API Calls

Question: Users complain about unresponsive API calls. How do you handle this?
Answer:
To address this, I’d offload API calls to Coroutines on Dispatchers.IO to ensure they don’t block the main thread. Adding timeouts and retry mechanisms in OkHttp Interceptors prevents unresponsiveness. Showing a loading state keeps users informed, and local caching reduces API dependency for repeated data.


Scenario 6: Secure Token Storage

Question: How do you securely store API tokens in an Android app?
Answer:
For API 23+, I’d use EncryptedSharedPreferences or the Android Keystore System to store tokens securely. To prevent exposure during transit, all API requests are made over HTTPS, and sensitive tokens are refreshed periodically using OAuth mechanisms.


Scenario 7: Testing Flaky UI Tests

Question: How do you address flaky UI tests in your CI pipeline?
Answer:
I’d identify the root causes, such as timing issues or non-deterministic behaviors. Using IdlingResources ensures UI tests wait for background tasks to complete. Tests are rewritten for determinism by mocking external dependencies. For UI animations, disabling animations during testing improves reliability.


Scenario 8: Multi-Environment Support

Question: How do you configure an Android app for multiple environments (dev, staging, prod)?
Answer:
I’d use Gradle build flavors to define separate configurations for each environment. Each flavor would have its own properties file for environment-specific settings like base URLs or API keys. This setup ensures clean separation and easy switching during development and deployment.


Scenario 9: Junior Developer Code Review

Question: A junior developer’s pull request contains suboptimal code. How do you address this?
Answer:
I’d provide constructive feedback by highlighting specific issues, such as code inefficiencies or deviation from standards. Suggestions would be supported with documentation or examples. To improve learning, I’d schedule a pair programming session to guide them in implementing the changes.


Scenario 10: Periodic Background Jobs

Question: How do you fetch data periodically in an Android app without impacting performance?
Answer:
I’d use WorkManager with periodic work constraints. Configurations like network type and battery usage constraints ensure efficiency. Exponential backoff handles retries on failure, and app state is preserved using Data objects passed to the worker.


Scenario 11: Slow Build Times

Question: Your team is experiencing slow build times. How would you address this issue?
Answer:
I’d analyze the build process using Gradle’s build scan tools to identify bottlenecks, such as excessive task configurations or dependencies. Recommendations include:

  • Dependency resolution: Use API instead of implementation for unnecessary module dependencies.
  • Parallel execution: Enable parallel execution in Gradle.
  • Build caching: Configure remote caching to speed up builds in CI environments.
  • Modularization: Break the app into smaller modules, improving build times for incremental changes.

Scenario 12: App State Restoration

Question: How would you handle app state restoration after process death?
Answer:
I’d use ViewModel and SavedStateHandle to persist critical UI state. For larger datasets or app-wide data, Room Database or SharedPreferences would store non-sensitive data. The onSaveInstanceState method ensures transient states (like scroll positions) are restored using Bundle.


Scenario 13: Dependency Updates

Question: A library you depend on has released a critical update. How do you handle updating dependencies?
Answer:
I’d analyze the release notes and change logs to understand potential impacts. Before updating, I’d test the new version in a separate branch or a feature flag. CI pipelines would run full regression tests to ensure no functionality breaks. Gradle’s dependency locking helps manage controlled updates in the team.


Scenario 14: Multi-Module Navigation

Question: How would you implement navigation in a multi-module app?
Answer:
I’d use the Navigation Component with dynamic feature modules. Deep links are utilized for cross-module navigation. Navigation logic remains within each module, and shared arguments are passed using SafeArgs. Dynamic feature module delivery ensures lightweight APKs for specific features.


Scenario 15: Offline-First Architecture

Question: How would you design an offline-first app?
Answer:
I’d use a Room Database as the single source of truth, with APIs syncing data in the background using WorkManager. Conflict resolution strategies (e.g., timestamp-based syncing) ensure consistency. Data updates are observed via LiveData or Flow, providing a seamless offline and online experience.


Scenario 16: Memory Leaks in Fragments

Question: How would you debug and resolve memory leaks in fragments?
Answer:
I’d use tools like LeakCanary to detect leaks. Common issues include retaining fragment references in ViewModels or adapters. Avoiding anonymous inner classes and clearing listeners in onDestroyView prevents leaks. Switching to Fragment KTX utilities simplifies fragment lifecycle handling.


Scenario 17: Expanding App to Tablets

Question: How would you optimize your app to support tablet devices?
Answer:
I’d implement responsive layouts using resource qualifiers (layout-w600dp, etc.). ConstraintLayout or Compose's BoxWithConstraints helps create flexible designs. Tablets would leverage split-screen functionality and dual-pane layouts for a richer experience. I’d test on actual devices or simulators with varying aspect ratios.


Scenario 18: Network State Handling

Question: How would you handle intermittent network connectivity in your app?
Answer:
Using ConnectivityManager, I’d monitor network status and notify users of connectivity changes. For intermittent connections, I’d use WorkManager with a NetworkType.CONNECTED constraint for retries. Local caching with Room or offline-first design ensures minimal disruption for users.


Scenario 19: ProGuard Rules Issue

Question: After enabling ProGuard, some features break in release builds. How do you troubleshoot this?
Answer:
I’d check ProGuard logs for obfuscation rules causing issues. Adding keep rules for critical classes or libraries resolves the problem. For example, libraries like Gson need rules to retain serialized class names. Testing the release APK in a staging environment prevents such errors from reaching production.


Scenario 20: Gradle Dependency Conflicts

Question: How do you handle dependency version conflicts in a multi-module project?
Answer:
I’d resolve dependency conflicts by enforcing versions in the root build.gradle using the constraints block. Gradle’s dependency resolution tools or ./gradlew dependencies help identify conflicts. Upgrading to a shared compatible version or excluding transitive dependencies ensures compatibility.


Scenario 21: Gradual Migration to Jetpack Compose

Question: How would you migrate an app with traditional XML layouts to Jetpack Compose without disrupting ongoing development?
Answer:
I’d use ComposeView to integrate Jetpack Compose into existing XML-based screens. This allows for gradual migration by converting one feature or screen at a time. The strategy ensures compatibility between Compose and View-based UIs. Migration priorities would be screens with simple layouts or those undergoing redesigns. Thorough testing would validate seamless UI interactions during the transition.


Scenario 22: Managing App Performance During Animation

Question: How do you optimize animations to maintain 60 FPS in an Android app?
Answer:
I’d use Jetpack Compose animations or Android’s Animator API, ensuring minimal main-thread blocking. Optimizations include precomputing values, reducing overdraw with proper layering, and using lightweight vector assets instead of large bitmaps. Profiling tools like GPU Profiler or Layout Inspector help identify performance bottlenecks. Asynchronous animations using Coroutines also minimize UI thread load.


Scenario 23: Handling Sensitive Data in Logs

Question: How do you ensure sensitive user data doesn’t appear in logs during debugging?
Answer:
I’d avoid logging sensitive data altogether by following best practices like:

  • Tag masking for potentially sensitive information.
  • Utilizing Log.d() only in debug builds using BuildConfig.DEBUG.
  • Leveraging tools like Timber for conditional logging and ensuring proguard-rules.pro strips out all logs in release builds.

Scenario 24: App Crash on Specific Devices

Question: Your app works fine on most devices but crashes on a few specific models. How would you debug this?
Answer:
I’d start by analyzing device-specific crash logs through Firebase Crashlytics or Play Console’s Vitals. Common issues include hardware limitations, API compatibility, or vendor-specific customizations. I’d test on emulators or real devices mimicking those models and apply device-specific feature toggles or fallback logic to ensure stability.


Scenario 25: Adopting KMM (Kotlin Multiplatform Mobile)

Question: How would you decide if Kotlin Multiplatform Mobile (KMM) is suitable for your project?
Answer:
I’d evaluate if the app’s business logic can be shared across platforms. Projects with extensive API integrations or business rules benefit most from KMM. I’d ensure the team is comfortable with Kotlin and modular architecture. UI layers would remain native (Jetpack Compose for Android and SwiftUI for iOS), while the shared code handles networking, data parsing, and logic.


Scenario 26: Debugging High Battery Usage

Question: Users report your app drains their battery quickly. How would you address this?
Answer:
Using Android Studio’s Energy Profiler, I’d monitor wake locks, background jobs, and location services. Optimizations include batching background tasks with WorkManager, reducing GPS usage frequency with fused location providers, and leveraging low-power modes for periodic operations. Removing unnecessary foreground services helps reduce power consumption.


Scenario 27: Scaling an App for Millions of Users

Question: How would you scale an Android app to handle millions of active users?
Answer:
I’d optimize the app for minimal network usage using effective caching strategies (e.g., Room or DiskLruCache). Efficient API pagination and batched updates would reduce server strain. Metrics and crash monitoring tools (e.g., Firebase Analytics, Sentry) provide insights into performance and usage patterns. Incremental rollout strategies prevent widespread issues.


Scenario 28: Addressing App Uninstall Feedback

Question: How do you act on feedback that users uninstall your app due to poor onboarding?
Answer:
I’d streamline the onboarding process by reducing friction with concise, visually engaging walkthroughs. Adding in-app prompts for permissions only when necessary improves the first experience. A/B testing onboarding flows and analyzing retention metrics help refine the process.


Scenario 29: Maintaining Backward Compatibility

Question: How do you support legacy Android devices while using modern features?
Answer:
I’d use Jetpack Libraries to provide backward-compatible implementations of modern features. Features exclusive to higher API levels are guarded with Build.VERSION.SDK_INT checks. Custom implementations or polyfills provide fallbacks for unsupported features on older devices.


Scenario 30: Real-Time Feature Flags

Question: How would you implement feature flags for dynamic control over app behavior?
Answer:
I’d integrate a feature flag service like Firebase Remote Config or a custom implementation using a REST API and local caching. Flags are dynamically fetched at app startup and stored in local preferences. The code paths for new features remain toggleable, allowing easy enable/disable scenarios without requiring an app update.


Scenario 31: Integrating Payments

Question: How do you integrate in-app purchases securely?
Answer:
I’d use Google Play Billing API for secure purchase handling. Verifying purchases with the server ensures authenticity. Sensitive business logic is implemented server-side, and the app only displays validated purchase states. Subscription models follow best practices for automatic renewal handling and cancellation prompts.


Scenario 32: Codebase Documentation

Question: Your team is struggling with understanding parts of the codebase. How do you address this?
Answer:
I’d introduce KDoc comments in the code for method and class documentation. A centralized wiki system like Confluence or GitHub Pages provides detailed guides and architecture diagrams. Regular knowledge-sharing sessions ensure all team members are aligned.


Scenario 33: Leading a Team through Technical Debt

Question: Your team is facing significant technical debt due to quick fixes in the past. How would you prioritize and address this challenge?
Answer:
I’d start by identifying critical areas of technical debt using static analysis tools (e.g., SonarQube) and gathering feedback from developers. I’d prioritize issues that impact performance, security, or maintainability. During sprint planning, I would allocate time for refactoring while ensuring that new features are still being delivered. Introducing Code Reviews with an emphasis on long-term maintainability helps avoid further accumulation of technical debt. Additionally, I’d encourage the team to gradually improve the codebase with regular cleanup tasks and foster a culture of continuous refactoring.


Scenario 34: Managing Conflicting Opinions in the Team

Question: During a sprint planning session, two developers strongly disagree on the architecture approach for a feature. How would you handle this conflict?
Answer:
I’d listen to both developers and ensure they explain their reasoning clearly, focusing on the pros and cons of each approach. I’d encourage data-driven discussions (e.g., performance benchmarks, previous project examples). If needed, I’d bring in a third-party expert (e.g., senior developer or architect) to help make an informed decision. If the disagreement persists, I’d consider running a proof-of-concept (POC) to evaluate both approaches in real-world conditions. This fosters a collaborative environment while helping the team make the best technical decision for the feature.


Scenario 35: Onboarding a New Developer to an Existing Codebase

Question: How would you onboard a new developer who is unfamiliar with your app’s architecture and codebase?
Answer:
I’d provide the new developer with a structured onboarding guide, including:

  1. Codebase Overview: Detailed documentation on the architecture, technologies used, and the project structure.
  2. Key Concepts: Introduce design patterns like MVVM, dependency injection (e.g., Hilt), and CI/CD pipelines.
  3. Hands-On Tasks: Assign simple tasks (e.g., bug fixes or small feature enhancements) to help them get familiar with the codebase and development workflow.
  4. Mentorship: Pair the developer with a more experienced team member for code reviews and guidance.
    Regular check-ins ensure they feel supported throughout the process.

Scenario 36: Automated Tests in a CI/CD Pipeline

Question: How would you ensure that all tests are passing before deploying an app in a CI/CD pipeline?
Answer:
I’d configure the CI/CD pipeline (e.g., Jenkins, GitHub Actions) to run automated tests for every commit or pull request. This includes unit tests (via JUnit), UI tests (via Espresso or Compose Test), and integration tests. Code coverage tools (e.g., JaCoCo) would ensure comprehensive testing. Additionally, I’d enable static analysis tools like Lint and SonarQube to check for code quality issues. The pipeline would fail if any tests fail, preventing broken code from reaching production.


Scenario 37: Dealing with Deployment Failures

Question: You deploy a new version of the app, but users report issues. How do you handle this situation?
Answer:
I’d immediately roll back the deployment if the issues are critical, using Google Play Console’s staged rollout feature to limit exposure. Then, I’d review crash reports from Firebase Crashlytics and check analytics for affected users. Once the root cause is identified, I’d prioritize a hotfix, test it in a staging environment, and then redeploy. Post-mortem analysis would help understand what went wrong and ensure better testing or monitoring practices in the future. Communication with users about the issue and its resolution is essential for trust.


Scenario 38: Testing with Multiple Android Versions

Question: How would you ensure that your app works across various Android versions and devices?
Answer:
I’d set up automated tests targeting multiple Android versions using Firebase Test Lab or Sauce Labs to test on real devices. I’d include API version checks within the app to handle specific features and permissions for older Android versions. Additionally, I’d manually test on critical versions (e.g., Android 10, 11, 12, etc.) and leverage tools like Android Device Monitor to simulate devices with different screen sizes, resolutions, and OS versions.


Scenario 39: Continuous Integration (CI) Configuration

Question: You need to set up a new CI/CD pipeline for an Android project. What tools and steps would you use?
Answer:
I’d choose Jenkins, GitHub Actions, or CircleCI as CI tools. The pipeline steps would include:

  1. Clone Repository: Pull the latest code from the version control system (e.g., GitHub, GitLab).
  2. Build: Use Gradle to build the APK.
  3. Static Analysis: Run Lint and SonarQube checks for code quality.
  4. Run Tests: Execute unit tests (JUnit), UI tests (Espresso), and instrumented tests.
  5. Publish: Upload the build to Google Play Console or Firebase App Distribution for testing.
  6. Deploy: Deploy to production only after passing all tests and reviews.
    This setup automates the process and ensures consistency across environments.

Scenario 40: Code Freeze Before Release

Question: How do you manage a code freeze before a major release?
Answer:
Before the code freeze, I’d ensure all features for the release are completed and tested. The freeze date is communicated well in advance. During the freeze, only critical bug fixes and high-priority tasks are allowed. I’d schedule QA testing, run regression tests, and address any remaining issues promptly. A branch protection strategy is enforced, ensuring no new features or changes are merged during the freeze. The focus is on stabilizing the app for release, ensuring that no new bugs are introduced.


Scenario 41: Performance Testing and Optimization

Question: How would you approach performance testing and optimization for an Android app?
Answer:
I’d start by identifying critical areas that affect performance, such as startup time, network requests, memory usage, and UI responsiveness. Tools like Android Profiler, Systrace, and Firebase Performance Monitoring help pinpoint issues. For optimization:

  • Lazy loading reduces memory consumption and startup time.
  • Background tasks (e.g., using WorkManager) are optimized to avoid battery drain.
  • RecyclerView optimizations like ViewHolder patterns and DiffUtil for list updates.
  • Image loading is done efficiently using libraries like Glide or Picasso to handle caching and resizing.
  • Threading and Coroutines ensure smooth UI interactions by avoiding main-thread blocking.

Scenario 42: Handling App Crashes in Production

Question: How do you respond if your app crashes in production and affects a significant portion of users?
Answer:
I’d first check Firebase Crashlytics or Play Console crash reports to identify the crash’s root cause. If the issue is critical, I’d initiate a hotfix and ensure it passes all necessary tests before redeploying. Rollbacks or staged rollouts minimize user impact while fixing the issue. In parallel, I’d work on a longer-term solution to prevent similar issues, such as adding more test coverage, improving error handling, or adding more specific logging. Clear communication with users is also essential during this process.


Would you like even more specific scenarios or focus on any other technical areas? Let me know in comments below !