I will give a detailed guide on creating one without using any deprecated APIs.
First, let's start with the InputMethodService implementation, you can use the onCreateInputView() method to display the main keyboard layout. It needs you to return a View that will be displayed on the screen when the service starts. So you can create a Keyboard layout in res/layout/keyboard_view.xml:
public class ControlBoard extends InputMethodService {
 
     View mainKeyboardView;
     boolean shiftPressed = false; // shiftPressed and metaState will be used for tracking shift key
     int metaState = 0; 
     @Override
     public View onCreateInputView() {
         
         mainKeyboardView = getLayoutInflater().inflate(R.layout.keyboard_view, null);
         return mainKeyboardView;
     }
     .....
To create the layout, you can use whatever you want, but I will show you how to do it with LinearLayouts. You can use one LinearLayout as the root element with orientation:vertical, and then nest more LinearLayouts with orientation:horizontal to create rows, with Buttons. For storing the keycodes, you can use the android:tag attribute in the Buttons. For styles, you can apply theme to the root element or style to each Button.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/keyboard_view"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:background="@drawable/keyboard_bg"
    >
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        >
        
     <!-- 
     You can define the keycodes in "android:tag" using Unicode scheme or KeyEvent.KEYCODE_ scheme.
     I will use the latter because it is easier to implement and allows greater features,
     such as handling all special keys simultaneously and using meta keys like ctrl or alt. -->
        <Button style="@style/NormalKeyStyles" android:tag="SHIFT" android:text="SHFT" />
        <Button style="@style/NormalKeyStyles" android:tag="G" android:text="g" />
        <Button style="@style/NormalKeyStyles" android:tag="3" android:text="3" />
        <Button style="@style/NormalKeyStyles" android:tag="APOSTROPHE" android:text="\'" />
        <Button style="@style/NormalKeyStyles" android:tag="DEL" android:text="<\-" />
        <Button style="@style/NormalKeyStyles" android:tag="ENTER" android:text="<\-\|" />
        
    </LinearLayout>
    <!-- Add more rows with the same scheme. -->
</LinearLayout>
    
Reference: KeyEvent.KEYCODE_
res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="NormalKeyStyles" parent="TextAppearance.AppCompat">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_weight">1</item>
        <item name="android:layout_height">match_parent</item>
        <!-- key_bg_selector is a ColorStateList which specifies different background colors for different states of button. -->
        <item name="android:background">@color/key_bg_selector</item>
        <item name="android:textColor">@color/white</item>
        <item name="android:textSize">24sp</item>
        <item name="android:textAllCaps">false</item>
        <item name="android:onClick">onKeyClick</item>
    </style>
</resources>
As you can see, onKeyClick() method is bound to each Button via the NormalKeyStyles, it will handle the click events. Let's see how to handle them.
We can use the View.getTag() method to extract the keycode we assigned to the keys in xml file. And then, we can use InputConnection.sendKeyEvent() to send the event to the client.
 public void onKeyClick(View pressedKey) {
        InputConnection inputConnection = getCurrentInputConnection();
        if (inputConnection == null) return;
        
        long now = System.currentTimeMillis();
        String keyType = (String) pressedKey.getTag();
        
        }
        
        switch(keyType) {
         
            case "SHIFT":
                shiftPressed = !shiftPressed;
            default:
                // check if shift is active and then set metaState accordingly for sending the KeyEvent
                if (shiftPressed) {
                    metaState = KeyCode.META_SHIFT_MASK;
                } else {
                    metaState = 0;
                }
                try {
                    // keycode is retrieved from the KeyEvent.KEYCODE_keyname variables using Reflection API.
                    int keycode = KeyEvent.class.getField("KEYCODE_" + keyType).getInt(null), 0, metaState);
                    
                    // you can also use commitText() if you want, but you will have to add extra cases for special keys like ENTER, DEL, etc.
                    // This approach handles all of them in one case
                    inputConnection.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keycode);
                } catch (IllegalAccessException | NoSuchFieldException e) {
                    //
                }
                }
            }
    }
And now you are good to go, but I will like to add some notes that might be helpful to you:
- You can change the layout after initialization using - setInputView(), so you may add a key with tag "CHANGE_LAYOUT" and add a case for it in the- onKeyClick()method to switch between different keyboards.
 
- If you want to add extra symbols in the layout, which has no direct - KeyEventkeycode field, such as $ sign, or emojis or other unicode characters, you can use- SHIFTmask there if available (like- SHIFT + 4produces $), or you can use- commitText()for inputting that.
 
- For adding key preview popups, you can use a - PopupWindowwith a- TextViewand display it on top of the key when touch starts, and dismiss it when touch ends, with- setText()to change the- PopupWindowtext.
 
- For a more sophisticated implementation, you can see my Control Board source on GitHub.