Jetpack Composeのモーダルって直感的じゃないですよね
スペースマーケットでAndroidエンジニアをしていますseoです。
本日はJetpack ComposeでBottomSheetModalを実装する機会があったので、そのときの工夫についてシェアしたいと思います。
ModalBottomSheetLayout
「Jetpack Compose bottom sheet」などでググれば、真っ先に出てくる実装方法がこちらです。
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MainScreen() {
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = { it != ModalBottomSheetValue.HalfExpanded }
)
val coroutineScope = rememberCoroutineScope()
BackHandler(sheetState.isVisible) {
coroutineScope.launch { sheetState.hide() }
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = { BottomSheet() },
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 24.dp)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "メインスクリーンです",
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.h4,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
coroutineScope.launch {
if (sheetState.isVisible) sheetState.hide()
else sheetState.show()
}
}
) {
Text(text = "bottomSheetが出ます")
}
}
}
}
@Composable
fun BottomSheet() {
Column(
modifier = Modifier.padding(32.dp)
) {
Text(
text = "Bottom sheet",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "外側をタップすると閉じます",
style = MaterialTheme.typography.body1
)
}
}
BottomSheetを出したい画面をModalBottomSheetLayout
でwrapし、sheetState
によって、出したり、閉じたりするオーソドックスな方法です。
ただ、どんどんViewが複雑化していき、さまざまな機能追加がある場合、モーダルはあくまでメイン画面の補助的な画面の位置付けが多いと思います。
例えば、弊社のアプリで言うと、予約詳細画面に設置してある予約延長機能がこちらにあたります。
すでにあるViewをModalBottomSheetLayout
でwrapするのが、直感的でなく何か嫌な感じだな、と思ったので、ボタンを押すとModalが出現してくれるような実装を紹介したいと思います。
実装
今回は、Activityの拡張関数として、showAsBottomSheet
メソッドを定義し、ActivityからBottomSheetを呼び出すようにしてみたいと思います。
// ActivityからModalを呼び出すメソッド
fun Activity.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
// 後述
}
次にModalBottomSheetLayout
を使って、Modalの原型となるBottomSheetWrapper
を作っていきます。
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BottomSheetWrapper(
parent: ViewGroup,
composeView: ComposeView,
content: @Composable (() -> Unit) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var isSheetOpened by remember { mutableStateOf(false) }
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
content {
coroutineScope.launch {
modalBottomSheetState.hide()
}
}
}
) {}
BackHandler {
coroutineScope.launch {
modalBottomSheetState.hide()
}
}
LaunchedEffect(modalBottomSheetState.currentValue) {
when (modalBottomSheetState.currentValue) {
ModalBottomSheetValue.Hidden -> {
when {
isSheetOpened -> parent.removeView(composeView)
else -> {
isSheetOpened = true
modalBottomSheetState.show()
}
}
}
else -> {}
}
}
}
最後に、BottomSheetWrapper
をaddViewするメソッドを定義し、Activity.showAsBottomSheet
メソッドでコールします。
private fun addContentToView(
viewGroup: ViewGroup,
content: @Composable (() -> Unit) -> Unit
) {
viewGroup.addView(
ComposeView(viewGroup.context).apply {
setContent {
BottomSheetWrapper(viewGroup, this, content)
}
}
)
}
fun Activity.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
val viewGroup = this.findViewById(android.R.id.content) as ViewGroup
addContentToView(viewGroup, content)
}
使ってみましょう
@Composable
fun MainScreen() {
val activity = LocalContext.current as Activity
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 24.dp)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "メインスクリーンです",
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.h4,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
activity.showAsBottomSheet { hideModal ->
BottomSheet()
}
}
) {
Text(text = "bottomSheetが出ます")
}
}
}
activityからshowAsBottomSheet
を呼び出し、ラムダ式の中はModalのComposeを記述します。
Modal内に、「閉じる」ボタンがある場合は、内引数のhideModal
を渡してあげると、Modalを閉じることができます。
公式の通りに実装せえ、と言われそうですが、なんか直感的じゃない感じがするんですよね...
お好みで使ってみてください!!
最後に
スペースマーケットでは、パーティー用途から1人用の作業ワークスペースなど、さまざまな用途のスペースが15分単位で借りられます。
実は弊社オフィス自体もこのように貸し出ししてます!!
会議室や個室ブースも貸し出しています。
いつも自宅でコーディング飽きたな、と思ったそこのあなた、
スペースマーケットのオフィスで気分転換してみてはいかがでしょうか?
また、アプリエンジニアも絶賛募集中なので、興味ある方はご応募お待ちしています!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion