# How to Create Custom Keyboard Layouts with Dynamic Keys in Sumire

> Learn to create custom keyboard layouts with dynamic keys in Sumire. Define keyIds, flick actions, and attach states to build your personalized keyboard experience. Get started now!

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

---

**To create custom keyboard layouts with dynamic keys in Sumire, define a unique `keyId` in your XML layout, declare a list of `FlickAction` states in [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt), attach those states to a `KeyData` object, and pass a state index map to `createFinalLayout` in [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt).**

Sumire, the privacy-first Japanese keyboard from the `kazumaproject/japanesekeyboard` repository, generates its interface dynamically at runtime rather than relying on static XML configurations. This architecture allows developers to create custom keyboard layouts with dynamic keys in Sumire that can change their appearance and behavior based on input context, user preferences, or application state.

## Understanding the Dynamic Key Architecture

### The Layout Generation Pipeline

When the user activates Sumire's `TenKeyQWERTYMode`, the system executes a three-stage pipeline defined in [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt) and [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt). First, the service determines the current input mode—`HIRAGANA`, `ENGLISH`, or `SYMBOLS`. Second, it generates a base layout via `KeyboardDefaultLayouts.createFinalLayout`, which accepts parameters for layout type, input style, and delete flick preferences. Third, the system applies dynamic key states by mapping string identifiers to integer indices representing specific visual or functional states.

The `createFinalLayout` function signature in [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt) reveals this dependency:

```kotlin
fun createFinalLayout(
    mode: KeyboardInputMode,
    dynamicKeyStates: Map<String, Int>,
    inputLayoutType: String,
    inputStyle: String,
    isDeleteFlickEnabled: Boolean
): KeyboardLayout

```

### Applying State to Individual Keys

The `applyKeyState` private function handles the actual mutation of key properties. Located in [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt), this method searches the base layout for a key matching the provided `keyId`, retrieves the corresponding state from the key's `dynamicStates` list using the provided index, and returns a modified layout with the updated key configuration.

```kotlin
private fun applyKeyState(
    baseLayout: KeyboardLayout, 
    keyId: String, 
    stateIndex: Int
): KeyboardLayout

```

### Runtime Wiring in the IME Service

The [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt) file serves as the orchestration layer. When constructing the Sumire layout, the service prepares a `dynamicKeyStates` map linking predefined key identifiers to current state indices stored as instance variables:

```kotlin
val dynamicStates = mapOf(
    "enter_key" to currentEnterKeyIndex,
    "dakuten_toggle_key" to currentDakutenKeyIndex,
    "space_convert_key" to currentSpaceKeyIndex,
    "katakana_toggle_key" to currentKatakanaKeyIndex
)

```

For incremental updates without full layout reconstruction, the service calls `updateDynamicKey` on the `customLayoutDefault` view:

```kotlin
mainLayoutBinding?.customLayoutDefault?.apply {
    updateDynamicKey(keyId = "enter_key", stateIndex = currentEnterKeyIndex)
    updateDynamicKey(keyId = "dakuten_toggle_key", stateIndex = currentDakutenKeyIndex)
    updateDynamicKey(keyId = "space_convert_key", stateIndex = currentSpaceKeyIndex)
    updateDynamicKey(keyId = "katakana_toggle_key", stateIndex = currentKatakanaKeyIndex)
}

```

## Adding Your Own Dynamic Key to Sumire

### Step 1: Define the Key in XML Layout

Locate your base layout XML file, typically found in [`tenkey/src/main/res/layout/keyboard_layout.xml`](https://github.com/kazumaproject/japanesekeyboard/blob/main/tenkey/src/main/res/layout/keyboard_layout.xml) or a similar path within the `custom_keyboard` module. Add a button element with a unique `android:keyId` attribute that the runtime system can target:

```xml
<androidx.appcompat.widget.AppCompatButton
    android:id="@+id/key_quick_emoji"
    android:keyId="quick_emoji_key"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginVertical="@dimen/key_margin_vertical_size"
    app:layout_constraintTop_toTopOf="parent" />

```

### Step 2: Declare the State List

In [`custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/layout/KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/layout/KeyboardDefaultLayouts.kt), define a private list containing the possible states for your new key. Each state maps to a `FlickAction` that specifies the action performed and the visual label displayed:

```kotlin
private val quickEmojiStates = listOf(
    FlickAction.Action(KeyAction.InputText("😀"), label = "😀"),
    FlickAction.Action(KeyAction.InputText("😂"), label = "😂"),
    FlickAction.Action(KeyAction.InputText("👍"), label = "👍")
)

```

### Step 3: Attach States to the Key Data

Within the layout factory method that constructs your base keyboard (such as `createHiraganaToggleLayout` or `createEnglishLayout`), instantiate a `KeyData` object for your custom key. Assign the `dynamicStates` property to the list created in Step 2:

```kotlin
val quickEmojiKey = KeyData(
    keyId = "quick_emoji_key",
    label = "", // Populated dynamically by the state
    action = KeyAction.InputText(""), // Placeholder
    dynamicStates = quickEmojiStates
)
keys.add(quickEmojiKey)

```

### Step 4: Expose the Key to the Runtime Map

In [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt), extend the `dynamicStates` map to include your new key. The integer value represents the default state index to display when the keyboard initializes:

```kotlin
val dynamicStates = mapOf(
    "enter_key" to currentEnterKeyIndex,
    "dakuten_toggle_key" to currentDakutenKeyIndex,
    "space_convert_key" to currentSpaceKeyIndex,
    "katakana_toggle_key" to currentKatakanaKeyIndex,
    "quick_emoji_key" to 0   // Default to first emoji (😀)
)

```

To update the key dynamically without rebuilding the entire layout, use the `updateDynamicKey` method on the `customLayoutDefault` view:

```kotlin
mainLayoutBinding?.customLayoutDefault?.updateDynamicKey(
    keyId = "quick_emoji_key", 
    stateIndex = 2  // Switch to 👍
)

```

## Full Minimal Example

The following implementation demonstrates a complete custom dynamic key that cycles through three emoji options:

```kotlin
// 1️⃣ XML (res/layout/keyboard_layout.xml)
<androidx.appcompat.widget.AppCompatButton
    android:keyId="quick_emoji_key"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

// 2️⃣ KeyboardDefaultLayouts.kt – define states
private val quickEmojiStates = listOf(
    FlickAction.Action(KeyAction.InputText("😀"), label = "😀"),
    FlickAction.Action(KeyAction.InputText("😂"), label = "😂"),
    FlickAction.Action(KeyAction.InputText("👍"), label = "👍")
)

// 3️⃣ Add the key to a base layout (illustrated inside createHiraganaToggleLayout)
val quickEmojiKey = KeyData(
    keyId = "quick_emoji_key",
    label = "",   // will be replaced dynamically
    action = KeyAction.InputText(""),
    dynamicStates = quickEmojiStates
)
keys.add(quickEmojiKey)

// 4️⃣ IMEService – inject the dynamic state when Sumire is active
val dynamicStates = mapOf(
    "enter_key"          to currentEnterKeyIndex,
    "dakuten_toggle_key" to currentDakutenKeyIndex,
    "space_convert_key"  to currentSpaceKeyIndex,
    "katakana_toggle_key" to currentKatakanaKeyIndex,
    "quick_emoji_key"    to 1   // show "😂" by default
)

val finalLayout = KeyboardDefaultLayouts.createFinalLayout(
    mode = customKeyboardMode,
    dynamicKeyStates = dynamicStates,
    inputLayoutType = sumireInputKeyLayoutType ?: "toggle",
    inputStyle = sumireInputStyle ?: "default",
    isDeleteFlickEnabled = isDeleteLeftFlickPreference ?: true
)
mainLayoutBinding?.customLayoutDefault?.setKeyboard(finalLayout)

```

**Result**: The **quick-emoji** key appears on the Sumire keyboard, showing the second emoji (`😂`). Changing `dynamicStates["quick_emoji_key"]` to `2` and calling `updateDynamicKey` will instantly switch the icon to 👍.

## Key Files to Reference

| File | Purpose | Direct link |
|------|---------|-------------|
| [`custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/layout/KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/layout/KeyboardDefaultLayouts.kt) | Layout factories, `createFinalLayout`, `applyKeyState`, state-list definitions | [KeyboardDefaultLayouts.kt](https://github.com/kazumaproject/japanesekeyboard/blob/master/custom_keyboard/src/main/java/com/kazumaproject/custom_keyboard/layout/KeyboardDefaultLayouts.kt) |
| [`app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt) | Runtime mode handling, dynamic map construction, UI updates | [IMEService.kt](https://github.com/kazumaproject/japanesekeyboard/blob/master/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt) |
| [`tenkey/src/main/res/layout/keyboard_layout.xml`](https://github.com/kazumaproject/japanesekeyboard/blob/main/tenkey/src/main/res/layout/keyboard_layout.xml) (or any layout XML) | Visual definitions of keys, including `android:keyId` that can be targeted dynamically | [keyboard_layout.xml](https://github.com/kazumaproject/japanesekeyboard/blob/master/tenkey/src/main/res/layout/keyboard_layout.xml) |
| [`core/src/main/res/values/strings.xml`](https://github.com/kazumaproject/japanesekeyboard/blob/main/core/src/main/res/values/strings.xml) | Human-readable strings for key labels (optional) | [strings.xml](https://github.com/kazumaproject/japanesekeyboard/blob/master/core/src/main/res/values/strings.xml) |

## Summary

- **Sumire** generates keyboards dynamically at runtime through `KeyboardDefaultLayouts.createFinalLayout`, accepting a `Map<String, Int>` that maps key IDs to state indices.
- **Dynamic states** are defined as lists of `FlickAction` objects attached to `KeyData` instances, allowing a single physical key to cycle through multiple labels and actions.
- **Implementation** requires four steps: defining the XML layout with `android:keyId`, declaring state lists in [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt), attaching those states to `KeyData` objects, and wiring the key into the `dynamicKeyStates` map within [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt).
- **Runtime updates** use `updateDynamicKey` on the `customLayoutDefault` view to change key appearance without full layout reconstruction, optimizing performance during mode switches.

## Frequently Asked Questions

### How do I change a dynamic key's appearance based on user input context?

Update the integer value associated with your key ID in the `dynamicKeyStates` map, then call `updateDynamicKey` on the `customLayoutDefault` view with the new state index. This method, located in [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt), modifies the key label and action immediately without rebuilding the entire keyboard layout, making it ideal for context-sensitive changes like switching between emoji categories or input modes.

### What is the difference between `createFinalLayout` and `updateDynamicKey`?

`createFinalLayout`, defined in [`KeyboardDefaultLayouts.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardDefaultLayouts.kt), constructs an entirely new `KeyboardLayout` object by applying all dynamic states from the provided map to a base layout. Use this when initializing the keyboard or changing fundamental layout structures. In contrast, `updateDynamicKey` is a view-level method that updates a single key's appearance and behavior on the existing UI, offering better performance for incremental state changes during typing sessions.

### Can I use dynamic keys with flick gestures or toggle input styles?

Yes. The dynamic key system in Sumire is independent of the input style mechanism. When calling `createFinalLayout`, you specify the `inputLayoutType` (such as "toggle" or "flick") and `inputStyle` separately from the `dynamicKeyStates` map. The base layout factory methods—like `createHiraganaToggleLayout` or flick variants—handle the gesture logic, while `applyKeyState` overlays your dynamic configuration on top, allowing flick gestures and dynamic key states to coexist.

### Where should I store user preferences for dynamic key states?

Store persistent user preferences in Android's `SharedPreferences` or a datastore, then read these values when constructing the `dynamicKeyStates` map in [`IMEService.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/IMEService.kt). For example, if users can customize their default emoji, save the selected index to preferences and retrieve it as `val emojiIndex = prefs.getInt("quick_emoji_default", 0)` when building the map. This ensures the keyboard reflects user choices across sessions while maintaining the dynamic update capability via `updateDynamicKey` for real-time changes.