How to Create Custom Keyboard Layouts with Dynamic Keys in Sumire

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, attach those states to a KeyData object, and pass a state index map to createFinalLayout in 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 and 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 reveals this dependency:

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, 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.

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

Runtime Wiring in the IME Service

The 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:

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:

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 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:

<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, 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:

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:

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, extend the dynamicStates map to include your new key. The integer value represents the default state index to display when the keyboard initializes:

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:

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:

// 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 Layout factories, createFinalLayout, applyKeyState, state-list definitions KeyboardDefaultLayouts.kt
app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt Runtime mode handling, dynamic map construction, UI updates IMEService.kt
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
core/src/main/res/values/strings.xml Human-readable strings for key labels (optional) 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, attaching those states to KeyData objects, and wiring the key into the dynamicKeyStates map within 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, 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, 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →