We provide end-to-end development of autocomplete search in mobile apps: query architecture, debounce, cancellation of stale requests, caching of suggestions, search history, and highlight of matches. The task is solved on Swift, Kotlin, Dart, and React Native. From our practice: in one project switching to autocomplete reduced time to first result from 2.1 seconds to 0.4 seconds — search conversion increased by 18%.
Two main pain points are race conditions and keyboard overlap. Race condition: a response to an outdated request arrives later than a new one and overwrites the suggestion list. The solution — flatMapLatest (Android) or switchToLatest() (iOS): upon a new character, the previous request is automatically cancelled. The keyboard overlaps suggestions if the list's bottom anchor is attached to the screen rather than to the keyboard. We fix this via KeyboardAvoiding (RN) or WindowInsetsCompat (Android), discussing this detail with the designer before writing code.
Developing such search covers several technical stages: architecture selection, debounce, cache, history, and highlighting. Below is the specific mechanics of each with code examples.
Choosing the Approach: Client-Side or Server-Side Search?
The choice of search architecture determines development time and offline behavior. Three working options:
| Approach | When to Use | Stack | Response Time |
|---|---|---|---|
| Client-Side | Dataset up to 5,000 records | fuse.js (RN), fuse_dart (Flutter), Room FTS4 (Android) | <50 ms |
| Server-Side | Large catalog, personalization | REST/GraphQL + debounce | 100–400 ms |
| Combined | eCommerce, marketplace | Offline cache + API fallback | <100 ms |
Client-side search via fuse.js is faster than server-side for small datasets and requires no network. The combined approach is more effective for eCommerce: instant response from local cache, background API refresh.
Debounce and Cancellation of Stale Requests
Without debounce, the search sends a request for every typed character. At a typing speed of 3–5 characters per second — that's 3–5 extra network calls. Responses arrive out of order, the suggestion list flickers. A 300 ms debounce eliminates the problem: the request is sent only after a pause in typing.
// iOS: debounce via Combine @Published var query = "" cancellable = $query .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .flatMap { [weak self] q in self?.fetchSuggestions(q) ?? Empty().eraseToAnyPublisher() } .sink { [weak self] in self?.suggestions = $0 } // Android: debounce via Flow + coroutines searchFlow .debounce(300) .distinctUntilChanged() .flatMapLatest { query -> suggestionsUseCase(query) } .collect { suggestions = it } flatMapLatest on Android and its counterpart on iOS automatically cancel the stale request on each new input, without manual cancellation token management.
Caching Search Suggestions
Results of popular queries are cached on the device. We use an LRU cache with 100–200 entries: on repeated input the response comes instantly from memory, without an API call. Traffic savings — from 40% to 70% for apps with recurring queries.
Example LRU cache for suggestions in Kotlin
private val cache = object : LinkedHashMap<String, List<String>>(200, 0.75f, true) { override fun removeEldestEntry(eldest: Map.Entry<String, List<String>>) = size > 200 } fun getSuggestions(query: String): List<String>? = cache[query.lowercase()] fun putSuggestions(query: String, results: List<String>) { cache[query.lowercase()] = results } For offline access, we extend the cache to Room FTS4 on Android or CoreData on iOS — the user gets suggestions even without a network.
Search History
We store the last 10–15 queries in local storage. Display them with a clock icon above the API suggestions, de-duplicate by text. A clear history button is mandatory: users expect it.
| Platform | Storage | Limit |
|---|---|---|
| iOS | UserDefaults | 15 entries |
| Android | DataStore / SharedPreferences | 15 entries |
| React Native | AsyncStorage | 10 entries |
| Flutter | shared_preferences | 15 entries |
When a suggestion from history is selected, we save the query again to update the order, then trigger a full search for the selected text.
Highlighting Matches in Results
We split the suggestion text into parts: the matching fragment is highlighted in color. On Android — SpannableString with ForegroundColorSpan, on iOS — NSAttributedString, on Flutter — RichText with TextSpan. Highlighting is case-insensitive and accounts for multiple occurrences in one line.
// iOS: highlight via NSAttributedString func highlight(_ text: String, query: String) -> NSAttributedString { let attr = NSMutableAttributedString(string: text) var searchRange = text.startIndex..<text.endIndex while let range = text.range(of: query, options: .caseInsensitive, range: searchRange) { attr.addAttribute(.foregroundColor, value: UIColor.systemBlue, range: NSRange(range, in: text)) searchRange = range.upperBound..<text.endIndex } return attr } Important to handle an empty query string: when the field is empty, suggestions are not shown or only history is displayed.
What's Included in Search Development?
We perform the full cycle of work turnkey:
- Architectural analysis: choice of approach (client-side, server-side, combined)
- Implementation of debounce logic and request cancellation
- Caching of search suggestions: LRU in-memory and persistent layer
- Search history with deduplication and clear button
- Highlighting matches on native components
- Handling edge cases: empty query, offline, API errors
- Testing under slow connection and without network
- Documentation and delivery of source code with usage examples
We estimate the project for free — drop us a message, we'll calculate the cost and timeline.
From Our Practice
Our client — a building materials marketplace — complained about slow search: 1.5–2 seconds response on each character. We implemented a combined approach: fuse.js on an offline cache with 3,000 SKUs plus API for non-standard queries. Result: average response time 60 ms, search abandonment decreased by 34%, completed orders increased.
In another project — a medical app on iOS and Android — we added search history and highlighting. Time to select a drug was reduced from 8 to 4 screen touches.
Our experience — over 50 delivered mobile projects, 6 years in native and cross-platform development. We guarantee deadlines and code quality.
Timelines
| Scope | Timeline |
|---|---|
| Basic search: debounce + API + suggestions | 2–3 days |
| Extended: history + cache + highlighting | 3–4 days |
| Full package: offline FTS + RN/Flutter | 4–5 days |
The cost is determined after analysis. Leave a request — we'll calculate the timeline and cost for your project.
The standard Apple UISearchController covers basic search, but custom autocomplete gives you control over ranking algorithm, offline mode, and suggestion design.







