Kotlin Compose Search Bar - kotlin

I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.

Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out

Related

Can I change the value of a component from a separate button in Compose Multiplatform?

I am trying to make a desktop application that allows you to search through a number of predefined locations stored in Kotlin classes in a separate directory. To accomplish this, I've used the reflections and compose-jb libraries.
The problem I've run into is that I can't figure out how to update a Column of Boxes (located in another Box component) to change when I press the search button after entering tags that I want to search by.
My code is below (for the Main.kt file) that describes the entire desktop application.
val reflections = Reflections("io.github.mobomega.project.attractions")
var display = mutableSetOf<Attraction>()
fun main() = application {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
var state = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
)
Window(
onCloseRequest = ::exitApplication,
title = "Search",
state = rememberWindowState(width = 2256.dp, height = 1504.dp)
) {
val count = remember { mutableStateOf(1) }
MaterialTheme {
Column {
val text = remember { mutableStateOf("") }
OutlinedTextField(
value = text.value,
singleLine = true,
onValueChange = { text.value = it },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Row (modifier = Modifier.size(2256.dp, 50.dp), horizontalArrangement = Arrangement.Center) {
Button(modifier = Modifier.align(Alignment.Top),
onClick = {
val tags = text.value.split(", ", ",")
for (tag in tags) {
search(tag.lowercase())
println("$display have tag $tag")
}
// Setting the new value of the Box
state = create(stateVertical, stateHorizontal)
// Creates error:
// "#Composable invocations can only happen from the context of a #Composable function"
}) {
Text("Search")
}
Button (modifier = Modifier.align(Alignment.Top),
onClick = {
display.clear()
}) {
Text("Reset")
}
}
Row (horizontalArrangement = Arrangement.Center) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(red = 0xFF, green = 0xFF, blue = 0xFF))
.padding(10.dp)
) {
state // Creating the state Box component in the Row
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
}
}
}
}
}
}
#Composable
fun textBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.width(400.dp)
.background(color = Color(200, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
#Composable
fun create(stateVertical: ScrollState, stateHorizontal: ScrollState) = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
var x = 0
for (attr in display) {
x++
textBox(attr.name)
if (x < display.size) {
Spacer(modifier = Modifier.height(5.dp).align(Alignment.CenterHorizontally))
}
}
}
}
fun search(text: String) {
for (attr in reflections.getSubTypesOf(Attraction::class.java)) {
val temp = attr.getConstructor().newInstance()
println("${temp.name} has tags ${temp.tags}")
if (temp.matches(text) && (temp !in display)) {
display += temp
}
}
}
I have tried to update the value of the Box that contains all of the items that match any of the search criteria, but I have run into a number of issues, such as the "onClick" function in which I set the new value of the "state" variable (storing all of the matching items) not being a Composable function, and therefore I can't change the value.
How would I accomplish changing the value of a Component such as a Box from another Component, such as a Button?
In Compose you can't create a view like you're doing with state variable. Result of your call is just Unit, and when you later call it you should see a warning "The expression is unused". The view is added at the tree hierarchy at the moment your variable is created.
To solve your problem you need to declare display as a mutable state - it's a new thing made especially for Compose, which allows triggering recomposition when this state changes:
val display by mutableStateOf<Attraction>(setOf())
And then update like this in your search:
val mutableDisplay = mutableSetOf<Attraction>()
// for
// ...
mutableDisplay += temp
// ...
display = mutableDisplay
Note that you can't use mutable set inside your mutable state, as mutable state won't be able to track changes of this set.
To learn more about state in Compose I suggest you checking this youtube video which explains the basic principles, and Compose mental model for better understanding of how to work with it.

Why needn't the author to wrap text.isNotBlank() with remember?

The Code A is from the official Sample project here.
1: I think I can wrap text.isNotBlank() withremember, so I think Code B is good, right?
BTW, I know the system will re-calculate when the text ( val (text, setText) = remember { mutableStateOf("") } ) is changed. So
2: In Code B, val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)} and val iconsVisible = remember {text.isNotBlank() } will be re-launched when the text is changed (val (text, setText) = remember { mutableStateOf("") }) , right?
Code A
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
Column {
Row( /* ... */ ) {
/* ... */
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Code B
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = remember {text.isNotBlank() } //I add remember
Column {
...
}
}
If you use remember as in Code B, iconsVisible will be calculated only once and the same value will be used across all recompositions and not get updates when text changes, which is not what we want here.
If you want to use remember here, you should pass text as a key to it, remember(text) { text.isNotBlank() }. But as this is not a time consuming calculation, you can just skip the remember block and use it as in Code A. The .isNotBlank() function will be invoked in every recomposition but that doesn't matter much here.

Should I use remember with animateDpAsState?

The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here

Kotlin / What i need to pass to init: (index: Int)->Contact?

I have a question, what value I need to pass to "init: (index: Int)->Contact" as it expects:
The integer literal does not conform to the expected type (Int) ->
TypeVariable(T)
Snippet of the function
#Composable
fun ContactList(textVal: MutableState<TextFieldValue>, navigateToProfile: (Contact) -> Unit) {
var contacts = remember { DataProvider.contactList }
var contactList = contacts.toMutableList()
var filteredContacts: MutableList<Contact>
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
val searchText = textVal.value.text
filteredContacts = if (searchText.isEmpty()){
contactList
}
else{
val resultList = MutableList<Contact>(10, "init: (inded: Int)->Contact")
for (contact in contactList) {
if (contact.contains(searchText.lowercase())) {
resultList.add(contact)
}
}
resultList
}
items(filteredContacts) {
ContactListItem(contact = it, navigateToProfile)
}
}
}
Snippet of the Contact
data class Contact(
val id: Int,
val name: String,
val description: String,
val recipe: String,
val ImageId: Int = 0
)
(index: Int)->Contact means: a function that receives Int and returns Contact. But I guess this does not really solve your problem.
The problem is that you use MutableList "constructor" (it is actually a function) which is intended to create a list with exactly 10 items. Then you need to provide these items and this is what init function is for. What you actually need here is to create an empty list and you can do this with:
val resultList = mutableListOf<Contact>()
However, if you just need to filter some collection and create another one, it is much easier to use filter():
val resultList = contactList.filter { it.contains(searchText.lowercase()) }
If Contact.contains() is an operator then we can also simplify it to:
val resultList = contactList.filter { searchText.lowercase() in it }

Update State outside the composable function. (Jetpack compose)

I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.
You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}
Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.
You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.
You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}