androidandroid-jetpack-composeandroid-softkeyboardandroid-input-methodinputconnection

Calling methods of InputMethodService from Jetpack Compose


Trying to make a custom Keyboard using Jetpack Compose. Cannot figure out how call currentInputConnection or other methods from Composable.

@Composable
fun CustomKeyboard() {
    
    var inputVal by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp)
    ) {
        Spacer(modifier = Modifier.height(50.dp))
        Text("Last key pressed: $inputVal")

        Row(modifier = Modifier.fillMaxWidth(), 
            verticalAlignment = Alignment.CenterVertically) 
        {
            MyButton(mText = "A") { inputVal = it}
            MyButton(mText = "B") { inputVal = it}
            MyButton(mText = "C") { inputVal = it}
        }
    }
}

@Composable
private fun MyButton(
    mText: String,
    onPressed: (String) -> Unit
) {
    OutlinedButton(
        onClick = {
            onPressed(mText)
        },
        modifier = Modifier.padding(4.dp)
    ) {
        Text(text = mText, fontSize = 30.sp, color = Color.White)
    }
}

And the InputMethodService class here...

class ComposeKeyboardView(context: Context) : AbstractComposeView(context) {

    @Composable
    override fun Content() {
        CustomKeyboard()
    }
}


class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner,
    SavedStateRegistryOwner {

    override fun onCreateInputView(): View {
        val view = ComposeKeyboardView(this)

        window!!.window!!.decorView.let { decorView ->
            ViewTreeLifecycleOwner.set(decorView, this)
            ViewTreeViewModelStoreOwner.set(decorView, this)
            ViewTreeSavedStateRegistryOwner.set(decorView, this)
        }
        view.let {
            ViewTreeLifecycleOwner.set(it, this)
            ViewTreeViewModelStoreOwner.set(it, this)
            ViewTreeSavedStateRegistryOwner.set(it, this)
        }
        return view
    }


    fun doSomethingWith(mData: String) {
        currentInputConnection?.commitText(mData, 1)
    }


    //Lifecylce Methods

    private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }


    private fun handleLifecycleEvent(event: Lifecycle.Event) =
        lifecycleRegistry.handleLifecycleEvent(event)

    override fun onCreate() {
        super.onCreate()
        savedStateRegistry.performRestore(null)
        handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    }



    override fun onDestroy() {
        super.onDestroy()
        handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    }


    //ViewModelStore Methods
    private val store = ViewModelStore()

    override fun getViewModelStore(): ViewModelStore = store

    //SaveStateRegestry Methods

    private val savedStateRegistry = SavedStateRegistryController.create(this)

    override fun getSavedStateRegistry(): SavedStateRegistry = savedStateRegistry.savedStateRegistry
}

Solution

  • If you just need to call a single function, i.e. doSomethingWith(mData: String), or a few of them, then you can pass them into your composables and call them when you want to. This approach would be more loosely coupled and easier to @Preview the CustomKeyboard composable.

    @Composable
    fun CustomKeyboard(onKeyPressed: (String) -> Unit) {
        //...
                MyButton(mText = "A") {
                    inputVal = it
                    onKeyPressed(it)
                }
                // ...
    }
    
    class ComposeKeyboardView(
        context: Context,
        private val onKeyPressed: (String) -> Unit,
    ) : AbstractComposeView(context) {
    
        @Composable
        override fun Content() {
            CustomKeyboard(onKeyPressed)
        }
    }
    
    class IMEService : InputMethodService() {
        override fun onCreateInputView(): View {
            val view = ComposeKeyboardView(this, onKeyPressed = this::doSomethingWith)
            // ...
            return view
        }
    
        private fun doSomethingWith(mData: String) {
            currentInputConnection?.commitText(mData, 1)
        }
    }
    

    If you plan to add many more functions to the IMEService that you will have to also call, then you can just pass the IMEService (or some interface that IMEService implements) into your composables and then call its members normally. Using an interface over an actual class would make it possible to @Preview the CustomKeyboard composable.

    @Composable
    fun CustomKeyboard(imeService: IMEService) {
        //...
                MyButton(mText = "A") {
                    inputVal = it
                    imeService.doSomethingWith(it)
                }
                // ...
    }
    
    class ComposeKeyboardView(private val imeService: IMEService) : AbstractComposeView(imeService) {
    
        @Composable
        override fun Content() {
            CustomKeyboard(imeService)
        }
    }
    
    class IMEService : InputMethodService() {
        override fun onCreateInputView(): View {
            val view = ComposeKeyboardView(this)
            // ...
            return view
        }
    
        fun doSomethingWith(mData: String) {
            currentInputConnection?.commitText(mData, 1)
        }
    }