Slide 1

Slide 1 text

© 2024 Wantedly, Inc. Jetpack Composeの BottomSheetとの戦い Mobile勉強会#14 2024/07/23 久保出雅俊

Slide 2

Slide 2 text

© 2024 Wantedly, Inc. wantedly.com/id/kubode @swiz_ard @kubode

Slide 3

Slide 3 text

© 2024 Wantedly, Inc. Agenda ● BottomSheetの苦労話 ● 実装については少し難しめの話

Slide 4

Slide 4 text

© 2024 Wantedly, Inc. Jetpack Composeの標準のBottomSheet実装

Slide 5

Slide 5 text

© 2024 Wantedly, Inc. 標準のBottomSheet実装 https://m3.material.io/components/bottom-sheets/overview ① Standard ② Modal Material 2 BottomSheetScaffold ModalBottomSheetLayout Material 3 BottomSheetScaffold ModalBottomSheet

Slide 6

Slide 6 text

© 2024 Wantedly, Inc. Modalの実装の違い @Composable private fun M3ModalPreview() { var showSheet by remember { mutableStateOf(false) } Button(onClick = { showSheet = true }) { Text("Show") } if (showSheet) { ModalBottomSheet( onDismissRequest = { showSheet = false } ) { Text("Hello, World!") } } } @Composable private fun M2ModalPreview() { val sheetState = rememberModalBottomSheetState( ModalBottomSheetValue.Hidden ) val coroutineScope = rememberCoroutineScope() ModalBottomSheetLayout( sheetContent = { Text("Hello, World!") }, sheetState = sheetState ) { Button( onClick = { coroutineScope.launch { sheetState.show() } } ) { Text("Show") } } }

Slide 7

Slide 7 text

© 2024 Wantedly, Inc. 課題

Slide 8

Slide 8 text

© 2024 Wantedly, Inc. やりたいこと ● 検索画面のフィルターをModalで表示 したい ● 複数選択のフィルター項目の場合、 OKボタンを画面の下にStickyで出す

Slide 9

Slide 9 text

© 2024 Wantedly, Inc. 課題 1 BottomNavigationは Activityが所有する XMLのView Activity BottomNavigation FragmentContainer SearchFragment with Compose

Slide 10

Slide 10 text

© 2024 Wantedly, Inc. 課題 1 StandardやMaterial2のModalは BottomNavigationが前面に出る (Material3のModalだと問題ないが、現 状はMaterial2を使用

Slide 11

Slide 11 text

© 2024 Wantedly, Inc. 課題 2 標準のModalの上には、コンテンツを 配置できない

Slide 12

Slide 12 text

© 2024 Wantedly, Inc. 解決策

Slide 13

Slide 13 text

© 2024 Wantedly, Inc. 解決策 Modalを自作した ● ウィンドウ操作が必要と判断 ● ComposeのDialogのコードをもとに自作 ○ Material3のModalじゃない理由は後述しますが、Material3のModalとほぼ同じ実 装

Slide 14

Slide 14 text

© 2024 Wantedly, Inc. @Composable fun Modal( onDismissRequest: () !> Unit, sheetContent: @Composable () !> Unit, overlayContent: @Composable (() !> Unit)? = null, sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), ) { !/ 色々省略あり val dialog = remember(view) { DialogWrapper(/*省略*/).apply { setContent(composition) { DialogLayout { Box(modifier = Modifier.fillMaxSize()) { ModalLayout( sheetState = sheetState, onDismissRequest = onDismissRequest, sheetContent = sheetContent, ) if (overlayContent != null) { OverlayContent( sheetState = sheetState, overlayContent = overlayContent, ) } } } } } }

Slide 15

Slide 15 text

© 2024 Wantedly, Inc. @Composable fun Modal( onDismissRequest: () !> Unit, sheetContent: @Composable () !> Unit, overlayContent: @Composable (() !> Unit)? = null, sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), ) { !/ 色々省略あり val dialog = remember(view) { DialogWrapper(/*省略*/).apply { setContent(composition) { DialogLayout { Box(modifier = Modifier.fillMaxSize()) { ModalLayout( sheetState = sheetState, onDismissRequest = onDismissRequest, sheetContent = sheetContent, ) if (overlayContent != null) { OverlayContent( sheetState = sheetState, overlayContent = overlayContent, ) } } } } } } 透明なフルスクリーンウィンドウ

Slide 16

Slide 16 text

© 2024 Wantedly, Inc. @Composable fun Modal( onDismissRequest: () !> Unit, sheetContent: @Composable () !> Unit, overlayContent: @Composable (() !> Unit)? = null, sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), ) { !/ 色々省略あり val dialog = remember(view) { DialogWrapper(/*省略*/).apply { setContent(composition) { DialogLayout { Box(modifier = Modifier.fillMaxSize()) { ModalLayout( sheetState = sheetState, onDismissRequest = onDismissRequest, sheetContent = sheetContent, ) if (overlayContent != null) { OverlayContent( sheetState = sheetState, overlayContent = overlayContent, ) } } } } } } ウィンドウの中にBottomSheet BottomSheetの制御系

Slide 17

Slide 17 text

© 2024 Wantedly, Inc. @Composable fun Modal( onDismissRequest: () !> Unit, sheetContent: @Composable () !> Unit, overlayContent: @Composable (() !> Unit)? = null, sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), ) { !/ 色々省略あり val dialog = remember(view) { DialogWrapper(/*省略*/).apply { setContent(composition) { DialogLayout { Box(modifier = Modifier.fillMaxSize()) { ModalLayout( sheetState = sheetState, onDismissRequest = onDismissRequest, sheetContent = sheetContent, ) if (overlayContent != null) { OverlayContent( sheetState = sheetState, overlayContent = overlayContent, ) } } } } } } BottomSheetの前面にオーバーレイ BottomSheetのスクロールには連動 しない→Sticky

Slide 18

Slide 18 text

© 2024 Wantedly, Inc. Usage @Composable private fun ModalPreview() { var showSheet by remember { mutableStateOf(false) } Button(onClick = { showSheet = true }) { Text("Show") } if (showSheet) { Modal( onDismissRequest = { showSheet = false }, sheetContent = { Text("Content") }, overlayContent = { Button(/*省略*/) }, ) } }

Slide 19

Slide 19 text

© 2024 Wantedly, Inc. まとめ ● ないものは作る ● ソースを読んで理解する ○ ComposeのWindow関連APIは結構泥臭いことをやってて面白い ● 下調べは重要 ○ 実装時はMaterial3のModalを知らなかった ○ 知っていれば、もう少し実装時間を減らせたはず

Slide 20

Slide 20 text

© 2024 Wantedly, Inc.