# How the Search Feature Implements History and Suggestions in GSYGitHubAppCompose

> Discover how GSYGitHubAppCompose search implements history and suggestions using Room and ViewModel. Learn how past queries become clickable suggestions for a seamless user experience.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: internals
- Published: 2026-02-26

---

**The GSYGitHubAppCompose search feature persists the last 10 queries in a Room database, exposes them as reactive state through a ViewModel, and renders them as clickable suggestions when the search field is focused and empty.**

The open-source GSYGitHubAppCompose project demonstrates a modern Android search implementation using Jetpack Compose and Room. This article breaks down exactly how the search feature implements history and suggestions across the data, domain, and UI layers, referencing the actual Kotlin source code from the repository.

## Data Layer: Room Database for Persistent Storage

The persistence layer uses Room to store search terms locally, enforcing a maximum of 10 entries to keep storage bounded.

### Entity Definition

The `SearchHistoryEntity` in [`core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/entity/SearchHistoryEntity.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/entity/SearchHistoryEntity.kt) defines a simple table structure where the query string serves as the primary key and a timestamp tracks recency.

```kotlin
@Entity(tableName = "search_history")
data class SearchHistoryEntity(
    @PrimaryKey val query: String,
    val timestamp: Long
)

```

### Data Access Object

The `SearchHistoryDao` in [`core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/dao/SearchHistoryDao.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/database/src/main/java/com/shuyu/gsygithubappcompose/core/database/dao/SearchHistoryDao.kt) provides three critical operations: insert with conflict replacement, retrieval of the latest 10 entries, and cleanup of older records.

```kotlin
@Dao
interface SearchHistoryDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertSearchHistory(searchHistoryEntity: SearchHistoryEntity)

    @Query("SELECT * FROM search_history ORDER BY timestamp DESC LIMIT 10")
    fun getSearchHistory(): Flow<List<SearchHistoryEntity>>

    @Query("DELETE FROM search_history WHERE query NOT IN (SELECT query FROM search_history ORDER BY timestamp DESC LIMIT 10)")
    suspend fun deleteOldSearchHistory()
}

```

### Repository Wrapper

The `SearchHistoryRepository` in [`data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/SearchHistoryRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/SearchHistoryRepository.kt) exposes a clean API for the ViewModel, handling the transaction of inserting a new query and immediately pruning excess entries.

```kotlin
fun getSearchHistory(): Flow<List<SearchHistoryEntity>> = searchHistoryDao.getSearchHistory()

suspend fun saveSearchQuery(query: String) {
    val entity = SearchHistoryEntity(query = query, timestamp = System.currentTimeMillis())
    searchHistoryDao.insertSearchHistory(entity)
    searchHistoryDao.deleteOldSearchHistory()
}

```

## Domain Layer: ViewModel State Management

The `SearchViewModel` in [`feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchViewModel.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchViewModel.kt) bridges the data layer and UI, exposing search history as a `StateFlow` that survives configuration changes.

### Collecting History on Initialization

The ViewModel subscribes to the repository's `Flow` in its `init` block, converting the database stream into observable state.

```kotlin
init {
    searchHistoryRepository.getSearchHistory()
        .onEach { _searchHistory.value = it }
        .launchIn(viewModelScope)
}

```

### Saving Queries After Successful Searches

To avoid polluting history with incomplete or failed attempts, the ViewModel only persists queries after a first-page search succeeds. The `saveSearchQuery` method launches a coroutine to handle the database write asynchronously.

```kotlin
if (page == 1) {
    saveSearchQuery(_searchQuery.value)
}

private fun saveSearchQuery(query: String) {
    viewModelScope.launch {
        searchHistoryRepository.saveSearchQuery(query)
    }
}

```

## UI Layer: Jetpack Compose Implementation

The `SearchScreen` in [`feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt) conditionally renders the history list based on user focus state and input content.

### Conditional Suggestion Display

The UI shows a `LazyColumn` of suggestions only when three conditions are met: the search field has focus, the query text is blank, and history items exist.

```kotlin
if (isSearchFieldFocused && searchQuery.isBlank() && searchHistory.isNotEmpty()) {
    LazyColumn {
        items(searchHistory) { historyItem ->
            Text(
                text = historyItem.query,
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable {
                        searchViewModel.onSearchQueryChanged(historyItem.query)
                        searchViewModel.performSearch()
                        focusManager.clearFocus()
                    }
                    .padding(8.dp)
            )
            HorizontalDivider()
        }
    }
}

```

### Handling Suggestion Clicks

When a user taps a history item, the app populates the search field, executes the search, and clears focus to dismiss the suggestion list and reveal results.

## Summary

- **Room database** stores queries with timestamps, using `query` as the primary key to automatically update existing entries via `OnConflictStrategy.REPLACE`.
- **DAO limits** history to 10 items through SQL `LIMIT 10` clauses in both the select query and the cleanup delete statement.
- **ViewModel exposes** history as a `StateFlow` collected in the `init` block, ensuring the UI always reflects current data.
- **Persistence timing** occurs only after successful first-page searches, preventing storage of typo-ridden or abandoned queries.
- **Compose UI** displays suggestions only when the input is focused and empty, optimizing screen real estate and user flow.

## Frequently Asked Questions

### How does the app limit search history to 10 items?

The `SearchHistoryDao` enforces this limit at the database level. The `getSearchHistory()` method uses `LIMIT 10` in its SQL query, while `deleteOldSearchHistory()` removes any rows not present in the top 10 most recent entries. This ensures the database never stores more than 10 records regardless of UI state.

### When exactly does a search query get saved to history?

The `SearchViewModel` saves a query only after the first page of search results returns successfully. Inside the search execution logic, the code checks `if (page == 1)` before calling `saveSearchQuery()`, ensuring that pagination requests or failed initial searches do not create history entries.

### What triggers the display of search suggestions on screen?

Three conditions must be satisfied simultaneously: the search text field must have focus (`isSearchFieldFocused`), the query string must be blank (`searchQuery.isBlank()`), and the history list must contain at least one item (`searchHistory.isNotEmpty()`). When these align, the `LazyColumn` renders the suggestion list.

### Why does the DAO use `OnConflictStrategy.REPLACE` for inserts?

This strategy ensures that searching for an existing term updates its timestamp rather than creating a duplicate entry. Since `query` is the primary key, attempting to insert an existing query triggers the replace operation, effectively moving that term to the top of the history list without manual deletion logic.