# How to Debug and Test Custom Keyboard Layouts with the FlickKeyboardView API

> Debug and test custom keyboard layouts using FlickKeyboardView API. Build layouts, attach them, use Logcat, and perform JVM and Espresso tests for robust validation.

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

---

**To debug and test custom keyboard layouts with the FlickKeyboardView API, build a `KeyboardLayout` with your `KeyData` definitions, attach it via `setKeyboard()`, enable verbose Logcat logging for runtime inspection, and validate behavior through JVM unit tests for layout logic and Espresso instrumentation tests for flick gestures.**

The `FlickKeyboardView` class in the **kazumaproject/japanesekeyboard** repository provides the core UI component for rendering Japanese flick-style keyboards. When you debug and test custom keyboard layouts with the FlickKeyboardView API, you interact with a `GridLayout`-based container that dynamically constructs key views, attaches specialized flick controllers, and manages runtime state mutations through the `dynamicKeyMap`.

## Understanding the FlickKeyboardView Architecture

Before debugging or testing, you must understand how data models convert into interactive views. The architecture cleanly separates layout definitions from view construction and gesture handling.

### Core Components

The system consists of these primary classes located in the `custom_keyboard` module:

- **`FlickKeyboardView`**: The main `GridLayout` container defined in [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt) (lines 49-71) that manages key view creation, theme application, and stores the current `KeyboardLayout` instance. It maintains a `dynamicKeyMap` (lines 84-92) for runtime key updates.
- **`KeyboardLayout`**: A data class in [`KeyboardLayout.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardLayout.kt) that defines `rowCount`, `columnCount`, the list of `KeyData` objects, and the `flickKeyMaps` that map directions to actions for specific keys.
- **`KeyData`**: Defined in [`KeyData.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyData.kt), this class specifies visual properties (label, drawable), behavioral `KeyType` (STANDARD_FLICK, CIRCULAR_FLICK, etc.), grid position, and optional `dynamicStates` for keys that change appearance at runtime.
- **Flick Controllers**: Classes such as [`CustomAngleFlickController.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/CustomAngleFlickController.kt), [`StandardFlickInputController.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/StandardFlickInputController.kt), [`CrossFlickInputController.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/CrossFlickInputController.kt), and [`GridFlickInputController.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/GridFlickInputController.kt) implement gesture detection algorithms and popup rendering.
- **`OnKeyboardActionListener`**: Interface defined within [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt) (lines 53-60) that the host activity or IME implements to receive key text, action events, and flick-direction changes.

### How Key Views Are Constructed

When you invoke `setKeyboard(layout)` in [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt) (lines 203-246), the view executes this sequence:

1. Clears previous child views and controller instances via the layout clearing logic.
2. Stores the new `KeyboardLayout` reference.
3. Iterates through `layout.keys`, calling `createKeyView(keyData)` to build either an `AppCompatImageButton` (for icon keys) or `AutoSizeButton` (for text keys).
4. Invokes `attachKeyBehavior(keyView, keyData)` to instantiate the appropriate flick controller based on `KeyType`, wiring its listener back to the view's `OnKeyboardActionListener`.
5. Populates `dynamicKeyMap` (lines 84-92) for any keys containing a `keyId`, enabling future runtime updates.

## Debugging Custom Keyboard Layouts

Effective debugging requires visibility into the view's internal state and the ability to manipulate keys at runtime without full reconstruction.

### Enable Verbose Logging

The `FlickKeyboardView` and controller classes emit debug logs at critical lifecycle points. Enable comprehensive logging in your debug build to trace layout rebuilds and controller attachments:

```kotlin
if (BuildConfig.DEBUG) {
    Log.d("FlickKeyboardView", "Debug mode – full logging enabled")
}

```

Monitor Logcat for tags including `FlickKeyboardView`, `CustomAngleFlickController`, and `StandardFlickInputController` to trace when `setKeyboard()` rebuilds the grid, when controllers attach to specific keys, and when flick gestures trigger callbacks.

### Inspect Runtime State with dynamicKeyMap

The `dynamicKeyMap` property (lines 84-92 in [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt)) stores metadata for keys that support state changes. You can inspect this map at runtime to verify key configurations and debug why updates might fail:

```kotlin
fun FlickKeyboardView.dumpDynamicKeys() {
    dynamicKeyMap.forEach { (id, info) ->
        Log.d("FlickDebug", "KeyId=$id label=${info.keyData.label} action=${info.keyData.action}")
    }
}

```

This is particularly useful when debugging why `updateDynamicKey` might not affect the expected key, as it confirms whether the `keyId` exists in the map and what state it currently holds.

### Step-Through Dynamic Key Updates

The `updateDynamicKey(keyId, stateIndex)` method allows runtime mutation of key appearance and behavior without rebuilding the entire keyboard. The method executes this sequence in [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt):

1. Retrieves the stored `KeyInfo` from `dynamicKeyMap`.
2. Constructs a new `KeyData` instance with the specified state index.
3. Determines if the view type must change (icon versus text) – lines 76-78.
4. Detaches the existing controller via `detachKeyBehavior`.
5. Attaches a new controller via `attachKeyBehavior` with the updated `KeyData`.
6. If the view type changed, replaces the view in the GridLayout while preserving layout parameters – lines 82-89.

### Common Debugging Pitfalls

| Symptom | Likely Cause | Fix |
|---|---|---|
| Flick pop-ups never appear | Controller not attached due to missing `flickKeyMaps` entry | Verify `layout.flickKeyMaps[keyData.label]` contains valid direction mappings for the specific key label. |
| Wrong label after `updateDynamicKey` | Stale `dynamicKeyMap` entry or wrong view instance | Ensure you call `updateDynamicKey` on the same `FlickKeyboardView` instance that executed `setKeyboard`, and verify the `keyId` matches exactly. |
| Theme colors not applied | `applyKeyboardTheme` called after `setKeyboard` without rebuild | Either call `applyKeyboardTheme` before `setKeyboard`, or re-invoke `setKeyboard` after theme changes to force view reconstruction with new theme attributes. |

## Testing Custom Keyboard Layouts

Comprehensive testing validates both the data structure correctness and the interactive gesture behavior across different scenarios.

### Unit Testing Layout Construction

Test layout logic on the JVM without Android runtime dependencies. The `KeyboardLayout` and `KeyData` classes are pure Kotlin data classes suitable for unit testing, while view construction requires a Robolectric or AndroidX test context.

```kotlin
class FlickKeyboardViewTest {

    private lateinit var context: Context
    private lateinit var view: FlickKeyboardView

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        view = FlickKeyboardView(context)
    }

    @Test
    fun `setKeyboard builds correct number of child views`() {
        val layout = KeyboardLayout(
            rowCount = 2,
            columnCount = 3,
            keys = listOf(
                KeyData(
                    label = "A",
                    keyType = KeyType.STANDARD_FLICK,
                    row = 0,
                    column = 0,
                    rowSpan = 1,
                    colSpan = 1,
                    keyId = null
                )
            ),
            flickKeyMaps = emptyMap()
        )

        view.setKeyboard(layout)

        assertEquals(1, view.childCount)
        val firstKey = view.getChildAt(0) as Button
        assertEquals("A", firstKey.text)
    }
}

```

Use `ApplicationProvider` from `androidx.test:core` to obtain a context without launching an activity, allowing fast JVM test execution.

### Instrumentation Testing for Flick Gestures

Validate flick interactions using Espresso in an Android instrumentation test. Launch a test activity, inject a layout with defined flick maps, and simulate directional swipes.

```kotlin
@RunWith(AndroidJUnit4::class)
class FlickInteractionTest {

    @Rule @JvmField 
    val activityRule = ActivityScenarioRule<TestKeyboardActivity>()

    @Test
    fun `circular flick triggers correct character`() {
        activityRule.scenario.onActivity { activity ->
            val layout = KeyboardLayout(
                rowCount = 1,
                columnCount = 1,
                keys = listOf(
                    KeyData(
                        label = "へ",
                        keyType = KeyType.CIRCULAR_FLICK,
                        row = 0,
                        column = 0,
                        rowSpan = 1,
                        colSpan = 1
                    )
                ),
                flickKeyMaps = mapOf(
                    "へ" to listOf(
                        mapOf(
                            FlickDirection.UP to FlickAction.Input("へ"),
                            FlickDirection.RIGHT to FlickAction.Input("べ")
                        )
                    )
                )
            )
            activity.keyboardView.setKeyboard(layout)
        }

        onView(withId(R.id.flickKeyboardView))
            .perform(swipeFromCenterTo(Direction.UP, 150))

        onView(withId(R.id.outputTextView))
            .check(matches(withText("へ")))
    }
}

```

Implement `swipeFromCenterTo` using Espresso's `GeneralSwipeAction` to simulate precise directional movements from the center of the target view.

### Validating Dynamic Key Updates

Test state transitions for keys with multiple states using `updateDynamicKey` to ensure the view updates without reconstruction.

```kotlin
@Test
fun `updateDynamicKey changes label and action`() {
    val enterKey = KeyData(
        label = "Enter",
        keyType = KeyType.NORMAL,
        row = 0,
        column = 2,
        keyId = "enter_key",
        dynamicStates = listOf(
            KeyData.State(label = "Enter", action = KeyAction.Enter),
            KeyData.State(label = "↵", action = KeyAction.NewLine)
        )
    )
    
    val layout = KeyboardLayout(
        rowCount = 1,
        columnCount = 3,
        keys = listOf(enterKey),
        flickKeyMaps = emptyMap()
    )

    view.setKeyboard(layout)

    val btn = view.getChildAt(0) as Button
    assertEquals("Enter", btn.text)

    view.updateDynamicKey("enter_key", 1)
    assertEquals("↵", btn.text)
}

```

## Summary

- **FlickKeyboardView** acts as the `GridLayout` container that builds key views from `KeyboardLayout` definitions and manages flick controllers according to `KeyType`.
- **Debug** by enabling Logcat logging for `FlickKeyboardView` and controller tags, inspecting `dynamicKeyMap` at runtime, and stepping through `updateDynamicKey` for state mutations.
- **Test** layout construction with JVM unit tests using `ApplicationProvider`, validate flick gestures with Espresso instrumentation tests simulating directional swipes, and verify dynamic key state changes through assertions on view properties.
- Reference specific source files like [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt) (lines 49-71, 84-92, 203-246) and controller implementations to ground your debugging in the actual implementation.

## Frequently Asked Questions

### How do I enable debug logging for FlickKeyboardView?

Enable verbose logging by filtering Logcat for tags `FlickKeyboardView`, `CustomAngleFlickController`, and `StandardFlickInputController`. The source code in [`FlickKeyboardView.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/FlickKeyboardView.kt) already includes `Log.d` calls at critical points such as layout rebuilds in `setKeyboard()` and controller attachments. For additional tracing, wrap your `OnKeyboardActionListener` implementations with log statements to capture key text and flick direction callbacks.

### What is the difference between KeyboardLayout and KeyData?

`KeyboardLayout` is a data class defined in [`KeyboardLayout.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyboardLayout.kt) that describes the overall keyboard structure, including `rowCount`, `columnCount`, the list of `KeyData` objects, and the `flickKeyMaps` that define flick directions for specific keys. `KeyData`, defined in [`KeyData.kt`](https://github.com/kazumaproject/japanesekeyboard/blob/main/KeyData.kt), represents individual key properties such as label, `keyType` (STANDARD_FLICK, CIRCULAR_FLICK, etc.), grid position coordinates, and optional `dynamicStates` for keys that change appearance or behavior at runtime.

### How can I test flick gestures without a physical device?

Use Android instrumentation tests with Espresso to simulate flick gestures on an emulator or test device. Create a test activity that hosts `FlickKeyboardView`, inject a `KeyboardLayout` with defined `flickKeyMaps`, then use a custom `GeneralSwipeAction` to perform directional swipes from the center of specific keys. Verify the results by checking that your test activity's `OnKeyboardActionListener` received the expected text or action callbacks, or by asserting on the state of an output view.

### Why does my dynamic key not update after calling updateDynamicKey?

The most common cause is calling `updateDynamicKey` on a different `FlickKeyboardView` instance than the one that built the layout, or providing a `keyId` that does not match the `keyId` assigned in the original `KeyData`. Additionally, if the `KeyData` lacks `dynamicStates` or the `stateIndex` exceeds the available states, the update will fail silently. Verify that the key exists in `dynamicKeyMap` by dumping its contents at runtime, and ensure you are using the same view instance that originally called `setKeyboard`.