Create a stopwatch and timer with Python and kivymd Part 1
Tags: python, beginners, mobile, tutorial
Published: 2023-01-28 13:38:47

In this article I am going to show you how to develop a stop watch and timer application with the kivymd framework. The first part of this tutorial will take you through creating the stopwatch and the second part through creating the the timer.

## Final Product
The final application will look as below:

![Stopwatch](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbz28qrpytlsm6fakra7.gif)

![Timer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpdo8dl0ptx2jsd28td8.gif)


---

## The UI

Lets get started with the user interface.

**Prerequisites**
For this project, I'm using:

```
kivy==2.1.0
kivymd==1.1.1
```

Make sure both are installed in a virtual environment.

Create two files:
- `main.py`
- `main.kv`

Inside `main.py` add the following code:
```py
import kivy
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import TwoLineIconListItem, IconLeftWidget
from kivy.properties import NumericProperty, StringProperty
from datetime import timedelta
from kivy.clock import Clock
kivy.require('2.1.0')


class MainApp(MDApp):

    def build(self):
        # Setting the theme
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "Gray"
        self.theme_cls.primary_hue = "200"


if __name__ == '__main__':
    app = MainApp()
    app.run()

```

And inside **main.kv**, add the following code:
```py
#:kivy 2.1.0

MDScreenManager:
    MDScreen:
        name: 'main_screen'


        MDBottomNavigation:
            selected_color_background: "orange"
            text_color_active: "lightgrey"

            MDBottomNavigationItem:
                name: 'screen 1'
                text: 'Stop Watch'
                icon: 'timer'

                MDBoxLayout:
                    orientation: 'vertical'


                    MDLabel:
                        text: 'Stop Watch'
                        halign: 'center'
                        theme_text_color: "Custom"
                        text_color: 1, 1, 1, 1
                        font_style: 'H2'


            MDBottomNavigationItem:
                name: 'screen 2'
                text: 'Timer'
                icon: 'timer-sand'


                MDBoxLayout:
                    orientation: 'vertical'

                    MDTopAppBar:
                        right_action_items: [["plus", ]]
                        md_bg_color: app.theme_cls.bg_dark

                    MDLabel:
                        text: 'Timer'
                        halign: 'center'
                        theme_text_color: "Custom"
                        text_color: 1, 1, 1, 1
                        font_style: 'H2'
```

If you run this application right now, you will get the following:

![Stopwatch tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zaz041wljyen2n0a7aqh.png)

---

### The stopwatch

First, we are going to build the stopwatch. The stop watch is going to be fairly simple. It will consist of a `Label`, to show the elapsed time, and three buttons, one to reset the stopwatch, another to start, pause or stop the stopwatch and the last one to record the lap times. Lastly, it will have a list within a `ScrollView` widget in which we will be adding the recorded lap times.

Start by modifying `main.kv` as follows :

```py
#...

# Replace the label with the text "Timer" with the following
MDLabel:
    id: stopwatch_lbl
    text: app.stopwatch_time
    halign: 'center'
    theme_text_color: "Custom"
    text_color: 1, 1, 1, 1
    font_style: 'H2'

# Add a scroll view and a list
MDScrollView:
    MDList:
        id: count_list

# Add a float layout
MDFloatLayout:
    pos_hint: {'center_x': .5}
    size_hint_y: .3
    size_hint_x: .5
    spacing: 50

    # Add three icon buttons
    MDIconButton:
        id: reset_btn
        icon: 'undo-variant'
        pos_hint: {'center_x': .2}
        on_press: app.reset_stopwatch()
        disabled: True


    MDIconButton:
        id: play_pause_btn
        icon: 'play'
        pos_hint: {'center_x': .5}
        on_press: app.start_or_stop_stopwatch()


   MDIconButton:
       id: record_lap_btn
       icon: 'timelapse'
       pos_hint: {'center_x': .8}
       on_press: app.time_lap()
       disabled: True

#...
```

`main.py`

