Skip to main content

Mobile engineer coding interview questions

11 questions on coding for mobile engineer candidates. Expect prompts such as “Implement an LRU cache in Swift or Kotlin” and “Debounce a search text field in Swift async/await”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Implement an LRU (Least Recently Used) cache that holds up to N items. It should support O(1) get and O(1) put operations. Write it in Swift or Kotlin.

Sample answer outline

Candidate combines a doubly linked list for O(1) insert/remove at head/tail and a hash map from key to node for O(1) lookup. get moves the accessed node to the head. put inserts at the head; if capacity exceeded, removes the tail node and its map entry. Must handle thread safety (synchronized or actor wrapper) if used from multiple queues.

Reference implementation (swift)

Swift
class LRUCache<Key: Hashable, Value> {
    private class Node {
        var key: Key
        var value: Value
        var prev: Node?
        var next: Node?
        init(_ key: Key, _ value: Value) { self.key = key; self.value = value }
    }
    private let capacity: Int
    private var map = [Key: Node]()
    private let head = Node(/* sentinel */ ...)
    private let tail = Node(/* sentinel */ ...)
    init(capacity: Int) { ... }
    func get(_ key: Key) -> Value? { ... }
    func put(_ key: Key, _ value: Value) { ... }
}

Expect these follow-ups

  • How would you make this thread-safe for concurrent reads and exclusive writes?
  • NSCache on iOS does LRU eviction automatically; when would you prefer your custom implementation?
algorithmsdata-structureslruswiftkotlin

As asked

A user types in a search field and you want to fire a network request only 300 ms after they stop typing, cancelling any in-flight request if they type again. Implement this in Swift using async/await and Task.

Sample answer outline

Cancel the previous Task before creating a new one each time the text changes. Inside the Task, await Task.sleep(nanoseconds: 300_000_000) then call the search function. If the Task is cancelled during sleep, the CancellationError propagates and the network call never fires. The pattern is a classic debounce via task cancellation.

Reference implementation (swift)

Swift
class SearchViewModel: ObservableObject {
    @Published var query = ""
    private var searchTask: Task<Void, Never>?

    func onQueryChanged(_ newQuery: String) {
        searchTask?.cancel()
        searchTask = Task {
            try? await Task.sleep(nanoseconds: 300_000_000)
            guard !Task.isCancelled else { return }
            await performSearch(newQuery)
        }
    }

    private func performSearch(_ q: String) async { /* ... */ }
}

Expect these follow-ups

  • How would you throttle instead of debounce?
  • How would you do the same thing in Combine using debounce operator?
swiftasync-awaitconcurrencynetworkingios

As asked

Implement a simple paginated list loader in Kotlin using coroutines. It should load the next page when the user scrolls near the end, show a loading indicator, and deduplicate in-flight requests for the same page.

Sample answer outline

Track currentPage and a loading flag. In onScrolled listen, when lastVisibleItem > totalCount - threshold, call loadNextPage(). Guard with if (isLoading || hasReachedEnd) return. Inside loadNextPage, set isLoading=true, launch a coroutine, append results to the StateFlow list, increment page, set isLoading=false. Candidate should also mention using Jetpack Paging 3 as the production answer.

Reference implementation (kotlin)

Kotlin
class FeedViewModel(private val api: FeedApi) : ViewModel() {
    private val _items = MutableStateFlow<List<Post>>(emptyList())
    val items: StateFlow<List<Post>> = _items
    private var page = 1
    private var loading = false
    private var ended = false

    fun loadMore() {
        if (loading || ended) return
        loading = true
        viewModelScope.launch {
            val result = api.getPosts(page)
            if (result.isEmpty()) { ended = true } else {
                _items.update { it + result }
                page++
            }
            loading = false
        }
    }
}

Expect these follow-ups

  • How does Jetpack Paging 3 handle retry on failure compared to this manual approach?
  • How would you handle the user pulling to refresh, which resets the page count?
kotlinandroidpagingcoroutinesrecyclerview

As asked

Given a tree of Comment objects where each comment has an array of child replies, write a function that returns a flat array in depth-first pre-order, each element annotated with its nesting depth, suitable for rendering in a RecyclerView.

Sample answer outline

Iterative DFS with a stack of (node, depth) pairs or recursive DFS accumulating into a result list. Each visited node is appended with its depth before pushing children in reverse order (for a stack) to preserve left-to-right order. Candidate should handle the case of deeply nested trees overflowing a recursive call stack.

Reference implementation (kotlin)

Kotlin
data class Comment(val id: String, val text: String, val replies: List<Comment>)
data class FlatComment(val comment: Comment, val depth: Int)

