В нашу жизнь андроид разработчиков уже прочно вошел фреймворк Compose. И при создании composable функций возникает соблазн добавить в качестве параметра viewModel. А уже в самой compose функции подписываться на состояния, которые находятся внутри viewModel. Я хочу рассказать, почему так делать не стоит, а передавать в качестве параметров простые классы.

Не забываем, что для отображения Preview, нам нужно включить compose через gradle. Удивительно, что сам фреймворк работает и без этого. Но вот Preview и LayoutInspector - не работают.

 buildFeatures {
        compose true
    }

А еще не забываем про ui-tooling зависимость.

//в toml
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }

//в gradle модуля
debugImplementation(libs.ui.tooling)

Приведу пример кода, с которым буду работать:

@Composable
private fun LoginScreenRoot(
    viewModel: LoginViewModel
) {
    LoginScreen(viewModel)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoginScreen(viewModel: LoginViewModel) {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text(text = "Авторизация") })
        },
        modifier = Modifier.background(Color.White),
    ) { padding ->
        when (viewModel.state.loadingState) {
            is LoginState.LoadingState.Error -> {
                CommonErrorView(
                    modifier = Modifier.padding(padding),
                    onRepeat = { viewModel.onAction(LoginAction.OnRepeat) },
                    error = (viewModel.state.loadingState as LoginState.LoadingState.Error).error
                )
            }

            LoginState.LoadingState.NotSent -> {
                if (viewModel.state.isRegistering) {
                    Register(padding, viewModel.state, viewModel::onAction)
                } else {
                    Login(padding, viewModel.state, viewModel::onAction)
                }
            }

            LoginState.LoadingState.Progress -> ProgressView(2, Modifier.padding(padding))
        }
    }
}

Суть этого кода - показать экран логина и при необходимости изменения стейта изменять UI. Нас сейчас волнуют параметры функции LoginScreen. Как можно заметить, мы передаем внутрь параметр viewModel.

На первый взгляд все замечательно. Уже внутри функции мы можем обращаться к полям viewModel. Это очень удобно и сохраняет минимум параметров функции LoginScreen.

Однако, если же мы попытаемся вызвать эту функцию для превью, то столкнемся с проблемой. Чтобы создать viewModel, мы могли бы использовать вызов внутри класса Fragment или Activity. Тут бы нам помогли DI фреймворки, типа hilt или koin. Но они выдают ошибку: ViewModels creation is not supported in Preview. Это первое.

Это первая проблема. Вторая заключается в тестах. Ведь для того чтобы использовать библиотеку screenshot testing от андроид, нам нужно замокать нашу вьюмодель. А это можно сделать с использованием дополнительных библиотек.

Как мы можем из этого выйти? На самом деле, очень просто и с минимумом затрат. В дополнительной функции LoginScreenRoot, которая не будет использоваться в Preview, мы используем методы onAction и state. При MVI подходе набор параметров функции также не будет расти. Будет меняться лишь контракт внутри класса LoginState и LoginAction.

@Composable
private fun LoginScreenRoot(
    navigateAction: (LoginNavigation) -> Unit,
    viewModel: LoginViewModel
) {
    LoginScreen(
        viewModel::onAction,
        viewModel.state
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoginScreen(
    onAction: (LoginAction) -> Unit,
    state: LoginState
) {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text(text = "Авторизация") })
        },
        modifier = Modifier.background(Color.White),
    ) { padding ->
        when (state.loadingState) {
            is LoginState.LoadingState.Error -> {
                CommonErrorView(
                    modifier = Modifier.padding(padding),
                    onRepeat = { onAction(LoginAction.OnRepeat) },
                    error = state.loadingState.error
                )
            }

            LoginState.LoadingState.NotSent -> {
                if (state.isRegistering) {
                    Register(padding, state, onAction)
                } else {
                    Login(padding, state, onAction)
                }
            }

            LoginState.LoadingState.Progress -> ProgressView(2, Modifier.padding(padding))
        }
    }
}

И теперь мы спокойно можем вызвать Preview нашей функции. А также писать скриншот тесты. Когда они выйдут из альфы.

@Preview(name = "login")
@Composable
private fun PreviewLoginScreen() {
    LoginScreen(
        {}, LoginState(false, "test", "test", LoginState.LoadingState.NotSent)
    )
}
preview for LoginScreen
preview for LoginScreen

Вот такая короткая статья для тех, кто только начинает разбираться во фреймворке, который нам дал Google.

Благодарю за прочтение. Надеюсь, было полезно.

Комментарии (0)