Blog Infos
Author
Published
Topics
,
Published

It’s time for a Shift!

This post is a continuation of the article TimerApp — Part 1 in which we created a timer selection screen in Jetpack Compose. If you haven’t read it and want to have a look, please read here.

Alright, here is what we’re going to achieve:

We’ll be selecting a time whenever a user presses a number key. Following points to be watch out for while developing this

  • Every number is added from the extreme right which is right bit of seconds.
  • Adding numbers will shift existing entered numbers towards left by one place.
  • Once the hour’s left bit is occupied/filled then no more entries will be accepted.
  • Entering even a single number will change the color of the time display from Gray to Blue and deleting all numbers will change it from Blue to Gray.
  • Double zero key will add two zeroes only if space is available for both the zeros otherwise nothing will happen.

PS: I’ll not be covering the keypad design and callbacks. For those details please refer here.

Step 1 — Setting up the base composable

So as we see that our basic UI element here will be the two zeroes text and the time unit text and repeating them thrice will give us our time display. So let’s name it as TimeDisplayUnit whose constructor contains

@Composable
fun TimeDisplayUnit(
    time: TimeUnit = TimeUnit(),
    unit: String,
    textColor: Color = GRAY_TEXT
)

time: TimeUnit — This is a data class which contains our two digits namely left and right one

data class TimeUnit(
    val leftDigit: Int = 0,
    val rightDigit: Int = 0,
)

unit: String — Name of the time unit to be displayed i.e s for seconds and so on.

textColor: Color — Color for the text in time display unit composable.

Step 2 — Creating the time state class

Now we’ll create a data class TimeData which will hold our data for the selected time.

data class TimeData(
val hours: TimeUnit = TimeUnit(),
val mins: TimeUnit = TimeUnit(),
val secs: TimeUnit = TimeUnit(),
) {
fun isDataFull() = hours.leftDigit > 0
fun isDataEmpty() =
hours.leftDigit == 0 && hours.rightDigit == 0
&& mins.leftDigit == 0 && mins.rightDigit == 0
&& secs.leftDigit == 0 && secs.rightDigit == 0
fun isHoursHalfFull() = hours.leftDigit == 0 && hours.rightDigit > 0
}
view raw TimeData.kt hosted with ❤ by GitHub

Here we’ve three helper functions which we’ll discuss later in the article. So the data class is self explanatory. It holds time units for hours, mins and secs.

Step 3 — Create state in view model and pass to UI
var timeState = mutableStateOf(TimeData())

We created a time state of TimeData. We update this state inside our view model and observe this state in our composable.

val timeState by remember {
    viewModel.timeState
}
....
Box {
   TimeDisplay(
    time = timeState,
   )
} ....

TimeDisplay is our composable which contain three time display units.

@Composable
fun TimeDisplay(
time: TimeData = TimeData(),
) {
val timeUnitColor = if (time.isDataEmpty()) GRAY_TEXT else BLUE_LIGHT
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
TimeDisplayUnit(
time = time.hours,
unit = "h",
textColor = timeUnitColor
)
TimeDisplayUnit(
time = time.mins,
unit = "m",
textColor = timeUnitColor
)
TimeDisplayUnit(
time = time.secs,
unit = "s",
textColor = timeUnitColor
)
}
}
view raw TimeDisplay.kt hosted with ❤ by GitHub

So now we’ve our state passed to our display units.

Step 4 — Logic for shifting numbers to left

To accommodate new numbers correctly and shifting them to left require some logic. Here we simply shifts each number to its adjacent number and so on as depicted in the diagram below. The only catch here is that we need to start shifting from hours digit.

Here is the code for the above logic

private fun addTime(value: String) {
val intValue = value.toInt()
val hours = TimeUnit(
leftDigit = timeState.value.hours.rightDigit,
rightDigit = timeState.value.mins.leftDigit,
)
val mins = TimeUnit(
leftDigit = timeState.value.mins.rightDigit,
rightDigit = timeState.value.secs.leftDigit,
)
val secs = TimeUnit(
leftDigit = timeState.value.secs.rightDigit,
rightDigit = intValue,
)
timeState.value = timeState.value.copy(
hours = hours,
mins = mins,
secs = secs
)
}
view raw TimeAddition.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components

Are your ViewModels exponentially growing out of control as they manage the state for each of your Composables? This talk introduces Molecule, a new library for creating state holders in Jetpack Compose.
Watch Video

Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components

Jack Adams
Senion Android Engineer
Trainline

Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components

Jack Adams
Senion Android Engin ...
Trainline

Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components

Jack Adams
Senion Android Engineer
Trainline

Jobs

Step 5 — Logic for deleting numbers

This function is the reverse of what we did in step 4. We simply start shifting from second’s digit first and appends a zero on the hours left digit.

Here is the code for the above logic

private fun deleteTime() {
val secs = TimeUnit(
rightDigit = timeState.value.secs.leftDigit,
leftDigit = timeState.value.mins.rightDigit,
)
val mins = TimeUnit(
rightDigit = timeState.value.mins.leftDigit,
leftDigit = timeState.value.hours.rightDigit,
)
val hours = TimeUnit(
rightDigit = timeState.value.hours.leftDigit,
leftDigit = 0,
)
timeState.value = timeState.value.copy(
hours = hours,
mins = mins,
secs = secs
)
}
view raw Delete.kt hosted with ❤ by GitHub
Step 6 — Handling the edge cases

We need to prevent over filling and empty deletion. For this now refer to the three functions we created in our TimeData class. In case of deletion we first check if the data is empty then we simply return. In case of addition we check if data is full then also return.

Now we consider data is full when hours left digit is greater than zero and data is empty when all the digits are zero.

private fun onKeyPress(key: Keypad) {
when(key) {
Keypad.KeyDelete -> {
/* when delete key is clicked */
if (timeState.value.isDataEmpty())
return
deleteTime()
}
Keypad.Key00 -> {
/* when double zero key is clicked */
if (timeState.value.isDataEmpty()
|| timeState.value.isHoursHalfFull()
|| timeState.value.isDataFull()
)
return
addTime(Keypad.Key0.value)
addTime(Keypad.Key0.value)
}
Keypad.Key0 -> {
/* when zero key is clicked */
if (timeState.value.isDataEmpty()
|| timeState.value.isDataFull()
)
return
addTime(key.value)
}
else -> {
/* when any num key is clicked */
if (timeState.value.isDataFull())
return
addTime(key.value)
}
}
}
view raw OnKeyPress.kt hosted with ❤ by GitHub

That’s it! Now if you run the app, you’ll find the numbers are shifting left and right correctly and following our constraints. Well! that was easy.

We’ve successfully managed our time data state and updated the UI accordingly.

For full source code of the timer app, refer the below repo.

To understand about the approach, please read below link

That’s all for now. Hope it’ll help!

Until next time..

Cheers!

This article was originally published on proandroiddev.com on May 28, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu