DEV Community

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

Posted on • Edited 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 (20)

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
 
dharshikisarapu profile image
dharshikisarapu

checking whether the C compiler works... no
configure: error: in /content/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/other_builds/freetype/armeabi-v7a__ndk_target_21/freetype/builds/unix':
configure: error: C compiler cannot create executables
See
config.log' for more details
make: *** [builds/unix/detect.mk:91: setup] Error 77
how can i resolve this

Collapse
 
joser profile image
Jose Cruz Recinos

It looks like the problems is that you have in your buildozer.spec the target architecture like this: "android.archs = arm64-v8a, armeabi-v7a" and then buildozer cannot find the sources to install in order to compile the packages from the "requirements=" option for this architecture "armeabi-v7a", the easiest way to solve this issue is to remove that architecture and compile for "arm64-v8a" only since the armeabi-v7a or arm-v7a it's already deprecated.

Collapse
 
overflow profile image
overFlow

hey YOU!!! new guy!!!!
WE welcome you here by force. with love.
You like it or not you shall be loved here at dev.to. Loved and Loved Dearly.
WElcome.

Collapse
 
agrillea profile image
agrillea

I'm running this great example you made. Unfortunately, the requirements you specified are no longer valid. What do I need to change in the Buildozer requirements to make it work. Thank you

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Glad you liked the article! I'll update the article soon.

I've managed to use the following requirements to get it to run successfully:
requirements = python3,kivy==2.1.0,kivymd==1.1.1,sdl2_ttf==2.0.15,pillow,android

Hope that helps

Collapse
 
joser profile image
Jose Cruz Recinos

Does anybody knows how to make this function work with KivyMD 2.0.1.dev0?
def show_task_dialog(self):
if not self.task_list_dialog:
self.task_list_dialog = MDDialog(
cls=DialogContent(),
)
self.task_list_dialog.open()
Like this above it results an error before to run the app:
Error: 'MainApp' object has no attribute 'task_list_dialog'
So I changed to this:
def show_task_dialog(self):
self.task_list_dialog = MDDialog(
cls=DialogContent(),
)
self.task_list_dialog.open()
Then it runs but it results in another error when called by:
on_release: app.show_task_dialog()
The console log bellow:
self.task_list_dialog = MDDialog(
^^^^^^^^^
File "kivy/event.pyx", line 262, in kivy._event.EventDispatcher.init_
File "kivy/properties.pyx", line 520, in kivy.properties.Property.set
File "kivy/properties.pyx", line 938, in kivy.properties.ListProperty.set
File "kivy/properties.pyx", line 815, in kivy.properties.ObservableList.init
TypeError: 'DialogContent' object is not iterable

Any idea how to fix it? thank you.

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
 
vinaybsee profile image
vinaybsee

It is a great post. I have tried the same steps. Everything works fine, but while converting to apk and after installing in android app it is showing loading and closes the app. Any way to resolve that. I followed the proper steps and created the .apk. Please can you guide me as I am in a learning phase using python.

Collapse
 
ngonidzashe profile image
Ngonidzashe Nzenze

Hello. I'm glad you liked the post. The best way to figure out why your app is crashing on android is to install it using ADB(android debug bridge). This will allow you to trace the error encountered by your program during execution. If you have never used ADB before, check out this article(the very bottom section), in which I go over the basics of how you can use it. I hope that helps :)

Collapse
 
sehgalspandan profile image
Spandan Sehgal

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

Collapse
 
youssouf profile image
Joseph BAYEMI

Thank for this tutorial. Nice written