【Android】LazyColumnに手を出す前に、スクロール可能なColumnを作る方法

# コンポーザブルで実装したい画面のレイアウト

まず、次のようにテキストフィールドが複数あって、ボタンを押すことで入力内容に基づいた何かしらのアクションを行える画面を作りたい場面を考えます。

![名称未設定のデザイン (25).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/4e919e3f-916c-ed75-3018-560777f3eec4.png)

# LazyColumnを使った無理矢理な実装

上記のような画面を作りたい時、今までは`LazyColumn`と`item`を組み合わせて次のようなコンポーザブルを作成していました。

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SignUpScreen(
    scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
    email: String,
    password: String,
    name: String,
    loading: Boolean = false,
    showError: Boolean = false,
    onEmailChange: (String) -> Unit = {},
    onPasswordChange: (String) -> Unit = {},
    onNameChange: (String) -> Unit = {},
    onUpPress: () -> Unit = {},
    onClick: () -> Unit = { },
    onDismissErrorDialog: () -> Unit = {},
    onHideKeyboard: () -> Unit = {},
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    ) {
        TopAppBar(
            titleRes = R.string.title_signup,
            scrollBehavior = scrollBehavior,
            navigationIcon = {
                IconButton(
                    modifier = Modifier.testTag("BackButton"),
                    onClick = {
                        onUpPress()
                    }
                ) {
                    Icon(
                        imageVector = Icons.Outlined.Close,
                        contentDescription = null
                    )
                }
            },
            actions = {}
        )
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .padding(
                    horizontal = 16.dp
                ),
            verticalArrangement = Arrangement.SpaceBetween
        ) {
            LazyColumn() {
                item() {
                    Spacer(modifier = Modifier.height(8.dp))
                }
                item {
                    TextField(
                        modifier = Modifier
                            .testTag("EmailTextField")
                            .fillMaxWidth(),
                        label = {
                            Text(stringResource(id = R.string.email))
                        },
                        singleLine = true,
                        value = email,
                        keyboardActions = KeyboardActions(
                            onDone = {
                                onHideKeyboard()
                            }
                        ),
                        onValueChange = onEmailChange,
                    )
                    Spacer(modifier = Modifier.height(16.dp))
                }
                item {
                    TextField(
                        modifier = Modifier
                            .testTag("PasswordTextField")
                            .fillMaxWidth(),
                        label = {
                            Text(stringResource(id = R.string.password))
                        },
                        singleLine = true,
                        value = password,
                        visualTransformation = PasswordVisualTransformation(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Password
                        ),
                        keyboardActions = KeyboardActions(
                            onDone = {
                                onHideKeyboard()
                                if (email.isNotEmpty() && password.isNotEmpty()) {
                                    onClick()
                                }
                            }
                        ),
                        onValueChange = onPasswordChange,
                    )
                    Spacer(modifier = Modifier.height(16.dp))
                }
                item() {
                    TextField(
                        modifier = Modifier
                            .testTag("NameTextField")
                            .fillMaxWidth(),
                        label = {
                            Text(stringResource(id = R.string.name))
                        },
                        singleLine = true,
                        value = name,
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Text
                        ),
                        keyboardActions = KeyboardActions(
                            onDone = {
                                onHideKeyboard()
                            }
                        ),
                        onValueChange = {
                            onNameChange(it)
                        }
                    )
                    Spacer(modifier = Modifier.height(16.dp))
                }
            }
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f, false)
                    .padding(
                        bottom = 16.dp
                    )
            ) {
                Button(
                    modifier = Modifier
                        .testTag("RegisterButton")
                        .fillMaxWidth(),
                    enabled = !loading,
                    onClick = onClick,
                ) {
                    Text(
                        text = stringResource(id = R.string.register)
                    )
                }
            }
        }
    }
}
```

# Modifier.verticalScroll()を使ってスクロール可能なColumnを実装

なぜ、上記のような冗長な実装をおこなっていたかというと、過去に`Column`を使って同じような画面を作成した時に、スクロールができないため領域からはみ出てしまったコンポーザブルが画面上に表示されないという問題を抱えていました。その時の対処法として`LazyColumn`を使っていて、それを今日まで続けてしまっていたというのが理由です。

さすがに、何個も`item`を配置してレイアウトを行なっていくのは美しくないと考え、一念発起して調べてみることに。すると、`Modifier.verticalScroll()`というメソッドがあり、これを`Column`に指定すれば、最大描画領域の縦サイズをコンポーザブルの高さが超える場合に縦方向に限りスクロールできるようにしてくれます。

`verticalScroll()`に渡す値は`ScrollState`のインスタンスですので、`rememberScrollState()`の値を渡せば利用できるようになります。

そして、上記のコードを通常の`Column`を使って書き直したコードが次のようになります。

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SignUpScreen(
    scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
    email: String,
    password: String,
    name: String,
    loading: Boolean = false,
    showError: Boolean = false,
    onEmailChange: (String) -> Unit = {},
    onPasswordChange: (String) -> Unit = {},
    onNameChange: (String) -> Unit = {},
    onUpPress: () -> Unit = {},
    onClick: () -> Unit = { },
    onDismissErrorDialog: () -> Unit = {},
    onHideKeyboard: () -> Unit = {},
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
    ) {
        TopAppBar(
            titleRes = R.string.title_signup,
            scrollBehavior = scrollBehavior,
            navigationIcon = {
                IconButton(
                    modifier = Modifier.testTag("BackButton"),
                    onClick = {
                        onUpPress()
                    }
                ) {
                    Icon(
                        imageVector = Icons.Outlined.Close,
                        contentDescription = null
                    )
                }
            },
            actions = {}
        )
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .padding(
                    horizontal = 16.dp
                ),
            verticalArrangement = Arrangement.SpaceBetween
        ) {
            Column(
                modifier = Modifier
                    .verticalScroll(rememberScrollState())
                    .weight(1f, false)
            ) {
                Spacer(modifier = Modifier.height(8.dp))
                // メールアドレス
                TextField(
                    modifier = Modifier
                        .testTag("EmailTextField")
                        .fillMaxWidth(),
                    label = {
                        Text(stringResource(id = R.string.email))
                    },
                    singleLine = true,
                    value = email,
                    keyboardActions = KeyboardActions(
                        onDone = {
                            onHideKeyboard()
                        }
                    ),
                    onValueChange = onEmailChange,
                )
                Spacer(modifier = Modifier.height(16.dp))
                // パスワード
                TextField(
                    modifier = Modifier
                        .testTag("PasswordTextField")
                        .fillMaxWidth(),
                    label = {
                        Text(stringResource(id = R.string.password))
                    },
                    singleLine = true,
                    value = password,
                    visualTransformation = PasswordVisualTransformation(),
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Password
                    ),
                    keyboardActions = KeyboardActions(
                        onDone = {
                            onHideKeyboard()
                            if (email.isNotEmpty() && password.isNotEmpty()) {
                                onClick()
                            }
                        }
                    ),
                    onValueChange = onPasswordChange,
                )
                Spacer(modifier = Modifier.height(16.dp))
                // 名前(必須)
                TextField(
                    modifier = Modifier
                        .testTag("NameTextField")
                        .fillMaxWidth(),
                    label = {
                        Text(stringResource(id = R.string.name))
                    },
                    singleLine = true,
                    value = name,
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.Text
                    ),
                    keyboardActions = KeyboardActions(
                        onDone = {
                            onHideKeyboard()
                        }
                    ),
                    onValueChange = {
                        onNameChange(it)
                    }
                )
                Spacer(modifier = Modifier.height(16.dp))
            }
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(0.2f, false)
                    .padding(
                        bottom = 16.dp
                    )
            ) {
                Button(
                    modifier = Modifier
                        .testTag("RegisterButton")
                        .fillMaxWidth(),
                    enabled = !loading,
                    onClick = onClick,
                ) {
                    Text(
                        text = stringResource(id = R.string.register)
                    )
                }
            }
        }
    }
}
```

`item`コンポーザブルが消えることで、表現したい内容が明確になり、コードがすっきりしたように思えます。

# 完成形の画面

実際に画面外にはみ出るまでコンポーザブルを配置して、プレビューを確認してみました。
その結果、Rakuten Miniの画面サイズでも、はみ出した領域に関してはスクロールができるようになっていることが確認できました。

![名称未設定のデザイン (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/78b360b2-7756-7814-3361-42a919d37742.gif)

# まとめ

僕のように`LazyColumn`を使っていて、少しでも違和感を感じた方は`Column`を使ったお早めの対処をお勧めします。

# 参考にした記事

https://stackoverflow.com/questions/68164883/how-do-i-create-a-jetpack-compose-column-where-a-middle-child-is-scrollable-but