How to create a simple To-do list application with Kivymd
Tags: python, kivymd, tutorial, android
Published: 2021-10-19 13:11:06

To-do list applications are a simple way to get started with learning different frameworks. I am going show you how to create one. With that, let's look at what the final application looks like on an android device:

![todo on android](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxeq92ugal8o0tawcisz.gif)

---

## Developing the application

Make sure your have installed [kivy](https://kivy.org/#home) and [kivymd](https://kivymd.readthedocs.io/en/latest/) in a virtual environment.

Create 3 files in the same directory, namely:

- `main.py` - will to contain most of the application code and logic.
- `main.kv` - will contain code to display the interface. 
- `database.py` - will contain all the database code.

Inside `main.py`, add the following code:
```python
#main.py
from kivymd.app import MDApp

class MainApp(MDApp):
    def build(self):
        # Setting theme to my favorite theme
        self.theme_cls.primary_palette = "DeepPurple"

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

In `main.kv` add the following code:
```python
#main.kv

MDFloatLayout:
    MDLabel:
        id: task_label
        halign: 'center'
        markup: True
        text: "[u][size=48][b]My Tasks[/b][/size][/u]"
        pos_hint: {'y': .45}

    ScrollView:
        pos_hint: {'center_y': .5, 'center_x': .5}
        size_hint: .9, .8

        MDList:
            id: container

    MDFloatingActionButton:
        icon: 'plus-thick'
        on_release: app.show_task_dialog() #functionality to be added later
        elevation_normal: 12
        pos_hint: {'x': .8, 'y':.05}
```

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

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

---

### Add Tasks

Next we are going to create a dialog box in which we will be able to add tasks. The dialog box will allow us to enter the task name and completion date:

`main.py`

```python
#main.py

# add the following imports
from kivymd.uix.dialog import MDDialog
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.picker import MDDatePicker
from datetime import datetime

class DialogContent(MDBoxLayout):
    """OPENS A DIALOG BOX THAT GETS THE TASK FROM THE USER"""
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # set the date_text label to today's date when useer first opens dialog box
        self.ids.date_text.text = str(datetime.now().strftime('%A %d %B %Y'))

    
    def show_date_picker(self):
        """Opens the date picker"""
        date_dialog = MDDatePicker()
        date_dialog.bind(on_save=self.on_save)
        date_dialog.open()

    def on_save(self, instance, value, date_range):
        """This functions gets the date from the date picker and converts its it a
        more friendly form then changes the date label on the dialog to that"""

        date = value.strftime('%A %d %B %Y')
        self.ids.date_text.text = str(date)
```

Now change the `MainApp` class inside `main.py` to look like this:

```python
# main.py

#...

class MainApp(MDApp):
    task_list_dialog = None # Here
    def build(self):
        # Setting theme to my favorite theme
        self.theme_cls.primary_palette = "DeepPurple"

    # Add the below functions
    def show_task_dialog(self):
        if not self.task_list_dialog:
            self.task_list_dialog = MDDialog(
                title="Create Task",
                type="custom",
                content_cls=DialogContent(),
            )

        self.task_list_dialog.open()

    def close_dialog(self, *args):
        self.task_list_dialog.dismiss()

    def add_task(self, task, task_date):
        '''Add task to the list of tasks'''

        print(task.text, task_date)
        task.text = '' # set the dialog entry to an empty string(clear the text entry)
```

Now modify `main.kv`:

```python
# main.kv

#...

# add the following
<DialogContent>:
    orientation: "vertical"
    spacing: "10dp"
    size_hint: 1, None
    height: "130dp"

    GridLayout:
        rows: 1

        MDTextField:
            id: task_text
            hint_text: "Add Task..."
            pos_hint: {"center_y": .4}
            max_text_length: 50
            on_text_validate: (app.add_task(task_text, date_text.text), app.close_dialog())

        MDIconButton:
            icon: 'calendar'
            on_release: root.show_date_picker()
            padding: '10dp'

    MDLabel:
        spacing: '10dp'
        id: date_text

    BoxLayout:
        orientation: 'horizontal'

        MDRaisedButton:
            text: "SAVE"
            on_release: (app.add_task(task_text, date_text.text), app.close_dialog())
        MDFlatButton:
            text: 'CANCEL'
            on_release: app.close_dialog()
```

Running our code so far:
![Todo sample](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dot2cvjfnq3dsg2fc3pk.gif)

---

Now we want to add list items to the screen. We are going to create a custom list item with a checkbox to the left and a delete icon to the right:

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

#...

# Add these imports
from kivymd.uix.list import TwoLineAvatarIconListItem, ILeftBodyTouch
from kivymd.uix.selectioncontrol import MDCheckbox

# create the following two classes
class ListItemWithCheckbox(TwoLineAvatarIconListItem):
    '''Custom list item'''

    def __init__(self, pk=None, **kwargs):
        super().__init__(**kwargs)
        # state a pk which we shall use link the list items with the database primary keys
        self.pk = pk


    def mark(self, check, the_list_item):
        '''mark the task as complete or incomplete'''
        if check.active == True:
            # add strikethrough to the text if the checkbox is active
            the_list_item.text = '[s]'+the_list_item.text+'[/s]'
        else:
            # we shall add code to remove the strikethrough later
            pass

    def delete_item(self, the_list_item):
        '''Delete the task'''
        self.parent.remove_widget(the_list_item)



class LeftCheckbox(ILeftBodyTouch, MDCheckbox):
    '''Custom left container'''

```

Modify the `add_task` function in the `MainApp` class:

```python
# main.py

#...

class MainApp(MDApp):
    #...
    def add_task(self, task, task_date):
        '''Add task to the list of tasks'''

        print(task.text, task_date)
        self.root.ids['container'].add_widget(ListItemWithCheckbox(text='[b]'+task.text+'[/b]', secondary_text=task_date))
        task.text = '' # set the dialog entry to an empty string(clear the text entry)

```

`main.kv`

```python
# main.kv

# add the following code
<ListItemWithCheckbox>:
    id: the_list_item
    markup: True

    LeftCheckbox:
        id: check
        on_release: 
            root.mark(check, the_list_item)

    IconRightWidget:
        icon: 'trash-can-outline'
        theme_text_color: "Custom"
        text_color: 1, 0, 0, 1
        on_release:
            root.delete_item(the_list_item)

```

Running the application so far:
![Todo sample 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v50mmdrjo9js64acmgjf.gif)

---

Ok, now to work on the code for the database. Inside `database.py` add the following code:

```python
#database.py

import sqlite3

class Database:
    def __init__(self):
        self.con = sqlite3.connect('todo.db')
        self.cursor = self.con.cursor()
        self.create_task_table() #create the tasks table

    def create_task_table(self):
        """Create tasks table"""
        self.cursor.execute("CREATE TABLE IF NOT EXISTS tasks(id integer PRIMARY KEY AUTOINCREMENT, task varchar(50) NOT NULL, due_date varchar(50), completed BOOLEAN NOT NULL CHECK (completed IN (0, 1)))")
        self.con.commit()
        
    def create_task(self, task, due_date=None):
        """Create a task"""
        self.cursor.execute("INSERT INTO tasks(task, due_date, completed) VALUES(?, ?, ?)", (task, due_date, 0))
        self.con.commit()

        # GETTING THE LAST ENTERED ITEM SO WE CAN ADD IT TO THE TASK LIST
        created_task = self.cursor.execute("SELECT id, task, due_date FROM tasks WHERE task = ? and completed = 0", (task,)).fetchall()
        return created_task[-1]

    def get_tasks(self):
        """Get all completed and uncomplete tasks"""
        uncomplete_tasks = self.cursor.execute("SELECT id, task, due_date FROM tasks WHERE completed = 0").fetchall()
        completed_tasks = self.cursor.execute("SELECT id, task, due_date FROM tasks WHERE completed = 1").fetchall()
        # return the tasks to be added to the list when the application starts
        return completed_tasks, uncomplete_tasks

    

    def mark_task_as_complete(self, taskid):
        """Mark tasks as complete"""
        self.cursor.execute("UPDATE tasks SET completed=1 WHERE id=?", (taskid,))
        self.con.commit()

    def mark_task_as_incomplete(self, taskid):
        """Mark task as uncomplete"""
        self.cursor.execute("UPDATE tasks SET completed=0 WHERE id=?", (taskid,))
        self.con.commit()

        # return the task text
        task_text = self.cursor.execute("SELECT task FROM tasks WHERE id=?", (taskid,)).fetchall()
        return task_text[0][0]

    def delete_task(self, taskid):
        """Delete a task"""
        self.cursor.execute("DELETE FROM tasks WHERE id=?", (taskid,))
        self.con.commit()

    def close_db_connection(self):
        self.con.close()
```

The code above allows us to create, delete and modify tasks in the database.

---

Now to join this with the application interface:

`main.py`

```python
#main.py

#...

# add import
from database import Database
# Initialize db instance
db = Database()

# Modify the ListItemWithCheckbox class
class ListItemWithCheckbox(TwoLineAvatarIconListItem):
    #...
    def mark(self, check, the_list_item):
        '''mark the task as complete or incomplete'''
        if check.active == True:
            the_list_item.text = '[s]'+the_list_item.text+'[/s]'
            db.mark_task_as_complete(the_list_item.pk)# here
        else:
            the_list_item.text = str(db.mark_task_as_incomplete(the_list_item.pk))# Here

    def delete_item(self, the_list_item):
        '''Delete the task'''
        self.parent.remove_widget(the_list_item)
        db.delete_task(the_list_item.pk)# Here


# Modify the MainApp class
class MainApp(MDApp):
    #...

    # add this entire function
    def on_start(self):
        """Load the saved tasks and add them to the MDList widget when the application starts"""
        try:
            completed_tasks, uncomplete_tasks = db.get_tasks()

            if uncomplete_tasks != []:
                for task in uncomplete_tasks:
                    add_task = ListItemWithCheckbox(pk=task[0],text=task[1], secondary_text=task[2])
                    self.root.ids.container.add_widget(add_task)

            if completed_tasks != []:
                for task in completed_tasks:
                    add_task = ListItemWithCheckbox(pk=task[0],text='[s]'+task[1]+'[/s]', secondary_text=task[2])
                    add_task.ids.check.active = True
                    self.root.ids.container.add_widget(add_task)
        except Exception as e:
            print(e)
            pass

    # Modify the add_task function
    def add_task(self, task, task_date):
        '''Add task to the list of tasks'''

        # Add task to the db
        created_task = db.create_task(task.text, task_date)# Here

        # return the created task details and create a list item
        self.root.ids['container'].add_widget(ListItemWithCheckbox(pk=created_task[0], text='[b]'+created_task[1]+'[/b]', secondary_text=created_task[2]))# Here
        task.text = ''
```

And with that, we're done!

![Todo Sample 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iuzn6xhac9dljstu1yo.gif)

---

## Packaging for android

I have included all the code on [github](https://github.com/Ngonie-x/todo-application-kivymd), including the spec file I used to generate the apk.

A few changes are required so that we can create an android application. Edit `main.py` as follows:

```python
#...

from kivymd.uix.pickers import MDDatePicker # Here, instead of kivymd,uix.picker

# add the following just under the imports
if platform == "android":
    from android.permissions import request_permissions, Permission
    request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE])

```
The above code will prompt the user to allow the application to access storage.

Main changes in the buildozer spec file are as follows:
```
requirements = python3, kivy==2.1.0, https://github.com/kivymd/KivyMD/archive/master.zip,sdl2_ttf==2.0.15,pillow,android
```

And

```
android.permissions = WRITE_EXTERNAL_STORAGE
```

---

That's all for this tutorial. I hope you enjoyed it.

---

Cover Photo by <a href="https://unsplash.com/@glenncarstenspeters?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Glenn Carstens-Peters</a> on <a href="https://unsplash.com/s/photos/to-do-list?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>

---