Site icon AppTractor

Создаем пассивный UI в Jetpack Compose

Декларативный пользовательский интерфейс часто описывается как создание пользовательского интерфейса, который является «функциональным» по своей природе. То есть данные входят, а пользовательский интерфейс выходит. Это замечательно в абстрактном смысле, но в мире, где приложения без дополнительных побочных эффектов не особенно полезны для конечных пользователей, как этого можно добиться? В этом посте мы рассмотрим, как применять концепции и паттерны для создания «пассивного» или «тупого» пользовательского интерфейса — пользовательского интерфейса, который не делает ничего, кроме демонстрации визуальной картинки. Во многом этот пост является духовным наследником предыдущего поста о StateHolders, так что, возможно, будет полезно освежить в памяти те идеи, прежде чем продолжить чтение.

Пассивный UI

В своей статье 2006 года под названием «Пассивный View» Мартин Фаулер вводит концепцию абстрагирования логики представления в контроллере архитектуры MVC, чтобы устранить зависимость между представлением и моделью, рассматривая контроллер как арбитра между двумя другими компонентами. Опираясь на эту идею, можно представить себе composable UI, который не имеет внутреннего состояния и полностью полагается на внешнего координатора. Этот координатор будет управлять состоянием и обновлять его перед отправкой унифицированного объекта состояния в компонент для рендеринга. Это отделяет логику рендеринга от логики манипулирования состоянием, обеспечивая четкую границу ответственности. Вот пример, который поможет проиллюстрировать эту идею.

Прежде чем продолжить, следует отметить, что здесь будут обсуждаться функции с заглавной буквой «С», то есть функции компонентов, рендеримые Jetpack Compose, и функции со строчной буквой «с», то есть функции, предназначенные для повторного использования и объединения для совместного использования кода. Различие в регистре будет проводиться по мере необходимости.

Пример

Чтобы показать, что достигается, в этом примере мы будем работать в обратном направлении от менее идеального состояния к более. Для начала здесь представлен компонент, смешивающий логику состояния и рендеринга в один компонент.

@Composable
fun ToggleInput() {
    var isOn by remember { mutableStateOf(false) }

    LaunchedEffect(isOn) {
        if (isOn) {
            Log.i("Toggle", "Turned Input On")
        }
    }

    val title = if (isOn) {
        R.string.toggle_state_on
    } else {
        R.string.toggle_state_off
    }

    Column {
        Text(text = stringResource(title))

        Checkbox(
            checked = isOn,
            onCheckedChange = {
                isOn = !isOn
            }
        )
    }
}

Этот компонент выполняет огромную работу:

Логика может быть отделена от рендеринга, так что все, о чем заботится UI-компонент — это отображение заголовка и флажка и вызов updateToggle.

data class ToggleInputState(
    val title: String,
    val isOn: Boolean,
    val updateToggle: (Boolean) -> Unit,
)

@Composable
fun rememberToggleInputState(): ToggleInputState {
    var isOn by remember { mutableStateOf(false) }

    LaunchedEffect(isOn) {
        if (isOn) {
            Log.i("Toggle", "Turned Input On")
        }
    }

    val title = if (isOn) {
        R.string.toggle_state_on
    } else {
        R.string.toggle_state_off
    }

    return ToggleInputState(
        title = stringResource(id = title),
        isOn = isOn,
        updateToggle = { isOn = it },
    )
}

@Composable
fun ToggleInput(state: ToggleInputState) {
    Column {
        Text(text = state.title)

        Checkbox(
            checked = state.isOn,
            onCheckedChange = state.updateToggle,
        )
    }
}

Теперь ToggleInput отвечает только за рендеринг на основе текущего состояния. Это имеет несколько преимуществ, включая то, что ToggleInput и rememberToggleInputState теперь можно тестировать изолированно, и мы теперь контролируем API, доступный нашему UI-компоненту. Этот паттерн также имеет ряд преимуществ для композиции предварительных просмотров, потому что компонент теперь можно легко отобразить в любом желаемом состоянии, в данном примере во включенном или выключенном. Кроме того, превью могут отображаться с жестко заданным значением состояния или использовать rememberToggleInputState в зависимости от конкретного случая использования. Пример улучшенного предварительного просмотра приведен ниже.

@Preview
@Composable
fun PreviewToggleInputWithHolder() {
    ToggleInput(state = rememberToggleInputState())
}

@Preview
@Composable
fun PreviewToggleInputWithState() {
    val state = ToggleInputState(
        title = "Test Title",
        isOn = true,
        updateToggle = {},
    )

    ToggleInput(state = state)
}

@Preview
@Composable
fun PreviewToggleInputWithStateOff() {
    val state = ToggleInputState(
        title = "Test a Different Title",
        isOn = false,
        updateToggle = {},
    )

    ToggleInput(state = state)
}

Последнее улучшение теперь можно внести в наш state holder, когда он выведен за пределы UI-компонента. Логика управления состоянием переключения может быть разделена и объединена с логикой обработки побочных эффектов и загрузки строковых ресурсов без изменения ToggleInput или ToggleInputState.

data class ToggleState(
    val isOn: Boolean,
    val updateToggle: (Boolean) -> Unit,
)

@Composable
fun rememberToggleState(): ToggleState {
    var isOn by remember { mutableStateOf(false) }

    return ToggleState(
        isOn = isOn,
        updateToggle = { isOn = it },
    )
}

data class ToggleInputState(
    val title: String,
    val isOn: Boolean,
    val updateToggle: (Boolean) -> Unit,
)

@Composable
fun rememberToggleInputState(
    toggleState: ToggleState = rememberToggleState(),
): ToggleInputState {
    LaunchedEffect(toggleState.isOn) {
        if (toggleState.isOn) {
            Log.i("Toggle", "Turned Input On")
        }
    }

    val title = if (toggleState.isOn) {
        R.string.toggle_state_on
    } else {
        R.string.toggle_state_off
    }

    return ToggleInputState(
        title = stringResource(id = title),
        isOn = toggleState.isOn,
        updateToggle = toggleState.updateToggle,
    )
}

Теперь стало еще проще тестировать конкретную функциональность в отдельности (например, rememberToggleInputState с isOn true или false и вызовы updateToggle из rememberToggleState). Кроме того, если состояние переключения понадобится в другом месте приложения, rememberToggleState можно будет использовать повторно в этом месте, а не повторять одни и те же строки кода снова и снова.

Почему пассивный UI

Надеемся, предыдущий пример уже продемонстрировал положительные результаты перехода на использование пассивного пользовательского интерфейса. Чтобы быть уточнить, вот некоторые преимущества использования пассивного UI для компонентов.

Вывод

Пассивный пользовательский интерфейс имеет существенные преимущества перед тем, чтобы запихивать весь код в один компонент, особенно по мере роста сложности данного компонента. Как и в случае с любым новым паттерном, попробуйте начать применять паттерн небольшими частями, чтобы понять, стоят ли результаты дополнительных усилий по реализации. Чтобы поиграть с представленным выше кодом, загляните в gist. До следующего раза, спасибо!

Источник

Exit mobile version