```py

#...

class MainApp(MDApp):

    stopwatch_time = StringProperty()

    milliseconds = NumericProperty()

    seconds = NumericProperty()

    minutes = NumericProperty()

    def build(self):
        # Setting the theme
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "Gray"
        self.theme_cls.primary_hue = "200"

    def on_start(self):
        self.stopwatch_time = "00:00:00"

    def start_or_stop_stopwatch(self):
        pass

    def reset_stopwatch(self):
        pass

    def time_lap(self):
        pass

#...

```

The latest changes have added icon buttons and a dynamic label to the interface. By setting the text of the label to `app.stopwatch_time`, we can change the text displayed on the interface by simply changing the value of the attribute `stopwatch_time`. Kivy properties make it easy to pass updates from the python side to the user interface and hence we used the `StringProperty` object.

In the above code we have also defined seconds, milliseconds and minutes as Numeric Properties which we are going to use to make updates to the stopwatch.

The `on_start` function is going to set the stopwatch to `00:00:00` when the application starts.

The `start_or_stop_stopwatch` function will start the stopwatch if it isn't running or stop the stopwatch if it is.

The `reset_stopwatch` function will reset the stopwatch to `00:00:00`.

The `time_lap` funtion will measure the time between laps. 

On running the application, you will get the following:


![Stopwatch sample](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/do4plwyp8xynpi9kiyss.png)

---

Now lets get our stopwatch to work. 

`main.py`
```py
# ...

class MainApp(MDApp):

    #...

    # Lap counter
    count = 1
    
    watch_started = False # Whether watch is running or not

    # store the last recorded lap time
    last_lap_time = {
        'minutes': 0,
        'seconds': 0,
        'milliseconds': 0
    }

    #...

```

`count` is going to keep track of the list items that are added when timing a lap which we'll see in action soon. 

`watch_started` is going to keep track of whether or not the stop watch is running. 

The dictionary `last_lap_time` contains data on the last recorded lap time which is updated every time we hit the lap button.

`main.py`
```py
#...

class MainApp(MDApp):

    #...

    # add this function
    def get_string_time(self, dt):
        """Function to increment milliseconds and convert the time elapsed to string format to which the label is set"""
        self.increment_milliseconds()

        milliseconds = str(self.milliseconds)
        seconds = str(self.seconds)
        minutes = str(self.minutes)

        if len(milliseconds) < 2:
            milliseconds = '0' + milliseconds

        if len(seconds) < 2:
            seconds = '0' + seconds

        if len(minutes) < 2:
            minutes = '0' + minutes

        self.stopwatch_time = minutes + ":" + seconds + ":" + milliseconds

    # Modify start_or_stop_stopwatch to look as follows
    def start_or_stop_stopwatch(self):
        """Function to stop the stopwatch if it is not running otherwise stop it"""
        if self.watch_started:
            self.watch_started = False
            self.root.ids['record_lap_btn'].disabled = True
            self.root.ids['play_pause_btn'].icon = 'play'
            self.root.ids['reset_btn'].disabled = False
            Clock.unschedule(self.get_string_time) # Unschedule the get_string_time function
        else:
            self.watch_started = True
            self.root.ids['play_pause_btn'].icon = 'pause'
            self.root.ids['record_lap_btn'].disabled = False
            Clock.schedule_interval(self.get_string_time, 0.1) # schedule the get_string_time function to run every 10ms
    
    #...
```

The `get_string_time` function converts the timer from numbers to a string, that is, it gets the values of the milliseconds, seconds and minutes and converts them to a string. For the sake of display, it checks whether the values have two digits, and if they do not, it adds a `0` before the digit to make them two and hence we can always display our minutes, seconds and milliseconds with two digits.

We have modified the `start_or_stop_stopwatch` and now if the stopwatch was not running, it will change the status of `watch_started` to `True` to indicate that it is now running. It will then change the 'play' icon to the 'pause' icon, which will allow us to pause the stopwatch. The `record_lap_button` is then enabled so that we can record our laps. Finally, we will schedule the `get_string_time` function with kivy clock to run every 10 milliseconds so that the stopwatch is updated every 10 milliseconds.