fun flatten(roots: List<Comment>): List<FlatComment> {
    val result = mutableListOf<FlatComment>()
    val stack = ArrayDeque<Pair<Comment, Int>>()
    roots.reversed().forEach { stack.addLast(it to 0) }
    while (stack.isNotEmpty()) {
        val (node, depth) = stack.removeLast()
        result.add(FlatComment(node, depth))
        node.replies.reversed().forEach { stack.addLast(it to depth + 1) }
    }
    return result
}

Expect these follow-ups

  • How would you make the function lazy so you don't flatten the entire tree upfront for a 10,000-comment thread?
  • How do you handle collapsing/expanding a subtree without re-flattening everything?
algorithmstreeskotlinandroiddata-structures

As asked

Write a NetworkMonitor class in Swift using NWPathMonitor that publishes connectivity status changes as a Combine publisher, and describe where in the app lifecycle you would subscribe to it.

Sample answer outline

Wrap NWPathMonitor in a class that exposes a @Published var isConnected or a PassthroughSubject. Start the monitor on a dedicated DispatchQueue (not the main queue). In pathUpdateHandler, publish the new status. Subscribe in the root ViewModel or AppDelegate. Remember to cancel the monitor when the object is deallocated to avoid a retain cycle with the handler closure.

Reference implementation (swift)

Swift
import Network
import Combine

final class NetworkMonitor: ObservableObject {
    @Published private(set) var isConnected = true
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "net.monitor")

    init() {
        monitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                self?.isConnected = path.status == .satisfied
            }
        }
        monitor.start(queue: queue)
    }

    deinit { monitor.cancel() }
}

Expect these follow-ups

  • What is the difference between NWPathMonitor and SCNetworkReachability, and why does Apple now prefer NWPathMonitor?
  • How would you test code that depends on network reachability?
swiftnetworkingcombineiosnwpathmonitor

As asked

Given a sorted array that has been rotated at an unknown pivot, write a function to find the index of a target value in O(log n) time. This shows up when indexing sorted offline datasets on device.

Sample answer outline

Modified binary search: at each step, determine which half is sorted by comparing arr[left] to arr[mid]. If the left half is sorted and the target is within it, recurse left; otherwise recurse right, and vice versa. The key insight is that at least one half is always sorted in a single rotation. Return -1 if not found.

Reference implementation (kotlin)

Kotlin
fun search(nums: IntArray, target: Int): Int {
    var lo = 0; var hi = nums.size - 1
    while (lo <= hi) {
        val mid = (lo + hi) ushr 1
        if (nums[mid] == target) return mid
        if (nums[lo] <= nums[mid]) { // left half sorted
            if (target in nums[lo] until nums[mid]) hi = mid - 1
            else lo = mid + 1
        } else { // right half sorted
            if (target in (nums[mid] + 1)..nums[hi]) lo = mid + 1
            else hi = mid - 1
        }
    }
    return -1
}

Expect these follow-ups

  • How does the algorithm change if duplicates are allowed in the array?
  • What is the time complexity if the array has been rotated multiple times?
algorithmsbinary-searcharrayskotlinswift

As asked

Given a string and an integer K, find the length of the longest substring that contains at most K distinct characters. This type of sliding window question tests thinking about in-memory string processing on device.

Sample answer outline

Use a sliding window with a HashMap counting character frequencies. Expand the right pointer; if distinct character count exceeds K, shrink from the left, decrementing counts and removing entries at zero. Track the max window size. O(n) time, O(K) space. Candidate should handle edge cases: K=0 (empty result), K >= 26 (whole string is valid).

Reference implementation (swift)

Swift
func lengthOfLongestSubstringKDistinct(_ s: String, _ k: Int) -> Int {
    guard k > 0 else { return 0 }
    let chars = Array(s)
    var freq = [Character: Int](); var left = 0; var best = 0
    for right in 0..<chars.count {
        freq[chars[right], default: 0] += 1
        while freq.count > k {
            let c = chars[left]; left += 1
            freq[c]! -= 1
            if freq[c]! == 0 { freq.removeValue(forKey: c) }
        }
        best = max(best, right - left + 1)
    }
    return best
}

Expect these follow-ups

  • How does this change if K means exactly K distinct characters?
  • What would you do differently if the string is a stream and you cannot hold it all in memory?
algorithmssliding-windowstringsswiftkotlin

As asked

Implement serialize and deserialize for a binary tree, storing it as a string. This models persisting an in-memory tree structure to disk or sending it over a local IPC channel in a mobile app.

Sample answer outline

Pre-order DFS serialization: append node value then recurse left then right, using a sentinel (e.g. '#') for null. Deserialization: split on delimiter, use an index pointer or iterator, recursively build nodes consuming tokens left to right. O(n) both. Candidate should note that level-order (BFS) is an alternative that maps to array representation.

Reference implementation (kotlin)

Kotlin
class TreeNode(var `val`: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun serialize(root: TreeNode?): String {
    if (root == null) return "#"
    return "${root.`val`},${serialize(root.left)},${serialize(root.right)}"
}

fun deserialize(data: String): TreeNode? {
    val tokens = ArrayDeque(data.split(","))
    fun build(): TreeNode? {
        val v = tokens.removeFirst()
        if (v == "#") return null
        return TreeNode(v.toInt(), build(), build())
    }
    return build()
}

Expect these follow-ups

  • How does this compare to using NSKeyedArchiver or Parcelable for serializing a tree on iOS/Android?
  • What if the tree can have very deep nesting and you worry about recursion stack depth?
algorithmstreesserializationkotlindata-structures

As asked

Without using any third-party image library, write a simple image loader that fetches a URL once, caches the bitmap to disk, and serves the cached copy on subsequent requests. Handle concurrent requests for the same URL without downloading it twice.

Sample answer outline

Use a Mutex or ConcurrentHashMap of Deferred<Bitmap> keyed by URL. On first request, create and start a Coroutine, store its Deferred in the map. Concurrent callers await the same Deferred. On resolution, write the decoded Bitmap to a file named by URL hash. On subsequent calls, check the file cache before starting a download.

Reference implementation (kotlin)

Kotlin
class ImageLoader(private val cacheDir: File) {
    private val inFlight = ConcurrentHashMap<String, Deferred<Bitmap>>()
    private val scope = CoroutineScope(Dispatchers.IO)

    suspend fun load(url: String): Bitmap {
        val key = url.md5()
        val file = File(cacheDir, key)
        if (file.exists()) return BitmapFactory.decodeFile(file.path)
        return inFlight.getOrPut(url) {
            scope.async {
                val bytes = URL(url).readBytes()
                file.writeBytes(bytes)
                BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
            }
        }.await().also { inFlight.remove(url) }
    }
}

Expect these follow-ups

  • How would you add an in-memory LRU tier in front of the disk cache?
  • What is the difference between this and what Coil or Glide do internally?
kotlincoroutinesimage-loadingcachingandroid

As asked

Given two words and a word list, find the length of the shortest transformation chain from begin to end, changing one letter at a time, where each intermediate word must be in the list. This models shortest-path routing in a local graph on device.

Sample answer outline

BFS where each node is a word, neighbors are words differing by one character. Pre-process the word list into a map from generic patterns (h*t, ho*) to words for O(26*L) neighbor generation instead of O(N*L). Use a visited set. Return level count when end is dequeued. O(M*L^2) where M is word list size and L is word length.

Reference implementation (swift)

Swift
func ladderLength(_ beginWord: String, _ endWord: String, _ wordList: [String]) -> Int {
    var wordSet = Set(wordList)
    guard wordSet.contains(endWord) else { return 0 }
    var queue = [(beginWord, 1)]
    var i = 0
    while i < queue.count {
        let (word, level) = queue[i]; i += 1
        var chars = Array(word)
        for j in 0..<chars.count {
            let orig = chars[j]
            for c in "abcdefghijklmnopqrstuvwxyz" {
                chars[j] = c
                let next = String(chars)
                if next == endWord { return level + 1 }
                if wordSet.contains(next) { queue.append((next, level+1)); wordSet.remove(next) }
            }
            chars[j] = orig
        }
    }
    return 0
}

Expect these follow-ups

  • How would you reconstruct the actual path, not just the length?
  • How would you speed this up using bidirectional BFS?
algorithmsbfsgraphsswiftstrings

As asked

Write a DownloadQueue in Swift that accepts an arbitrary number of URL download tasks but runs at most N of them concurrently, queuing the rest until a slot opens up.

Sample answer outline

Use an AsyncSemaphore pattern: a TaskGroup or a manual counter inside an actor. Each submitted task awaits the semaphore (or checks active count), executes the download, then signals completion. With Swift Concurrency: withTaskGroup adding up to N children at a time, draining results before adding more. Candidate should handle cancellation propagation through the group.

Reference implementation (swift)

Swift
actor DownloadQueue {
    private let maxConcurrent: Int
    private var active = 0
    private var pending: [CheckedContinuation<Void, Never>] = []

    init(max: Int) { self.maxConcurrent = max }

    private func acquire() async {
        if active < maxConcurrent { active += 1; return }
        await withCheckedContinuation { pending.append($0) }
    }

    private func release() {
        if let next = pending.first { pending.removeFirst(); next.resume() } else { active -= 1 }
    }

    func enqueue(_ work: @escaping () async throws -> Void) async throws {
        await acquire()
        defer { Task { release() } }
        try await work()
    }
}

Expect these follow-ups

  • How does this compare to using OperationQueue with maxConcurrentOperationCount?
  • How would you prioritize certain downloads over others?
swiftconcurrencyasync-awaitnetworkingios