How to prevent the user from entering two dots in a text field jetpack compose - kotlin

I have a Text Field in android jetpack compose, which used to get number from user including floating points. How to prevent user from entering 2 dots in the input.
For Eg: Number should be 56.54,.266, 367.5
Should not be like this: 34.3.5

You can use onValueChange of TextField to filter the text:
#Composable
fun MyTextField(
) {
var text by remember { mutableStateOf("") }
TextField(
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
value = text,
onValueChange = {
val newString = it.filter { char ->
char == ".".first()
}
if (newString.length <= 1)
text = it
}
)
}

Related

Jetpack compose remember function not working

I'm trying to create a scrollable background in Jetpack Compose.
The problem is that the variable "currentPadding" isn't updating it's state after the value "padding" is modified after recomposition. In the first composition (loading state) the "padding" value is set to 112.dp and after load the value changes to 160.dp.
It's strange because I have used the remember function this way multiple times in other places in the app and it's the first time that this happens.
Could you help me out?
Thanks a lot.
#Composable
fun ScrollableBackground(
scrollState: ScrollState,
composable: ComposableFun,
modifier: Modifier = Modifier,
isBannerListEmpty: Boolean,
) {
val padding = if (isBannerListEmpty) {
112.dp
} else {
160.dp
}
val minPadding: Dp = 29.dp
val dp0 = dimensionResource(id = R.dimen.no_dp)
var currentPadding: Dp by remember { mutableStateOf(padding) }
val state: Dp by animateDpAsState(targetValue = currentPadding)
val nestedScrollConnection: NestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val percent = scrollState.value.toFloat() / scrollState.maxValue.toFloat() * 100f
val delta = available.y.dp
val newSize = currentPadding + delta / 3
currentPadding = when {
percent > 20f -> minPadding
newSize < minPadding -> minPadding
newSize > padding -> padding
else -> newSize
}
return Offset.Zero
}
}
}
Box(
modifier = modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
Surface(
color = White,
modifier = Modifier
.padding(top = state)
.fillMaxSize()
.clip(
CircleShape.copy(
topStart = Shapes.medium.topStart,
topEnd = Shapes.medium.topEnd,
bottomEnd = CornerSize(dp0),
bottomStart = CornerSize(dp0)
)
)
) {}
composable.invoke()
}
}
I tried sending other kind of parameters from the view model to the composable (instead of a boolean, in this case "isBannerListEmpty"), like the current desired padding value, and nothing seems to work.
You have put nestedScrollConnection in remember. It also remembers the padding variable when first encountered. So when the actual value of padding is changed, this change is not propagated in remember of nestedScrollConnection.
Put padding inside onPreScroll.
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val padding = if (...) { //Don't use isBannerListEmpty here as neither this will update on recomposition
112.dp
} else {
160.dp
}
...

Is it possible to get value of clicked Text using Modifier.clickable?

This is my scenario, I need to get text value of clicked text:
#Composable
fun keyboard(){
val letters: Array<Pair<String, String>> = letterMap.toList().toTypedArray();
val txtModifier = Modifier
.clickable(onClick = { btnTapAction( /* send a letter */ ) })
for(i in 0..3)
Row(){
for(j in (i*8)..(i*8+7))
if (j<30)
Text(letters[j].first, txtModifier)
}
}
I have 30 Text's, each containing one cyrillic letter, they act like keyboard buttons, and I want to send clicked letter to function btnTapAction() which handles entered letters.
I have looked at documentation and I could not find a way to do it, but I hope it can be done?
If you wish to return value of clicked item create a separate Composable using state-hoisting
#Composable
fun Keyboard(){
val letters: Array<Pair<String, String>> = letterMap.toList().toTypedArray();
val txtModifier = Modifier// You can do some styling here such as rounded shape, etc
for(i in 0..3)
Row(){
for(j in (i*8)..(i*8+7))
if (j<30)
Letter( txtModifier, letters[j].first){char:String->
// You can pass this letter from keyboard to parent Composable using another callback or process here
}
}
}
#Composable
private fun Letter(modifier: Modifier = Modifier, char: String, onClick: (String) -> Unit) {
Text(text = char,
modifier = modifier.clickable {
onClick(char)
}
)
}
Edit
Initial question was like a question to get position of a letter in a Text, you wish to get position of any character inside a Text you can use answer below
You can use ClickableText Composable to get position of click
val context = LocalContext.current
val text = "Some clickable text"
ClickableText(
text = buildAnnotatedString {
append(text)
},
onClick = { position: Int ->
Toast.makeText(
context,
text.substring(startIndex = 0, endIndex = position),
Toast.LENGTH_SHORT
).show()
}
)

Jetpack Compose Textfield value not getting updated

I'm trying to create a display where users can input a duration composed of hours, minutes and seconds. I manage the state of the duration with a class i wrote called TimeData. I set the value for the textfield to the state values, however this does not get updated when it changes. I have tried alot and i can't seem to figure out why this does not work, as similar implementations work just fine.
I save the state in viewmodel, inject it into the screen (composable) the screen passes fields of the TimeData to the three composables for the hours, seconds and minutes. These components handle displaying the textfields and changing the values
I've tried changing state directly, saving state in screen file with by remember instead of in viewmodel with State, i've tried changing val to var and back in the TimeData object and it's children. And much more.
p.s. this is my first question here, so if anything is not clear, please let me know.
TimeData class (saved as state in viewmodel)
data class TimeData(
val hours: TimeUnit = TimeUnit(TimeUnits.HOURS),
val mins: TimeUnit = TimeUnit(TimeUnits.MINS),
val secs: TimeUnit = TimeUnit(TimeUnits.SECS),
) {
fun isDataEmpty() = hours.value == 0L && mins.value == 0L && secs.value == 0L
}
fun TimeData.toISOString(): String = "PT" +
"${hours.value}${hours.unit.firstLetter}" +
"${mins.value}${mins.unit.firstLetter}" +
"${secs.value}${secs.unit.firstLetter}"
fun TimeData.toDuration(): Duration = Duration.parse(this.toISOString())
TimeUnit class (this value is updated)
data class TimeUnit(
var unit: TimeUnits
) {
var value: Long = 0
set(value) {
val parsedValue = value
val min = 0
val max = when (unit) {
TimeUnits.HOURS -> 99
TimeUnits.MINS -> 60
TimeUnits.SECS -> 60
}
if (parsedValue in min..max) {
field = value
}
}
override fun toString(): String {
return if (value in 1..9) "0$value" else value.toString()
}
}
enum class TimeUnits(val firstLetter: String) {
HOURS("h"),
MINS("m"),
SECS("s")
}
State in viewmodel
private val _timeState = mutableStateOf(TimeData())
val timeState: State<TimeData> = _timeState
Texfield
BasicTextField(
modifier = Modifier.widthIn(1.dp),
value = time.value.toString(),
onValueChange = {
if (it.isNotBlank()) {
//changing state directly
time.value = it.toLong()
//letting viewmodel handel change
viewModel.onEvent(AddEditExerciseEvents.DurationValueChanged(time))
}
},
singleLine = true,
textStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Medium,
color = textColor,
textAlign = TextAlign.Center
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Changing state value in viewmodel
when (event) {
is AddEditExerciseEvents.DurationValueChanged -> {
when (event.value.unit) {
TimeUnits.HOURS -> {
_timeState.value = timeState.value.copy(
hours = event.value
)
}
TimeUnits.MINS -> {
_timeState.value = timeState.value.copy(
mins = event.value
)
}
TimeUnits.SECS -> {
_timeState.value = timeState.value.copy(
secs = event.value
)
}
}
}

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.

Kotlin Compose Search Bar

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