# How Snippets (定型文) Work with Exact-Match Triggers in JapaneseKeyboard

> Learn how JapaneseKeyboard snippets (定型文) use exact-match triggers and strict SQL equality for instant phrase expansion. Boost your typing efficiency now.

- Repository: [Kazu/japanesekeyboard](https://github.com/kazumaproject/japanesekeyboard)
- Tags: how-to-guide
- Published: 2026-03-05

---

**JapaneseKeyboard triggers snippet expansion only when the typed text exactly matches the registered reading, using a Room DAO query with strict SQL equality (`reading = :reading`).**

The **kazumaproject/japanesekeyboard** repository implements a user-defined snippet system—called *定型文* (teikeibun)—that allows frequently used phrases to be inserted via short trigger words. This article explains how the exact-match trigger mechanism works at the database and IME level, including the specific DAO methods, SQL queries, and repository abstractions that enforce strict equality matching.

---

## What Are Snippets (定型文) in JapaneseKeyboard?

Snippets in JapaneseKeyboard are user-managed text templates stored in a local Room database. Each snippet consists of:

- **`word`** – The full text to be inserted (e.g., an email signature or Markdown table).
- **`reading`** – The exact trigger string that, when typed, causes the snippet to appear as a candidate.
- **`enabled`** – A boolean flag determining whether the snippet is active.

These entities are defined in [`UserTemplate.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/UserTemplate.kt) and persisted in the `user_template` table, which is part of `AppDatabase`.

---

## The Exact-Match Trigger Mechanism

The trigger system relies on strict string equality at the SQL level. When the user types a sequence, the input method service queries the database for rows where the `reading` column equals the typed text exactly—no partial matches, no wildcards.

### Database Schema and the UserTemplate Entity

The entity class located at [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt) maps to the following schema:

```kotlin
@Entity(tableName = "user_template")
data class UserTemplate(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "word") val word: String,
    @ColumnInfo(name = "reading") val reading: String,
    @ColumnInfo(name = "enabled") val enabled: Boolean
)

```

The `reading` field is indexed implicitly by the query pattern used in the DAO.

### DAO Query with Strict Equality

The exact-match logic is implemented in [`UserTemplateDao.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/UserTemplateDao.kt) at [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt):

```kotlin
@Dao
interface UserTemplateDao {
    @Query("SELECT * FROM user_template WHERE reading = :reading ORDER BY id DESC LIMIT :limit")
    fun searchByReadingExact(reading: String, limit: Int): LiveData<List<UserTemplate>>

    @Query("SELECT * FROM user_template WHERE reading = :reading ORDER BY id DESC LIMIT :limit")
    suspend fun searchByReadingExactSuspend(reading: String, limit: Int): List<UserTemplate>
}

```

The SQL clause `reading = :reading` enforces exact equality. The `ORDER BY id DESC` ensures that if multiple snippets share the same reading, the most recently added one appears first.

### Repository Layer Abstraction

The [`UserTemplateRepository.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/UserTemplateRepository.kt) at [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt) exposes these queries to the ViewModel and IME:

```kotlin
class UserTemplateRepository @Inject constructor(
    private val userTemplateDao: UserTemplateDao
) {
    fun searchByReadingExact(reading: String, limit: Int = 10): LiveData<List<UserTemplate>> =
        userTemplateDao.searchByReadingExact(reading, limit)

    suspend fun searchByReadingExactSuspend(reading: String, limit: Int = 10): List<UserTemplate> =
        userTemplateDao.searchByReadingExactSuspend(reading, limit)
}

```

The repository provides both `LiveData` (for UI observation) and `suspend` (for coroutine-based IME background threads) variants of the exact-match search.

---

## How the IME Performs the Lookup

When the user types on the keyboard, the input method service (implemented in the main IME class) intercepts the text stream and performs the following steps:

1. **Capture Input** – The current composing text or committed characters are collected into a `String` called `currentReading`.
2. **Exact-Match Query** – The IME calls `userTemplateRepository.searchByReadingExactSuspend(currentReading, limit = 1)` on a background coroutine.
3. **Candidate Generation** – If the DAO returns a non-empty list, the `word` field of the first result is wrapped as a candidate object.
4. **Insertion** – When the user selects the candidate, the IME commits the `word` text, replacing the typed `reading`.

Because the lookup uses `reading = :reading`, typing `htm` will **not** trigger a snippet registered under `html5`. Only when the user completes the exact trigger `html5` does the expansion occur.

---

## Code Examples

### Registering a Snippet with an Exact-Match Trigger

```kotlin
// Inside a ViewModel or Activity
val snippet = UserTemplate(
    id = 0,  // auto-generated
    word = "Thank you for your email. Best regards, ...",
    reading = "tymail",  // exact trigger
    enabled = true
)

// Insert via repository
lifecycleScope.launch {
    userTemplateRepository.insert(snippet)  // calls userTemplateDao.insert
}

```

### Querying for an Exact Match in the IME

```kotlin
suspend fun getSnippetWord(reading: String): String? {
    // Returns null if no exact match exists
    val results = userTemplateRepository.searchByReadingExactSuspend(
        reading = reading,
        limit = 1
    )
    return results.firstOrNull()?.word
}

```

### IME Integration Flow

```kotlin
// Pseudo-code inside the InputMethodService
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    val currentInput = getCurrentInputText()  // e.g., "sig"
    
    // Launch background search
    serviceScope.launch {
        val snippet = userTemplateRepository
            .searchByReadingExactSuspend(currentInput, 1)
            .firstOrNull()
        
        if (snippet != null) {
            showCandidate(snippet.word)  // Display full signature
        }
    }
    return super.onKeyDown(keyCode, event)
}

```

---

## Key Files and Their Roles

| File | Purpose |
|------|---------|
| [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt) | Entity class defining the `word`, `reading`, and `enabled` columns. |
| [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt) | DAO containing `searchByReadingExact` and `searchByReadingExactSuspend` with strict SQL equality. |
| [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt) | Repository exposing exact-match queries to ViewModels and the IME service. |
| [`app/src/main/res/layout/fragment_user_template.xml`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/res/layout/fragment_user_template.xml) | UI layout for adding, editing, and deleting snippets. |
| [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/database/AppDatabase.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/database/AppDatabase.kt) | Room database definition including the `user_template` table. |

---

## Summary

- **Exact-match enforcement** is implemented at the SQL level via `reading = :reading` in `UserTemplateDao.searchByReadingExactSuspend`.
- **Repository abstraction** provides both `LiveData` and coroutine-based access to the exact-match query, used by the UI and IME respectively.
- **Trigger isolation** ensures that partial typing (e.g., `htm`) does not expand a snippet registered under `html5`, preventing accidental insertions.
- **Background execution** allows the input method service to query the Room database off the main thread while the user types.

---

## Frequently Asked Questions

### What happens if I type a partial match of a snippet trigger?

Nothing. The DAO query uses strict equality (`reading = :reading`), so typing a prefix (e.g., `sig` when the trigger is `signature`) returns zero results. The snippet only appears once the exact trigger text is entered.

### Can I use special characters or spaces in the trigger reading?

Yes, but the exact-match logic treats the `reading` field as a literal string. If you register a reading with spaces or symbols (e.g., `my-email`), you must type those exact characters, including case sensitivity, for the trigger to fire.

### How many snippets can I store?

The limit is constrained only by the SQLite database size on the device. The `UserTemplateDao` queries accept a `limit` parameter (defaulting to 10 in the repository), but this only affects how many results are returned for a single reading. You can store hundreds of snippets; the exact-match query remains performant because the `reading` column is effectively filtered by the equality clause.

### Is the exact-match trigger case-sensitive?

Yes. The SQL comparison `reading = :reading` is case-sensitive by default in SQLite (unless a specific collation is applied). Therefore, registering a snippet under `HTML5` will not trigger when you type `html5`. You should register variations separately if you need case-insensitive matching.