DEV Community

Alexander Nozik
Alexander Nozik

Posted on

When encapsulation is a trouble...

I have not written anything for a long time, but I just encountered a problem, which I see quite frequently in API design so I can't pass it. So I will write a cer

I am doing an interface in Compose Desktop and I need to create a context menu that appears near the cursor. Compose Desktop has a neat component called CursorDropdownMenu for this.

The problem is that it captures the cursor position relative to the window borders. So when the window gets resized, the popup appears in the wrong position. OK, no problems here, I will just take the source code and create my variation of the component, which uses external coordinates. Here is the code:

@Composable
fun CursorDropdownMenu(
    expanded: Boolean,
    onDismissRequest: () -> Unit,
    focusable: Boolean = true,
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit
) {
    val expandedStates = remember { MutableTransitionState(false) }
    expandedStates.targetState = expanded

    if (expandedStates.currentState || expandedStates.targetState) {
        val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }

        var focusManager: FocusManager? by mutableStateOf(null)
        var inputModeManager: InputModeManager? by mutableStateOf(null)

        Popup(
            focusable = focusable,
            onDismissRequest = onDismissRequest,
            popupPositionProvider = rememberCursorPositionProvider(),
            onKeyEvent = {
                handlePopupOnKeyEvent(it, onDismissRequest, focusManager!!, inputModeManager!!)
            },
        ) {
            focusManager = LocalFocusManager.current
            inputModeManager = LocalInputModeManager.current

            DropdownMenuContent(
                expandedStates = expandedStates,
                transformOriginState = transformOriginState,
                modifier = modifier,
                content = content
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It seems like the only thing I need is to take the Popup part and just replace the popupPositionProvider. But I am not that lucky. See, the handlePopupOnKeyEvent, which holds the logic to evaluate closing the popup on clicks outside, is private.

@ExperimentalComposeUiApi
private fun handlePopupOnKeyEvent(
    keyEvent: androidx.compose.ui.input.key.KeyEvent,
    onDismissRequest: () -> Unit,
    focusManager: FocusManager,
    inputModeManager: InputModeManager
): Boolean
Enter fullscreen mode Exit fullscreen mode

This logic is not exposed directly to the library consumer so the developers decided to make it private. On practice, it means that anyone, who wants to slightly change the behavior, needs to fully replicate parts of a library. In this particular case, the logic is not very large, but I know cases, where a library user needs to replicate a significant part of a library codebase to change the behavior.

The major question is if encapsulation is justified in this case. No, it is not. It does not hide the implementation detail, instead, it hides the logic that is used by default. One should not leave functions like this hanging in the top-level scope because of the namespace pollution, but in Kotlin it is quite easy to avoid it. Just use extensions or a companion object of a nearby type and the method will be accessible without polluting the namespace.

The moral of this short story is that encapsulation is not always a good thing. One should hide a logic that is purely implementation detail, but one should not hide the logic, that is important for alternative implementations. A good recommendation on how to decide what needs to be public is to try to imagine what will happen if you need to implement the same logic with one slight change. If you need private APIs for it, you probably need to change something.

Latest comments (0)