Search code examples
androidscrollandroid-jetpack-composebottom-sheetlazycolumn

Scroll issue with LazyColumn inside BottomSheetDialogFragment


I use LazyColumn inside BottomSheetDialogFragment, but if to scroll LazyColumn list UP then Bottom Sheet Dialog scrolls instead of LazyColumn list. Seems like BottomSheetDialogFragment intercepts user touch input.

That's how it looks:

How to properly use LazyColumn inside BottomSheetDialogFragment?

MyBottomSheetDialogFragment.kt:

class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text("Header", color = Color.Black)
                    LazyColumn(
                        Modifier
                            .weight(1f)
                            .fillMaxWidth()) {
                        items(100) {
                            Text("Item $it", Modifier.fillMaxWidth(), Color.Black)
                        }
                    }
                }
            }
        }
    }
}

And show it using this code:

MyBottomSheetDialogFragment().show(activity.supportFragmentManager, null)

When we used the XML RecyclerView list, to fix this issue we had to wrap the RecyclerView list with NestedScrollView like described here, but how to fix it with Jetpack Compose?


Solution

  • I found an excellent answer to this issue. We can use Jetpack Compose bottom sheet over Android view using Kotlin extension.

    More details about how it works are here.

    Here is all code we need:

    // Extension for Activity
    fun Activity.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
        val viewGroup = this.findViewById(android.R.id.content) as ViewGroup
        addContentToView(viewGroup, content)
    }
    
    // Extension for Fragment
    fun Fragment.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
        val viewGroup = requireActivity().findViewById(android.R.id.content) as ViewGroup
        addContentToView(viewGroup, content)
    }
    
    // Helper method
    private fun addContentToView(
        viewGroup: ViewGroup,
        content: @Composable (() -> Unit) -> Unit
    ) {
        viewGroup.addView(
            ComposeView(viewGroup.context).apply {
                setContent {
                    BottomSheetWrapper(viewGroup, this, content)
                }
            }
        )
    }
    
    @OptIn(ExperimentalMaterialApi::class)
    @Composable
    private fun BottomSheetWrapper(
        parent: ViewGroup,
        composeView: ComposeView,
        content: @Composable (() -> Unit) -> Unit
    ) {
        val TAG = parent::class.java.simpleName
        val coroutineScope = rememberCoroutineScope()
        val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
        var isSheetOpened by remember { mutableStateOf(false) }
    
        ModalBottomSheetLayout(
            sheetBackgroundColor = Color.Transparent,
            sheetState = modalBottomSheetState,
            sheetContent = {
                content {
                    // Action passed for clicking close button in the content
                    coroutineScope.launch {
                        modalBottomSheetState.hide() // will trigger the LaunchedEffect
                    }
                }
            }
        ) {}
    
        BackHandler {
            coroutineScope.launch {
                modalBottomSheetState.hide() // will trigger the LaunchedEffect
            }
        }
    
        // Take action based on hidden state
        LaunchedEffect(modalBottomSheetState.currentValue) {
            when (modalBottomSheetState.currentValue) {
                ModalBottomSheetValue.Hidden -> {
                    when {
                        isSheetOpened -> parent.removeView(composeView)
                        else -> {
                            isSheetOpened = true
                            modalBottomSheetState.show()
                        }
                    }
                }
                else -> {
                    Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state")
                }
            }
        }
    }