DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for How to create a simple To-do list application with Kivymd
Ngonidzashe Nzenze
Ngonidzashe Nzenze

Posted on • Updated on

How to create a simple To-do list application with Kivymd

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


Developing the application

Make sure your have installed kivy and kivymd 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:

#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()
Enter fullscreen mode Exit fullscreen mode

In main.kv add the following code:

#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}
Enter fullscreen mode Exit fullscreen mode

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

Todo sample


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

#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)
Enter fullscreen mode Exit fullscreen mode

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

# 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)
Enter fullscreen mode Exit fullscreen mode

Now modify main.kv:

# 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()
Enter fullscreen mode Exit fullscreen mode

Running our code so far:
Todo sample


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

# 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'''

Enter fullscreen mode Exit fullscreen mode

Modify the add_task function in the MainApp class:

# 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)

Enter fullscreen mode Exit fullscreen mode

main.kv

# 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)

Enter fullscreen mode Exit fullscreen mode

Running the application so far:
Todo sample 2


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

#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()
Enter fullscreen mode Exit fullscreen mode

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


Now to join this with the application interface:

main.py

#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 = ''
Enter fullscreen mode Exit fullscreen mode

And with that, we're done!

Todo Sample 3


Packaging for android

I have included all the code on github, 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:

#...

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])

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And

android.permissions = WRITE_EXTERNAL_STORAGE
Enter fullscreen mode Exit fullscreen mode

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


Cover Photo by Glenn Carstens-Peters on Unsplash


Top comments (10)

Collapse
 
rto profile image
Romy OpeΓ±a

Learned a lot from your sample app here. Now, may I ask you if I can modify-tinker with your program a little bit and integrate it with a few small apps into one single app for Android (for personal use only)? Thanks.

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

I am glad you learned something. Please make use of it as you see appropriate. You're welcome.

Collapse
 
aakritigoyal12458 profile image
AakritiGoyal12458

Tysm for such an amazing todo list

Collapse
 
yassimosan profile image
Yassim Osa

Thanks for this amazing project. I wanted to ask you a question: what exactly should I modify to change the Check Boxes with Radio Buttons? I've really tried everything, but without getting what I want. Thank you.

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Hi,

The check boxes in kivymd work as radio buttons if you group them. Check out this documentation for more details. I hope it will be helpful.

Collapse
 
spandan profile image
Spandan

Really nice and helpful post you nailed it .
I have saved it
Keep it up

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Thanks a lot

Collapse
 
youssouf profile image
Joseph BAYEMI

Thank for this tutorial. Nice written

Collapse
 
regina_parasi profile image
mybeaut

Hello,

How are you, I tried out your code but it fails with the below error.
init() missing 1 required positional argument: 'url'

Kindly assist.

Collapse
 
mudjaycker profile image
maryimana butom

Good Tuto even one year later ! liked it a lot ! hopefully kivy could be used as a good alternative to ionic & capacitor, java, native script frameworks family or even kotlin ... hhh

18 Useful Github Repositories Every Developer Should Bookmark

>> Check out this classic DEV post <<