I am using androidx.compose.material3.SearchBar
in its expanded state in my Android project. I am using androidx.compose:compose-bom:2024.09.02
which includes androidx.compose.material3:material3-*:1.3.0
.
The screenshots shows what happens when a user performs the back gesture.
Here is the structure of the relevant classes and files:
SearchActivity.kt:
class SearchActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
if (savedInstanceState == null) {
addFragment(R.id.container, SearchFragment(), SearchFragment.FRAGMENT_TAG)
}
}
}
layout/activity_search.xml:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".search.SearchActivity"
tools:ignore="MergeRootFrame"
tools:layout="@layout/fragment_search" />
</androidx.constraintlayout.widget.ConstraintLayout>
SearchFragment.kt:
class SearchFragment : Fragment() {
companion object {
const val FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
fun replaceAtBackStack(fragmentManager: FragmentManager, @IdRes containerViewId: Int) {
fragmentManager.commit {
fragmentManager.popBackStack(FRAGMENT_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
fragmentManager.replaceFragment(containerViewId, SearchFragment(), FRAGMENT_TAG, FRAGMENT_TAG)
}
}
}
private val viewModelFactory by lazy { SearchViewModelFactory(/*...*/) }
private val viewModel: SearchViewModel by viewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_search, container, false).apply {
findViewById<ComposeView>(R.id.search_view).apply {
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
SearchScreen(
searchQuery = viewModel.searchQuery,
state = viewModel.searchResultsState.collectAsState().value,
onViewEvent = viewModel::onViewEvent,
)
}
isClickable = true
}
}
}
layout/fragment_search.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
SearchScreen.kt:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
searchQuery: String,
state: SearchResultState,
onViewEvent: (SearchViewEvent) -> Unit,
) {
MyTheme {
Scaffold { contentPadding ->
Box(
Modifier
.padding(contentPadding)
) {
var text by rememberSaveable { mutableStateOf(searchQuery) }
val expanded = true
SearchBar(
modifier = Modifier
.align(TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
query = text,
onQueryChange = {
text = it
onViewEvent(OnSearchQueryChange(it))
},
onSearch = { },
expanded = expanded,
onExpandedChange = { },
placeholder = { Text(stringResource(R.string.search_query_hint)) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
)
},
expanded = expanded,
onExpandedChange = { }
) {
when (state) {
is Loading -> Loading()
is Success -> {
val sessions = state.parameters
if (sessions.isEmpty()) {
NoSearchResult()
} else {
SearchResultList(
parameters = sessions,
onViewEvent = onViewEvent,
)
}
}
}
}
}
}
}
}
For some reason, the SearchBar
consumes the back gesture. - I noticed this when I replaced the SearchBar
with some other trivial composable. - This prevents the user from leaving the screen at all which is what I expect to happen on the back gesture.
How can this be fixed?
I was missing a androidx.activity.compose.BackHandler
which is available in androidx.activity:activity-compose
. Once the dependency is added, you can add the BackHandler
to the composable:
SearchScreen.kt:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
searchQuery: String,
state: SearchResultState,
onViewEvent: (SearchViewEvent) -> Unit,
) {
MyTheme {
Scaffold { contentPadding ->
Box(
Modifier
.padding(contentPadding)
) {
var text by rememberSaveable { mutableStateOf(searchQuery) }
val expanded = true
SearchBar(
modifier = Modifier
.align(TopCenter)
.semantics { traversalIndex = 0f },
inputField = { /*...*/ }
}
BackHandler {
onViewEvent(OnBackPress)
}
}
}
}
}
... and then pass it through the view model and in the fragment pop it from the backstack and finish the hosting activity:
SearchFragment.kt:
class SearchFragment : Fragment() {
companion object {
const val FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
fun replaceAtBackStack(fragmentManager: FragmentManager, @IdRes containerViewId: Int) {
fragmentManager.commit {
fragmentManager.popBackStack(FRAGMENT_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
fragmentManager.replaceFragment(containerViewId, SearchFragment(), FRAGMENT_TAG, FRAGMENT_TAG)
}
}
}
private val viewModelFactory by lazy { SearchViewModelFactory(/*...*/) }
private val viewModel: SearchViewModel by viewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_search, container, false).apply {
findViewById<ComposeView>(R.id.search_view).apply {
setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
setContent {
SearchScreen(
searchQuery = viewModel.searchQuery,
state = viewModel.searchResultsState.collectAsState().value,
onViewEvent = viewModel::onViewEvent,
)
}
isClickable = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewModel()
}
private fun observeViewModel() {
viewModel.navigateBack.observe(viewLifecycleOwner) {
parentFragmentManager.popBackStack(FRAGMENT_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
requireActivity().finish()
}
}
}