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

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 (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 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, 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, StandardFlickInputController.kt, CrossFlickInputController.kt, and GridFlickInputController.kt implement gesture detection algorithms and popup rendering.
  • OnKeyboardActionListener: Interface defined within 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 (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:

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

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:

  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.

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.

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

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

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 →