If the stopwatch was running, it will stop/pause the stopwatch and then disable the lap button since we can't record laps if the watch is not running. It will then switch the icon to the 'play' icon. The reset button is then enabled as well to allow us to reset our stopwatch. Finally, it will `unschedule` the `get_string_time` to stop it from executing every 10 milliseconds.


`main.py`
```py
class MainApp(MDApp):
    #...

    # Modify reset_stopwatch as follows
    def reset_stopwatch(self):
        """Set the stopwatch to 00:00:00"""
        if self.watch_started:
            Clock.unschedule(self.get_string_time)
        self.stopwatch_time = "00:00:00"
        self.milliseconds = 0
        self.seconds = 0
        self.minutes = 0

        # disable reset and lap buttons
        self.root.ids['reset_btn'].disabled = True
        self.root.ids['record_lap_btn'].disabled = True

        # Reset lap time
        self.last_lap_time['minutes'] = 0
        self.last_lap_time['seconds'] = 0
        self.last_lap_time['milliseconds'] = 0

    # Modify time_lap
    def time_lap(self):
        """Get the time between laps and add a list item to list"""
        lap_time = f"Count {self.count}: " + self.stopwatch_time
        list_item = TwoLineIconListItem(
            IconLeftWidget(
                icon="av-timer"
            ),
            text=lap_time,
            secondary_text=self.calculate_time_difference(),
            theme_text_color="Custom",
            text_color=(1, 1, 1, 1),
            secondary_theme_text_color="Custom",
            secondary_text_color=(1, 1, 1, 1)
        )

        self.root.ids['count_list'].add_widget(list_item, index=-1)

        self.count += 1
    
    # add the following function
    def increment_milliseconds(self):
        """Increment the milliseconds by 10ms"""
        self.milliseconds += 10

        if self.milliseconds == 100:
            self.increment_seconds()
            self.milliseconds = 0

    # add the following function
    def increment_seconds(self):
        """Increment the seconds by 1 second"""
        self.seconds += 1

        if self.seconds == 60:
            self.increment_minutes()
            self.seconds = 0

    # add the following function
    def increment_minutes(self):
        """Increment the minutes by 1 minute"""
        self.minutes += 1


    # add the following function
    def calculate_time_difference(self):
        """Calculate the time difference between the laps records
        """
        lap_time = timedelta(
            minutes=self.minutes,
            seconds=self.seconds,
            milliseconds=self.milliseconds
        ) - timedelta(
            minutes=self.last_lap_time['minutes'],
            seconds=self.last_lap_time['seconds'],
            milliseconds=self.last_lap_time['milliseconds']
        )

        lap_time = str(lap_time)[2:-3]

        lap_time = lap_time.split(':')

        lap_time = [i.split('.') for i in lap_time]

        minutes = int(lap_time[0][0])
        seconds = int(lap_time[1][0])
        milliseconds = int(lap_time[1][1])

        self.last_lap_time['minutes'] = self.minutes
        self.last_lap_time['seconds'] = self.seconds
        self.last_lap_time['milliseconds'] = self.milliseconds

        return f"{minutes}:{seconds}:{milliseconds}"
    #....
```

Ok, I know it's a lot of code but I'll do my best to explain it.

The `reset_stopwatch` method will check if the stopwatch is running. If it is, it will unschedule the `get_string_time` method. It will then set `stopwatch_time` to `00:00:00` which will update our label to '00:00:00'. Next it will set the milliseconds, seconds and minutes to 0 and also disable the reset and the lap buttons. Lastly it will set the minutes, seconds and milliseconds keys in the `last_lap_time` dictionary to zero.

The `time_lap` method gets the current count and creates a string with the count and current stopwatch time: `lap_time = f"Count {self.count}: " + self.stopwatch_time`. Next a two line icon list item is created with the `lap_time` as text. The secondary text is calculated by the `calculate_time_difference` method, which subtracts the last recorded lap time from the current time. The list item is then added to the the list.

Running the application now, we get our functional stopwatch:


![Stopwatch](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbz28qrpytlsm6fakra7.gif)

All the code for this app is available in this [Github](https://github.com/Ngonie-x/stopwatch) repository.


In the next part of the tutorial, we look at how to create the timer so stay tuned!🙂