I have a composable which gets an integer which is a mutable state !
The integer is a timer value ! like 0 which increases on every second
What i must do inside the composable is convert to representable string like for 60 seconds I will display 1minute
so I do this
val timeString = remember {
mutableStateOf("00:00")
}
val durationString = if(duration!=0) {
var secondTime = ((duration / 1000) % 60).toString()
var minuteTime = ((((duration / 1000) / 60) % 60)).toString()
if (secondTime.length == 1) {
secondTime = "0$secondTime"
}
if (minuteTime.length == 1) {
minuteTime = "0$minuteTime"
}
"$minuteTime:$secondTime"
}else{
"00:00"
}
timeString.value = durationString
Text(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
text = timeString.value,
style = MaterialTheme.typography.h4
)
But the text does not update but since duration is a mutable state if I use it directly the text updates , so I tried LaunchedEffect but it did't work I would like to know what is the best way I can do this in compose !
Like mentioned in my comment you need to warp what should be recalculated each time inside a remember and pass the state as a key (needed so that compose knows that it has to update the String).
See example code:
#Composable
fun outer() {
val durationState = remember { mutableStateOf(600000) }
LaunchedEffect(durationState) {
// This not how you do a timer but close enough
val step = 1000
repeat(600000.div(step)) {
durationState.value = durationState.value - step
delay(step.toLong())
}
}
questionComposable(durationState)
}
#Composable
fun questionComposable(durationState: MutableState<Int>) {
val durationString = remember(durationState.value) {
val duration = durationState.value
if (duration != 0) {
var secondTime = ((duration / 1000) % 60).toString()
var minuteTime = ((((duration / 1000) / 60) % 60)).toString()
if (secondTime.length == 1) {
secondTime = "0$secondTime"
}
if (minuteTime.length == 1) {
minuteTime = "0$minuteTime"
}
"$minuteTime:$secondTime"
} else {
"00:00"
}
}
Text(
text = durationString,
style = MaterialTheme.typography.h4
)
}
Related
I am experimenting with CountDownTimer in jetpack compose with the following code
#Composable
fun Timer() {
val millisInFuture: Long = 10 * 1000 // TODO: get actual value
val timeData = remember {
mutableStateOf(millisInFuture)
}
val countDownTimer =
object : CountDownTimer(millisInFuture, 1000) {
override fun onTick(millisUntilFinished: Long) {
Log.d("TAG", "onTick: ")
timeData.value = millisInFuture
}
override fun onFinish() {
}
}
DisposableEffect(key1 = "key") {
countDownTimer.start()
onDispose {
countDownTimer.cancel()
}
}
Text(
text = timeData.value.toString()
)
}
In the logcat I am able to see the timer is ticking but the UI is not updating .
Please explain why there is on recomposition on changing the value of state variable.
Well, Within the CountDownTimer, instead of setting millisInFuture, you should set millisUntilFinished. That variable holds the updated value, the millisInFuture never changes
timeData.value = millisUntilFinished
You can try this code to implement a Countdown timer:
val time = (timerDate.time).minus(Calendar.getInstance().timeInMillis)
var timer by remember { mutableStateOf(time) }
LaunchedEffect(key1 = timer) {
if (timer > 0) {
delay(1000L)
timer -= 1000L
}
}
val secMilSec: Long = 1000
val minMilSec = 60 * secMilSec
val hourMilSec = 60 * minMilSec
val dayMilSec = 24 * hourMilSec
val hours = (time % dayMilSec / hourMilSec).toInt()
val minutes = (time % dayMilSec % hourMilSec / minMilSec).toInt()
val seconds = (time % dayMilSec % hourMilSec % minMilSec / secMilSec).toInt()
Text(text = String.format("%02d:%02d:%02d", hours, minutes, seconds))
Composable only recompose when there is state change either from the Composable function param or by the value change of State<T> inside the Composable itself like mutableStateOf() or mutableStateListOf(). In your case, you haven't start the countDownTimer itself. Try to call countDownTimer.start() inside the DisposableEffect. Second you set the timeData with the wrong value, try to set it with millisUntilFinished
I have a TextField in which there cannot be more than 10 characters, and the user is required to enter date in the format "mm/dd/yyyy". Whenever user types first 2 characters I append "/", when the user types next 2 characters I append "/" again.
I did the following to achieve this:
var maxCharDate = 10
TextField(
value = query2,
onValueChange = {
if (it.text.length <= maxCharDate) {
if (it.text.length == 2 || it.text.length == 5)
query2 = TextFieldValue(it.text + "/", selection = TextRange(it.text.length+1))
else
query2 = it
}
emailErrorVisible.value = false
},
label = {
Text(
"Date of Birth (mm/dd/yyyy)",
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._12ssp).toSp() })
},
.
.
.
It's working except that the appended "/" doesn't get deleted on pressing backspace, while other characters do get deleted.
How do I make it such that "/" is deleted too on pressing backspace?
You can do something different using the onValueChange to define a max number of characters and using visualTransformation to display your favorite format without changing the value in TextField.
val maxChar = 8
TextField(
singleLine = true,
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
},
visualTransformation = DateTransformation()
)
where:
class DateTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return dateFilter(text)
}
}
fun dateFilter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text
var out = ""
for (i in trimmed.indices) {
out += trimmed[i]
if (i % 2 == 1 && i < 4) out += "/"
}
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 1) return offset
if (offset <= 3) return offset +1
if (offset <= 8) return offset +2
return 10
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <=2) return offset
if (offset <=5) return offset -1
if (offset <=10) return offset -2
return 8
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
The / is being deleted but as soon as you delete, the length of the text becomes 2 or 5. So it checks the condition,
if (it.text.length == 2 || it.text.length == 5)
Since the condition is true now, the / appends again into the text. So it seems like it is not at all being deleted.
One way to solve this is by storing the previous text length and checking if the text length now is greater than the previous text length.
To achieve this, declare a variable below maxCharDate as
var previousTextLength = 0
And change the nested if condition to,
if ((it.text.length == 2 || it.text.length == 5) && it.text.length > previousTextLength)
And at last update the previousTextLength variable. Below the emailErrorVisible.value = false add
previousTextLength = it.text.length;
Implementation of VisualTranformation that accepts any type of mask for Jetpack Compose TextField:
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
private fun offsetTranslator() = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return 0
var numberOfHashtags = 0
val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
override fun transformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
How to use it:
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.length <= DATE_LENGTH) {
date = it
}
},
visualTransformation = MaskVisualTransformation(DATE_MASK)
)
}
object DateDefaults {
const val DATE_MASK = "##/##/####"
const val DATE_LENGTH = 8 // Equals to "##/##/####".count { it == '#' }
}
I would suggest not only a date mask, but a simpler and generic solution for inputs masking.
A general formatter interface in order to implement any kind of mask.
interface MaskFormatter {
fun format(textToFormat: String): String
}
Implement our own formatters.
object DateFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '01212022' into '01/21/2022'")
}
}
object CreditCardFormatter : MaskFormatter {
override fun format(textToFormat: String): String {
TODO("Format '1234567890123456' into '1234 5678 9012 3456'")
}
}
And finally use this generic extension function for transforming your text field inputs and you won't need to care about the offsets at all.
internal fun MaskFormatter.toVisualTransformation(): VisualTransformation =
VisualTransformation {
val output = format(it.text)
TransformedText(
AnnotatedString(output),
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int = output.length
override fun transformedToOriginal(offset: Int): Int = it.text.length
}
)
}
Some example usages:
// Date Example
private const val MAX_DATE_LENGTH = 8
#Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.matches("^\\d{0,$MAX_DATE_LENGTH}\$".toRegex())) {
date = it
}
},
visualTransformation = DateFormatter.toVisualTransformation()
)
}
// Credit Card Example
private const val MAX_CREDIT_CARD_LENGTH = 16
#Composable
fun CreditCardTextField() {
var creditCard by remember { mutableStateOf("") }
TextField(
value = creditCard,
onValueChange = {
if (it.matches("^\\d{0,$MAX_CREDIT_CARD_LENGTH}\$".toRegex())) {
creditCard = it
}
},
visualTransformation = CreditCardFormatter.toVisualTransformation()
)
}
It is because you are checking for the length of the string. Whenever the length is two, you insert a slash. Hence the slash gets deleted, and re-inserted.
Why don't you just create three TextFields and insert Slashes as Texts in between. Such logic can be very hard to perfect. Keen users can use it to crash your app, and also devs can insert malicious stuff, and exploit this flaw because the handling logic can have loopholes as well, so... It is better in my opinion to just go with the simplest (and what I think is more elegant) way of constructing.
I would like to show a timer counting down in my composable, but I am not sure how to achieve this.
I was thinking to set a delay/timeout for a minute and trigger a recompose that way, but I am not sure if that's the right way to think about it.
#Composable
fun Countdown(completedAt: Date) {
val minutesLeft = ceil((completedAt.time - Date().time) / 60_000.0).toInt()
Handler(Looper.getMainLooper()).postDelayed({
// TODO: Recompose
}, 60_000)
Text(text = "$minutesLeft minutes until completed")
}
My goal is for the text to update every minute with the new time. How can I do this?
Store the amount of minutes as state.
Also make sure to clean up the postDelayed callback inside a DisposableEffect to prevent conflicting delays and memory leaks.
I have moved this logic to a minutesLeft composable function so it can be reused.
#Composable
fun minutesLeft(until: Date): Int {
var value by remember { mutableStateOf(getMinutesLeft(until)) }
DisposableEffect(Unit) {
val handler = Handler(Looper.getMainLooper())
val runnable = {
value = getMinutesLeft(until)
}
handler.postDelayed(runnable, 60_000)
onDispose {
handler.removeCallbacks(runnable)
}
}
return value
}
private fun getMinutesLeft(until: Date): Int {
return ceil((until.time - Date().time) / 60_000.0).toInt()
}
Usage
#Composable
fun Countdown(completedAt: Date) {
val minutes = minutesLeft(until = completedAt)
Text(text = "$minutes minutes until completed")
}
You can use a ViewModel with the CountDownTimer class.
Something like:
val countTimeViewModel : CountTimeViewModel = viewModel()
val minutes = countTimeViewModel.minutes.observeAsState(60)
Button( onClick={
countTimeViewModel.onStartClicked(60000*60) }
){
Text("Start")
}
Text(""+minutes.value)
with:
class CountTimeViewModel : ViewModel() {
private var timer: CountDownTimer? = null
private val _minutes = MutableLiveData(totalTime)
val minutes: LiveData<Int> get() = _minutes
private var totalTime : Long = 0L
fun startCountDown() {
timer = object : CountDownTimer(totalTime, 60000) {
override fun onTick(millisecs: Long) {
// Minutes
val minutes = (millisecs / MSECS_IN_SEC / SECS_IN_MINUTES % SECS_IN_MINUTES).toInt()
_minutes.postValue(minutes)
}
override fun onFinish() {
//...countdown completed
}
}
}
fun onStartClicked(totalTime : Long) {
this.totalTime = totalTime
startCountDown()
timer?.start()
}
override fun onCleared() {
super.onCleared()
timer?.cancel()
}
companion object {
const val SECS_IN_MINUTES = 60
const val MSECS_IN_SEC = 1000
}
}
#Composable
fun minutesLeft(until: Date): Int {
var timeout by remember { mutableStateOf(getMinutesLeft(until)) }
Text(text = "$timeout minutes until completed")
LaunchedEffect(timeout) {
if (timeout > 0) {
delay(1000 * 60)
timeout -= 1
}
}
}
private fun getMinutesLeft(until: Date): Int {
return ceil((until.time - Date().time) / 60_000.0).toInt()
}
these codes may give you an idea to achieve this goal:
val time = (timerDate.time).minus(Calendar.getInstance().timeInMillis)
var timer by remember { mutableStateOf(time) }
LaunchedEffect(key1 = timer) {
if (timer > 0) {
delay(1000L)
timer -= 1000L
}
}
val secMilSec: Long = 1000
val minMilSec = 60 * secMilSec
val hourMilSec = 60 * minMilSec
val dayMilSec = 24 * hourMilSec
val hours = (time % dayMilSec / hourMilSec).toInt()
val minutes = (time % dayMilSec % hourMilSec / minMilSec).toInt()
val seconds = (time % dayMilSec % hourMilSec % minMilSec / secMilSec).toInt()
Text(text = String.format(" %02d:%02d:%02d", hours, minutes, seconds))
When i start my app, which should get the speed of the device, i get a NaN( i think its for " not a number"?) value for 1- 3 seconds in the textfield.The numbers are safed in a repository, but if is Nan, the app crashes.
How can i replace this NaN with 0(Zero)?... for ex.: If( NaNvalue = true){ (value = 0)}...something like that. I didnt find anything written in android or kotlin, just python(which i am not familiar with).
Any help would be great!!! Thank you...
my code:
fun getSpeed(location: Location): Float{
if (mRunning ) {
// val getkmh = (location.speed * 3.6)
// mSpeedKmH = getkmh.toFloat()
mElapsedSeconds = ((SystemClock.elapsedRealtime()
-mChrono!!.getBase()) / 1000)
mSpeedMeter = mDistanceTotal/mElapsedSeconds.toFloat()
mSpeed = ( mSpeedMeter *3.6).toFloat()// *3.6 > KmH
}
return mSpeedKmH
}
fun updateSpeed() {
val speedMeter = String.format(Locale.getDefault(), "%.1f",mSpeed)
tvSpeed.text = speedMeter
}
You can do it like the other answer shows or using takeIf and the elvis operator:
val speedMeter = String.format(Locale.getDefault(), "%.1f", mSpeed.takeIf { !it.isNaN() } ?: 0.0)
val value: Double = yourDoubleValue.let { if (it.isNaN()) 0.0 else it}
The same can be applied to Float values
Thanks to Fernando and Tenfour04!!! Both versions are working!!
val speedGesammt = String.format(Locale.getDefault(), "%.1f",
mSpeedAverageKmH.takeIf { !it.isNaN() } ?: 0.0)
val speedGesammt = String.format(Locale.getDefault(), "%.1f",
mSpeedAverageKmH.let { if (it.isNaN()) 0.0 else it})
I'm building a calculator app and in it there's a ScrollView, used to show and switch the buttons for operators and units whenever the user switches between modes.
The problem is that I didn't want to create a XML layout for each mode, so I thought of adding those buttons programmatically (which now, for me, seems pretty hard to accomplish). Here's the code that's supposed to add them:
// For each text in texts (which represents the modes), a Button is created and, if its id (represented by the index of its respective text in the list) is greater than 0, it is positioned below the previous one.
fun ScrollView.add(context: Context, input: EditText, texts: List<String>) {
removeAllViews()
val container = ConstraintLayout(context).apply {
layoutParams = ConstraintLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
topToTop = this#add.top
startToStart = this#add.left
endToEnd = this#add.right
}
}
val buttons: MutableList<Button> = mutableListOf()
texts.forEach { text ->
val button = Button(context)
val originalWidth = 60
val width = originalWidth.toDimension(context, COMPLEX_UNIT_DIP)
val originalHeight = 60
val height = originalHeight.toDimension(context, COMPLEX_UNIT_DIP)
with(button) {
layoutParams = ConstraintLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
id = texts.indexOf(text)
val previous = try { buttons[id - 1] } catch (exception: Exception) { this }
setWidth(width)
setHeight(height)
with(layoutParams as ConstraintLayout.LayoutParams) {
if (id == 0)
topToTop = this#add.top
else if (id > 0) {
layoutParams = RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
(layoutParams as RelativeLayout.LayoutParams).addRule(BELOW, previous.id)
}
}
left = this#add.left
right = this#add.right
button.text = text
isAllCaps = false
textSize = 25f
while (text().contains(System.getProperty("line.separator").toString())) textSize -= 5
setOnClickListener { input.input((it as Button).text()) }
setBackgroundResource(0)
container.addView(this)
buttons.add(this)
val buttonAdded = "[Dynamic List] Button $id added as \"$text\""
println(if (id == 0) "$buttonAdded." else "$buttonAdded. The button was positioned below \"${previous.text}\" (${previous.id}).")
}
}
addView(container)
}
And here's the code I implemented using the method above:
// Each calculator mode is represented by an Int within the list.
val modes = listOf("calculator" to 1, "length" to 2, "temperature" to 3, "time" to 4)
fun mode(context: Context, button: Button, input: EditText, view: ScrollView) {
var counter = 0
val operators = listOf("+", "-", times.toString(), division.toString())
val length = with(context) { listOf(getString(R.string.light_year), context.getString(R.string.kilometer), context.getString(R.string.hectometer), context.getString(R.string.decameter), context.getString(R.string.mile), context.getString(R.string.meter), context.getString(R.string.centimeter), context.getString(R.string.millimeter), context.getString(R.string.micrometer)) }
val temperature = with(context) { listOf(getString(R.string.celsius), context.getString(R.string.fahrenheit), context.getString(R.string.kevin), context.getString(R.string.rankine), context.getString(R.string.reaumur)) }
val time = with(context) { listOf(getString(R.string.year), context.getString(R.string.month), context.getString(R.string.day), context.getString(R.string.hour), context.getString(R.string.minute), context.getString(R.string.second), context.getString(R.string.millisecond)) }
with(button) {
setOnClickListener {
if (counter < modes.size - 1) counter++ else counter = 0
with(view) {
val mode: Pair<String, Int>? = modes[counter]
when (mode?.first) {
"calculator" -> add(context, input, operators)
"length" -> add(context, input, length)
"temperature" -> add(context, input, temperature)
"time" -> add(context, input, time)
}
text = with(context) {
with(resources) { getString(identify(context, mode?.first, "string")) }
}
}
}
}
}
Well, the problem is when I run it, the UI ends up looking like this, with all the buttons positioned at the same place: