コンポーザブルで実装したい画面のレイアウト
まず、次のようにテキストフィールドが複数あって、ボタンを押すことで入力内容に基づいた何かしらのアクションを行える画面を作りたい場面を考えます。
LazyColumnを使った無理矢理な実装
上記のような画面を作りたい時、今まではLazyColumn
とitem
を組み合わせて次のようなコンポーザブルを作成していました。
@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
を使って書き直したコードが次のようになります。
@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の画面サイズでも、はみ出した領域に関してはスクロールができるようになっていることが確認できました。
まとめ
僕のようにLazyColumn
を使っていて、少しでも違和感を感じた方はColumn
を使ったお早めの対処をお勧めします。
参考にした記事