I want to create SearchView using jetpack compose, but I can't found any example that could helps me. Thanks in Advance.
This is a complex but full implementation for a SearchView from scratch. And the result will be as in the gif below, you can customize or remove InitialResults or Suggestions if you don't want your initial Composable to be displayed when SearchView is not focused and empty
Full implementation is available in github repository.
1- Create search states with
/**
* Enum class with different values to set search state based on text, focus, initial state and
* results from search.
*
* **InitialResults** represents the initial state before search is initiated. This represents
* the whole screen
*
*/
enum class SearchDisplay {
InitialResults, Suggestions, Results, NoResults
}
2- Then create class where you define your search logic
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<SuggestionModel>,
searchResults: List<TutorialSectionModel>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
override fun toString(): String {
return "🚀 State query: $query, focused: $focused, searching: $searching " +
"suggestions: ${suggestions.size}, " +
"searchResults: ${searchResults.size}, " +
" searchDisplay: $searchDisplay"
}
}
3- remember state to not update in every composition but only when our seach state changes
#Composable
fun rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<SuggestionModel> = emptyList(),
searchResults: List<TutorialSectionModel> = emptyList()
): SearchState {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
TutorialSectionModel is the model i used it can be generic type T or specific type you wish to display
4- Create a hint to be displayed when not focused
#Composable
private fun SearchHint(modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
color = Color(0xff757575),
text = "Search a Tag or Description",
)
}
}
I didn't use an Icon but if you wish you can add one
5- Create a SearchTextfield that can has cancel button, CircularProgressIndicator to display loading and BasicTextField to input
/**
* This is a stateless TextField for searching with a Hint when query is empty,
* and clear and loading [IconButton]s to clear query or show progress indicator when
* a query is in progress.
*/
#Composable
fun SearchTextField(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp,
bottom = 8.dp,
start = if (!focused) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50),
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.text.isEmpty()) {
SearchHint(modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.fillMaxHeight()
.weight(1f)
.onFocusChanged {
onSearchFocusChange(it.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 6.dp)
.size(36.dp)
)
}
query.text.isNotEmpty() -> {
IconButton(onClick = onClearQuery) {
Icon(imageVector = Icons.Filled.Cancel, contentDescription = null)
}
}
}
}
}
}
}
}
You can remove CircularProgressBar or add Icon to Row which contains BasicTextField
6- SearchBar with SearchTextField above and back arrow to return back feature with. AnimatedVisibility makes sure arrow is animated when we focus BasicTextField in SearchTextField, it can also be used with Icon as magnifying glass.
#ExperimentalAnimationApi
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SearchBar(
query: TextFieldValue,
onQueryChange: (TextFieldValue) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQuery: () -> Unit,
onBack: ()-> Unit,
searching: Boolean,
focused: Boolean,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
// Back button
IconButton(
modifier = Modifier.padding(start =2.dp),
onClick = {
focusManager.clearFocus()
keyboardController?.hide()
onBack()
}) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
}
SearchTextField(
query,
onQueryChange,
onSearchFocusChange,
onClearQuery,
searching,
focused,
modifier.weight(1f)
)
}
}
7- To use SearchBar create a rememberSearchState and update state as
Column is used here because rest of the screen is updated based on SearchState
LaunchedEffect or setting mutableState in ViewModel can be used to set query result or searching field of state to display loading
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
This is the SearchView you have in that image :
val (value, onValueChange) = remember { mutableStateOf("") }
TextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(fontSize = 17.sp),
leadingIcon = { Icon(Icons.Filled.Search, null, tint = Color.Gray) },
modifier = Modifier
.padding(10.dp)
.background(Color(0xFFE7F1F1), RoundedCornerShape(16.dp)),
placeholder = { Text(text = "Bun") },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
backgroundColor = Color.Transparent,
cursorColor = Color.DarkGray
)
)
TextField(
startingIcon = Icon(bitmap = searchIcon),
placeholder = { Text(...) }
)
Just create component, with FlexRow if you want to create UI like those.
FlexRow(crossAxisAlignment = CrossAxisAlignment.Start) {
inflexible {
drawImageResource(R.drawable.image_search)
}
expanded(1.0f) {
SingleLineEditText(
state,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search,
editorStyle = EditorStyle(textStyle = TextStyle(fontSize = 16.sp)),
onImeActionPerformed = {
onSearch(state.value.text)
}
)
}
}
Related
I have a page with a row of switches. I save the state of each row inside the composable using remember. And I update the ViewModel about the status only when the user navigates back from the page.
Now, I have a reset button at the top app bar which allows the user to reset all the user selections. Since Reset is at the upper level compose, I can't enforce the state change to the rows as the values are stored using remember. What would be the best strategy here to recompose the rows to the original state when the user clicks the reset button?
Here is the screen that I am implementing,
My Switch row compose
#Composable
fun SwitchRow(
name: String,
checked: Boolean,
onToggle: (name: String, checked: Boolean) -> Unit
) {
var checkedState by remember { mutableStateOf(checked) }
Surface(
modifier = Modifier.heightIn(min = 56.dp)
)
{
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = name, modifier = Modifier.weight(1f))
Switch(
checked = checkedState,
onCheckedChange = {
onToggle(name, it)
checkedState = it
}
)
}
}
}
A top App bar with a reset button,
#Composable
fun SwitchScreen(state: UiState,
onBackClicked: () -> Unit,
onToggleChange: (String, Boolean) -> Unit,
onReset: () -> Unit,
modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(0.dp)) {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = stringResource(R.string.settings_switch_rows))
TextButton(onClick = onReset) {
Text(stringResource(R.string.settings_reset))
}
}
},
backgroundColor = MaterialTheme.colors.background,
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.go_back)
)
}
}
)
when (state) {
is UiState.Loading -> Loading()
is State.Success -> SwitchRowList(
state = state,
onToggle = onToggleChange,
)
}
}
}
I'm praticing Compose navigation. I made an App that displays a ListItem with images, on what I call HomeScreen, when a particular item is clicked it navigates to a destination containing more information/ Details, on what I called HomeInfoScreen. And to do this I used Navigation compose Arugments. I basically did this by benchmarking the Android Developers Rally Compose.
But the problem is whenever I click on any of the rows it takes me to the details of the first Item, no matter which of them I click.
I believe the problem is from coming from HomeScreen (remember, I said this was the destination I'm navigating from)
I've previously researched, and got an Idea of passing my model object as a parameter.
But I think I did something wrong, because I get errors, and I don't know how to fix it.
Please understand that I arranged the code, in multiple composables, in this format;
SecondBodyElement -> SecondBodyGrid ------> HomeContentScreen
And Finally I call HomeContentScreen on the HomeScreen.
SecondBodyElement;
#Composable
fun SecondBodyElement(
#StringRes text: Int,
#DrawableRes drawable: Int,
modifier: Modifier = Modifier,
onHomeCardClick: (Int) -> Unit
) {
Surface(
shape = MaterialTheme.shapes.small,
elevation = 10.dp,
modifier = modifier
.padding(horizontal = 12.dp, vertical = 12.dp)
.clip(shape = RoundedCornerShape(corner = CornerSize(8.dp)))
.clickable { onHomeCardClick(drawable) }
) {
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.h3,
maxLines = 3,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 16.dp)
)
}
}
}
SecondyBodyGrid;
#Composable
fun SecondBodyGrid(
onHomeCardClick: (Int) -> Unit = {},
) {
LazyVerticalGrid(
columns = GridCells.Fixed(1),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.height(1450.dp)
//.disabledVerticalPointerInputScroll()
) {
items(SecondBodyData) { item ->
SecondBodyElement(
onHomeCardClick = onHomeCardClick,
drawable = item.drawable,
text = item.text,
modifier = Modifier
.height(380.dp)
.clickable { onHomeCardClick(item.drawable + item.text) }
)
}
}
}
HomeContentScreen;
#Composable
fun HomeContentScreen(
modifier: Modifier = Modifier,
onHomeCardClick: (String) -> Unit,
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
val homeInfo = remember(accountType) { HomeInfoModel.getHomeInfo(accountType) }
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
HomeQuote()
HomeTitleSection(title = R.string.favorite_collections) {
SecondBodyGrid { onHomeCardClick(homeInfo.title) }
}
}
}
And Finally HomeScreen;
#Composable
fun HomeScreen(onHomeCardClick: (String) -> Unit) {
HomeContentScreen(onHomeCardClick = onHomeCardClick)
}
Please like I said I'm practicing, I don't know what you are going to need. But I'm going to add my NavHost (Nav Graph) and the Model file, just incase, If you need any other thing I'm more than happy to provide.
Model, HomeInfoModel;
data class HomeInfoData(
val id: Int,
val title: String,
val sex: String,
val age: Int,
val description: String,
val homeInfoImageId: Int = 0
)
object HomeInfoModel {
val homeInfoModelList = listOf(
HomeInfoData(
id = 1,
title = "There's Hope",
sex = "Male",
age = 14,
description = "Monty enjoys chicken treats and cuddling while watching Seinfeld.",
homeInfoImageId = R.drawable.ab1_inversions
),
....
)
fun getHomeInfo(accountName: String?): HomeInfoData {
return homeInfoModelList.first { it.title == accountName }
}
}
My NavHost (Nav Graph);
....
composable(route = Home.route) {
HomeScreen(
onHomeCardClick = { accountType ->
navController.navigateToHomeInfoScreen(accountType)
}
)
}
composable(
route = HomeInfoDestination.routeWithArgs,
arguments = HomeInfoDestination.arguments,
) { navBackStackEntry ->
// Retrieve the passed argument
val accountType =
navBackStackEntry.arguments?.getString(HomeInfoDestination.accountTypeArg)
// Pass accountType
HomeInfoScreen(accountType)
}
....
}
}
....
private fun NavHostController.navigateToHomeInfoScreen(accountType: String) {
this.navigateSingleTopTo("${HomeInfoDestination.route}/$accountType")
}
I think the problem is from the Homescreen but I don't really know, So I'm going to Add the HomeInfoScreen, Sorry making this question any longer.
HomeInfoScreen;
#Composable
fun HomeInfoScreen(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
DisplayHomeInfo(accountType)
}
#Composable
fun WelcomeText() {
Text(
text = "Welcome, to Home Information",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)
)
}
#Composable
fun HomeInfoDetails(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
val homeInfo = remember(accountType) { HomeInfoModel.getHomeInfo(accountType) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Image(
painter = painterResource(id = homeInfo.homeInfoImageId),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.title,
style = MaterialTheme.typography.h3
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.description,
style = MaterialTheme.typography.h5
)
}
}
// Step: Home screen - Scrolling
#Composable
fun DisplayHomeInfo(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
WelcomeText()
HomeInfoDetails(accountType)
}
}
For Clarity Sake; How can I navigate to the exact item when it is clicked on the SuccessScreen.
I'll sinerely be greatful for any help. Thanks a lot in advance for your help.
In case somebody needs this, I've been able to do this. I followed a website's Developer's Breach tutorial.
I have the following screen built in Compose -
#Composable
fun DashboardScreen(heroesViewModel: HeroesViewModel = get()) {
val searchState by heroesViewModel.searchState.collectAsState()
val uiState by heroesViewModel.uiState.collectAsState()
val focusRequester = remember { FocusRequester() }
Column(modifier = Modifier.fillMaxSize()) {
SearchBar(
searchState = searchState,
onQueryChanged = { text ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchQueryChanged(text))
},
onSearchFocusChange = { focused ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchBarFocusChanged(focused))
},
onClearQueryClicked = {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ClearQueryClicked)
},
onBack = {},
focusRequester
)
LazyColumn {
items(uiState.modelsListResponse ?: listOf()) { model ->
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
}
}
And here is my SearchBar -
#Composable
fun SearchBar(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChange: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
onBack: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val focused = searchState.focused
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(visible = focused) {
BackButton(focusManager, keyboardController, onBack)
}
SearchTextField(
searchState,
onQueryChanged,
onSearchFocusChange,
onClearQueryClicked,
focusRequester
)
}
}
#Composable
fun SearchTextField(
searchState: SearchState,
onQueryChanged: (String) -> Unit,
onSearchFocusChanged: (Boolean) -> Unit,
onClearQueryClicked: () -> Unit,
focusRequester : FocusRequester,
modifier: Modifier = Modifier
) {
val focused = searchState.focused
var query = searchState.query
val searching = searchState.searching
Surface(
modifier = modifier
.then(
Modifier
.height(56.dp)
.padding(
top = 8.dp, bottom = 8.dp,
start = if (focused.not()) 16.dp else 0.dp,
end = 16.dp
)
),
color = Color(0xffF5F5F5),
shape = RoundedCornerShape(percent = 50)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = modifier
) {
if (query.isEmpty()) {
SearchHint(modifier = modifier.padding(start = 24.dp, end = 8.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = query,
onValueChange = {
query = it
onQueryChanged(it)
},
modifier = Modifier
.fillMaxSize()
.weight(1f)
.onFocusChanged { focusState ->
onSearchFocusChanged(focusState.isFocused)
}
.focusRequester(focusRequester)
.padding(top = 9.dp, bottom = 8.dp, start = 24.dp, end = 8.dp),
singleLine = true
)
when {
searching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 16.dp)
.width(25.dp)
.size(24.dp)
)
}
query.isNotEmpty() -> {
IconButton(onClick = onClearQueryClicked) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null
)
}
}
}
}
}
}
}
}
#Composable
fun SearchHint(
modifier: Modifier = Modifier,
hint: String = "Enter a hero name"
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.then(modifier)
) {
Text(
text = hint,
color = Color(0xff757575)
)
}
}
class SearchState(
query: String,
focused: Boolean,
searching: Boolean,
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
}
What I want to achieve is the ability to know when the user has tapped or click anywhere outside of my SearchBar Composable. I want to send an event to the ViewModel so that he recomposes the screen, removing the keyboard and removing the cursor I have on my SearchBar (basically just resetting the focus).
I have tried using the focusRequester like I did in my SearchBar but without success as nothing happened, tried using the clickable {} block which is not what I need (I need the tap and not the click) and tried using Modifier.pointerInput with detectTapGestures and it did not work, not at the root LazyColumn and not at the ListItem level.
What I am looking for should be something really easy.
Found the answer - I ended up using isScrollInProgress variable from LazyListState class that provides a boolean indicating if the list is currently in scrolling. When the value was true I removed the focus from where I needed to remove it and it worked :)
Attached the solution -
val listState = rememberLazyListState()
...
LazyColumn(state = listState) {
items(uiState.modelsListResponse ?: listOf()) { model ->
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListIsScrolling(listState.isScrollInProgress))
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
Use PointerInputModifier which provides access to the underlying MotionEvents originally dispatched to Compose.
Text(modifier = Modifier
.pointerInteropFilter { motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
// When the user touches the composable
}
MotionEvent.ACTION_UP -> {
// When the user removes touch from the composable
}
}
true
},
text = "Click Me!"
)
Inside pointerInteropFilter block MotionEvent.ACTION_DOWN is triggered when user touches on the composable.
I need to implement multiselect for LazyList, which will also change appBar content when list items are long clicked.
For ListView we could do that with just setting choiceMode to CHOICE_MODE_MULTIPLE_MODAL and setting MultiChoiceModeListener.
Is there a way to do this using Compose?
Since you're in control of the whole state in jetpack compose, you can easily create your own multi-select mode. This is an example.
First I created a ViewModel
val dirs: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.targetFolders)}.asFlow().asLiveData()
val all: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.allFolders)
}.asFlow().asLiveData()
val createFolder = mutableStateOf(false)
val refresh = mutableStateOf(false)
val enterSelectMode = mutableStateOf(false)
val selectedAll = mutableStateOf(false)
val selectedList = mutableStateOf(mutableListOf<String>())
fun updateList(path: String){
if(path in selectedList.value){
selectedList.value.remove(path)
}else{
selectedList.value.add(path)
}
}
}
Usage
Card(modifier = Modifier
.width(100.dp)
.height(120.dp)
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { **viewModel.enterSelectMode.value = true** },
onTap = {
if (viewModel.enterSelectMode.value) {
viewModel.enterSelectMode.value = false
}
}
)
}
,
shape = MaterialTheme.shapes.medium
) {
Image(painter = if (dirModel.dirCover != "") painter
else painterResource(id = R.drawable.thumbnail),
contentDescription = "dirThumbnail",
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(),
contentScale = ContentScale.FillHeight)
**AnimatedVisibility(
visible = viewModel.enterSelectMode.value,
enter = expandIn(),
exit = shrinkOut()
){
Row(verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.Start) {
Checkbox(checked = isSelected.value, onCheckedChange = {
isSelected.value = !isSelected.value
viewModel.updateList(dirModel.dirPath)
})
}
}**
}
Add selected field to some class representing the item. Then just compose proper code based on that field. In compose you don't have to look for some LazyColumn flag or anything like that. You are in control of the whole state of the list.
The same can be said about the AppBar, you can do a simple if there, like if (items.any { it.selected }) // display button
You can simply handle this by the below code:
Fore example This is your list:
LazyColumn {
items(items = filters) { item ->
FilterItem(item)
}
}
and this is your list item View:
#Composable
fun FilterItem(filter: FilterModel) {
val (selected, onSelected) = remember { mutableStateOf(false) }
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 8.dp)
.border(
width = 1.dp,
colorResource(
if (selected)
R.color.black
else
R.color.white
),
RoundedCornerShape(8.dp)
)) {
Text(
modifier = Modifier
.toggleable(
value =
selected,
onValueChange =
onSelected
)
.padding(8.dp),
text = filter.text)
}
}
and that's it use tooggleable on your click component modifier like here I did on text.
I want to create an auto complete text view in compose, and I created a composable that contains a TextField and a DropDown menu. The issue I'm seeing with this solution is that when the drop down menu is expanded the text field is no longer actionable, I can't type any text in it. Any suggestions on how to address this? The code is below
#Composable
fun AutoCompleteText(
value: String,
onValueChange: (String) -> Unit,
onOptionSelected: (String) -> Unit,
modifier: Modifier = Modifier,
label: #Composable (() -> Unit)? = null,
suggestions: List<String> = emptyList()
) {
Column(modifier = modifier) {
OutlinedTextField(
value = value,
onValueChange = { text -> if (text !== value) onValueChange(text) },
modifier = Modifier.fillMaxWidth(),
label = label,
)
DropdownMenu(
expanded = suggestions.isNotEmpty(),
onDismissRequest = { },
modifier = Modifier.fillMaxWidth()
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
onOptionSelected(label)
}) {
Text(text = label)
}
}
}
}
}
DropdownMenu has a property called PopupProperties that you can use to disable focusability. This should allow you to be able to continue typing in to the OutlinedTextField:
OutlinedTextField(
value = value,
onValueChange = { text -> if (text !== value) onValueChange(text) },
modifier = Modifier.fillMaxWidth(),
label = label,
)
DropdownMenu(
expanded = suggestions.isNotEmpty(),
onDismissRequest = { },
modifier = Modifier.fillMaxWidth(),
// This line here will accomplish what you want
properties = PopupProperties(focusable = false)
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
onOptionSelected(label)
}) {
Text(text = label)
}
}
}