Guys how would you handle mixing callback based an...
# compose
d
Guys how would you handle mixing callback based and hosted object based approaches? I got some sample code where I've started with object like here: https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#hoisted-state-types But I want to use this composable with callback after all. Sample in thread. I've used launched effect to translate changes of property to callback. Do you have any solutions / patterns to handle case like this?
Copy code
data class Suggestion(val description: String)

@Stable
interface DropdownFormFieldState {
    var expanded: Boolean
    val suggestions: List<Suggestion>
    var selected: Suggestion?
    var textFieldSize: Size
}

@Composable
fun rememberDropdownFormFieldState(
    expanded: Boolean = false,
    suggestions: List<Suggestion>,
    selected: Suggestion? = null,
    textFieldSize: Size = Size.Zero,
) = DropdownFormFieldState(
    expanded = expanded,
    suggestions = suggestions,
    selected = selected,
    textFieldSize = textFieldSize,
)

fun DropdownFormFieldState(
    expanded: Boolean,
    suggestions: List<Suggestion>,
    selected: Suggestion?,
    textFieldSize: Size
): DropdownFormFieldState = DropdownFormFieldStateImpl(
    expanded = expanded,
    suggestions = suggestions,
    selected = selected,
    textFieldSize = textFieldSize,
)

internal class DropdownFormFieldStateImpl(
    expanded: Boolean,
    override val suggestions: List<Suggestion>,
    selected: Suggestion?,
    textFieldSize: Size
) : DropdownFormFieldState {
    override var expanded: Boolean by mutableStateOf(expanded)
    override var selected: Suggestion? by mutableStateOf(selected)
    override var textFieldSize: Size by mutableStateOf(textFieldSize)
}

@Composable
fun DropdownFormField(
    modifier: Modifier = Modifier,
    label: String,
    expanded: Boolean,
    suggestions: List<Suggestion>,
    selected: Suggestion? = null,
    onSelected: (suggestion: Suggestion) -> Unit
) {
    val state = rememberDropdownFormFieldState(
        expanded = expanded,
        suggestions = suggestions,
        selected = selected,
    )
    LaunchedEffect(selected) {
        if (selected != state.selected) {
            state.selected?.let {
                onSelected(it)
            }
        }
    }
    DropdownFormField(
        modifier = modifier,
        label,
        state = state
    )
}

@Composable
fun DropdownFormField(
    modifier: Modifier = Modifier,
    label: String,
    state: DropdownFormFieldState,
) {
    val icon = if (state.expanded)
        Icons.Filled.ArrowDropUp
    else
        Icons.Filled.ArrowDropDown

    Box(
        modifier = modifier
    ) {
        val focusRequester = remember { FocusRequester() }
        val focusManager = LocalFocusManager.current
        SideEffect {
            if (state.expanded) {
                focusRequester.requestFocus()
            }
        }
        OutlinedTextField(
            value = state.selected?.description?.takeIf { !state.expanded } ?: "",
            readOnly = true,
            onValueChange = { /*selectedText = it */ },
            modifier = Modifier
                .fillMaxWidth()
                .onGloballyPositioned { coordinates ->
                    //This value is used to assign to the DropDown the same width
                    state.textFieldSize = coordinates.size.toSize()
                }
                .focusRequester(focusRequester)
                .onFocusChanged { focusState ->
                    state.expanded = focusState.isFocused
                },
            label = { Text(label) },
            trailingIcon = {
                Icon(icon, "contentDescription",
                    Modifier.clickable {
                        if (state.expanded) {
                            focusManager.clearFocus()
                        } else {
                            focusRequester.requestFocus()
                        }
                    })
            },
        )
        DropdownMenu(
            expanded = state.expanded,
            onDismissRequest = {
                focusManager.clearFocus()
            },
        ) {
            state.suggestions.forEach { suggestion ->
                val isCurrentSelected = suggestion == state.selected
                DropdownMenuItem(
                    onClick = {
                        state.selected = suggestion
                        focusManager.clearFocus()
                    }) {
                    Text(text = suggestion.description)
                }
            }
        }
    }
}
I know that sample is trivial and after refactoring hoisted object wont be needed but you got the idea. 🙂