Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 2: Screen Creation with Kivy & User Authentication)

Photo by Onur Binay on Unsplash

Building a Smart To-Do List Manager with Python, MongoDB & Kivy (Part 2: Screen Creation with Kivy & User Authentication)

Hello Tribe! Welcome to Part 2 of our To-Do app series.

Last time, we went over the project workflow, the setup and we created our database! Today, I will walk you through the creation of screens and user authentication (registration & log in).

Without further ado, let’s get into it.

1 - Screen Creation

Our first topic for today is how to build a screen using Kivy.

What is Kivy?

Kivy is an open-source Python framework for developing GUI apps that work cross-platform, including desktop, mobile and embedded platforms.

The aim is to allow for quick and easy interaction design and rapid prototyping whilst making your code reusable and deployable: Innovative user interfaces made easy.

Okay, well…How do you use it?

Quite simple actually. We are going to create the Welcome screen of our project. This is the first screen that can be seen when the project is launched. These are the generic steps to follow when creating a Kivy Window:

  • Initialise the Layout

    • Create a layout to hold the screen’s elements, such as GridLayout or BoxLayout.

    • Set the layout properties like cols, size_hint, pos_hint, padding, and spacing.

  • Add Widgets to the Layout

    • Add various widgets like Image, Label, Button, etc., to the layout.

    • Customise the widgets using parameters like size_hint, text, color, and more.

  • Bind Buttons to Functions for specific actions

    • Attach event listeners to buttons using the bind() method to define actions when the buttons are pressed.
  • Add the Layout to the Screen

    • Add the layout (which contains the widgets) to the screen using a syntax like self.add_widget().
  • Define Functions for Button Actions

    • Write functions for navigation (e.g., go_to_login, go_to_register) or other logic when buttons are clicked.

For our project, we will create a class name WelcomeScreen that will inherits from Kivy Screen class.

from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.core.window import Window
from kivymd.app import MDApp
from kivy.uix.image import Image

# First screen to choose between Login and Register
class WelcomeScreen(Screen):
    def __init__(self, **kwargs):
        super(WelcomeScreen, self).__init__(**kwargs)

        #Initialise the Layout
        self.window = GridLayout()
        self.window.cols = 1
        self.window.size_hint = (0.4, 0.7)
        self.window.pos_hint =  {"center_x":0.5, "center_y":0.5}
        # Add padding and spacing to the GridLayout
        self.window.padding = [20, 20, 20, 20]  # Padding around the grid
        self.window.spacing = [10, 10]  # Spacing between widgets
        #we can add our application's logo
        logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
        # Center the image horizontally
        logo_image.pos_hint = {"center_x": 0.5}
        #we add the image to the window/layout
        self.window.add_widget(logo_image)
        #creating and adding widgets/elements to the window/layout
        welcome_label = Label(text="Welcome to your Smart To-Do Manager", color=(0, 0, 0, 1), halign="center",valign="middle")
        self.window.add_widget(welcome_label)

        login_button = Button(text="Login", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
        self.window.add_widget(login_button)

        register_button = Button(text="Register", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
        self.window.add_widget(register_button)
        #here, we bind the buttons to specific functions for them to trigger an action
        login_button.bind(on_press=self.go_to_login)
        register_button.bind(on_press=self.go_to_register)

        # Add the complete layout to the screen
        self.add_widget(self.window)

    def go_to_login(self, instance):
        # Navigate to the login screen
        self.manager.current = 'login'

    def go_to_register(self, instance):
        # Navigate to the registration screen
        self.manager.current = 'register'

Explanation:

A- Imports

  1. from kivy.uix.screenmanager import Screen

    • Purpose: This imports the Screen class, which is used to define individual screens in a Kivy application that uses the screen manager. Each screen represents a different view or interface that the user can interact with.

    • Use in the code: The WelcomeScreen class inherits from Screen, making it a screen in the app.

  2. from kivy.uix.gridlayout import GridLayout

    • Purpose: This imports the GridLayout class, which is a layout that arranges its children (widgets) in a grid with rows and columns.

    • Use in the code: A GridLayout is used as the main layout to arrange the widgets (like labels, buttons, and images) in the welcome screen.

  3. from kivy.uix.label import Label

    • Purpose: This imports the Label class, which is a widget used to display text in the app.

    • Use in the code: The Label is used to show a welcome message to the user in the WelcomeScreen.

  4. from kivy.uix.button import Button

    • Purpose: This imports the Button class, which is a widget that the user can click to trigger an action (such as navigating to another screen or performing an action).

    • Use in the code: Button widgets are used for the "Login" and "Register" buttons on the screen.

  5. from kivy.uix.image import Image

    • Purpose: This imports the Image class, which is a widget used to display images in the app.

    • Use in the code: The Image widget is used to display a logo or graphic on the welcome screen (in this case, mylogo.png).

B- Code

  1. We define the Screen Class:

    The class inherits from Screen and represents the entire welcome screen. The __init__ method sets up the screen components.

  2. We initialise the layout

    Here, to initialise the layout, we used the GridLayout method as no specific orientation is needed.

    • We set the number of columns (cols=1) and adjust the size and position of the layout using size_hint and pos_hint.

    • We also add some padding to add extra pixels around the edges, keeping the size consistent during operations.

    • The spacing is used to ensure a certain distance between the elements

        #Initialise the Layout
        self.window = GridLayout()
        self.window.cols = 1
        self.window.size_hint = (0.4, 0.7)
        self.window.pos_hint =  {"center_x":0.5, "center_y":0.5}
        # Add padding and spacing to the GridLayout
        self.window.padding = [20, 20, 20, 20]  # Padding around the grid
        self.window.spacing = [10, 10]  # Spacing between widgets
      
  3. We add Widgets to the Layout

    Here, we add the widgets representing all the elements you want to display on our screen. On our Welcome screen, we are going to display our logo, a welcome message, the login and registration buttons.

    • We use the Image widget to create the logo and we will change its properties to make it centred.

    • We use the Label widget to display the text and the Button widget for the buttons. The key properties we play around here are

      • size_hint: (width_proportion, height_proportion). It is used to determine how a widget should be sized relative to its parent layout/container. Instead of specifying an absolute size for a widget (in pixels, for instance), size_hint allows you to set its size as a proportion of the available space in the parent container (like a BoxLayout or GridLayout).

      • color: is used to determine the color of the font used.

      • background_color is used to set a specific color for the background of a widget.

      • bold which obviously is to set a text in bold. When used in a Label widget, you have to add the property markup=True for the text to be set in bold.

    #we can add our application's logo
    logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
    # Center the image horizontally
    logo_image.pos_hint = {"center_x": 0.5}
    #we add the image to the window/layout
    self.window.add_widget(logo_image)
    #creating and adding widgets/elements to the window/layout
    welcome_label = Label(text="Welcome to your Smart To-Do Manager", color=(0, 0, 0, 1), halign="center",valign="middle")
    self.window.add_widget(welcome_label)

    login_button = Button(text="Login", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
    self.window.add_widget(login_button)

    register_button = Button(text="Register", size_hint=(0.5, 0.4), bold=True, background_color="#1E88E5")
    self.window.add_widget(register_button)
  1. We bind the buttons to the action functions

    Here, we use the bind() function to create a “triggering link“ between the buttons and the action functions. When pressed (on_press), the buttons will call the right functions.

     #defining some actions
     login_button.bind(on_press=self.login_action)
     back_button.bind(on_press=self.go_to_welcome)
    
  2. The action functions

    In the code, I have written two functions that will help us to go either on the login or the registration page.

    Simply put, since the class inherits from Screen, we have a screen manager that will allow us to go from a screen to another.

     def go_to_login(self, instance):
         # Navigate to the login screen
         self.manager.current = 'login'
    
     def go_to_register(self, instance):
         # Navigate to the registration screen
         self.manager.current = 'register'
    

Now, we can initialise our app and visualise our screen.

We create a ToDoApp class that inherits from MDApp.
I choose MDApp (from kivymd.app import MDApp) instead of Kivy App (from kivy.app import App) because to create the tasks later, we will need a date picker, a feature that the simple Kivy does not have.

class ToDoApp(MDApp):
    def __init__(self, dbname, **kwargs):
        super(ToDoApp, self).__init__(**kwargs)
        self.dbname = dbname

    def build(self):
        sm = ScreenManager()
        # Add WelcomeScreen, LoginScreen, and RegisterScreen to ScreenManager
        sm.add_widget(WelcomeScreen(name='welcome'))
        return sm

Now, in my main function, I run this code:

if __name__ == "__main__":   
   # Get the database
   dbname = mongo.get_database()
   #code pour lancer le projet en interface graphique
   graphic.ToDoApp(dbname).run()

We have this cute window

2- User Authentication

Now that we have created our first page, let’s create the logic for user registration and login.

A- User Registration

First, we will write the code not considering the intervention of Kivy, just as if we were implementing a command line application. This will be a foundation to re-write the functions to serve the purposes of the Kivy classes.

Let’s say, to register, the user has to type in the following command: register xxxx. We will implement a logic to parse the command entered by the user so that we have for this case an array where x[0] = ‘register‘ and x[1] = ‘xxxx‘. So, our register function will take the username typed and the database variable as parameters.

First, let’s see how the parsing should go. For practicality, we will use a dictionary to map each command to its action function.

PS: All the functions you see are here because obviously, they were all implemented. They are not all needed just yet.

command_map = {
    "login": commands.login,
    "register": commands.register,
    "add_task": commands.add_task,
    "edit_task": commands.edit_task,
    "delete_task": commands.delete_task,
    "view_task": commands.view_task,
    "search": commands.search
}

single_arg_command_map = {
    "ongoing_tasks": commands.ongoing_tasks,
    "view_completed_tasks": commands.view_completed_tasks
}

def read_cmd(user_input, dbname):
    split_cmd = shlex.split(user_input)
    if len(split_cmd) == 1:
        if split_cmd[0] == "help":
            print("help")
        if split_cmd[0] == "exit":
            #84 is the exit code
            return(84)
    if len(split_cmd) == 1 and split_cmd[0] in single_arg_command_map:
        single_arg_command_map[split_cmd[0]](dbname)
    if len(split_cmd) == 2 and split_cmd[0] in command_map:
        dbname = command_map[split_cmd[0]](split_cmd[1], dbname)
    return 0
  • In our registration function, we ask the user for a mail and password. For the password, we use the getpass function so that the password is not visible. Here’s what it looks like:

      mail = input("Enter a valid mail adress:\n")
          #add a verification with regex
          password = getpass.getpass("Set a password. It has to be at least 7 characters long:\n") 
          while len(password) < 7:
             password = getpass.getpass("Not strong enough. Try again:\n")
             if len(password) >= 7:
               break
    
  • For security purpose, the password needs to be hashed before storage. Here is a function that can do just that using the bcrypt module:

    You can read more about how to hash passwords in Python here.

      def hash_password(plain_password):
          # Generate a salt and hash the password
          salt = bcrypt.gensalt()
          hashed_password = bcrypt.hashpw(plain_password.encode('utf-8'), salt)
          return hashed_password
    
  • Now, let’s check if the username already exists:

      while dbname["users"].find_one({"name":username}):
          username = input("Username exits! Login or Enter new name:\n")
    
  • If everything is correct, we create a dictionary with all user’s information and insert it into our users collection.

      new_user_infos = {"name":username, "mail":mail, "password":hpwd}
      #we create our first collection inside the database
      users_collection = dbname["users"]
      if users_collection.insert_one(new_user_infos):
          print("Registration successful! Proceed.\n")
          connected_user = username
    

    You might have noticed connected_user = username. Good: I use connected_user as session management variable. Of course, there are different ways to do so but I chose this simple direction and it works for me (and this little app). I store everything with the name of the connected user making it simple to display user’s specific informations.

    Here is the whole function:

      def register(username,dbname):
          global connected_user
          mail = input("Enter a valid mail adress:\n")
          #add a verification with regex
          password = getpass.getpass("Set a password. It has to be at least 7 characters long:\n") 
          while len(password) < 7:
             password = getpass.getpass("Not strong enough. Try again:\n")
             if len(password) >= 7:
               break
          hpwd = hash_password(password)
          while dbname["users"].find_one({"name":username}):
              username = input("Username exits! Login or Enter new name:\n")
          #new_user_infos is a dictionary containing all the information about the user that is trying to register
          new_user_infos = {"name":username, "mail":mail, "password":hpwd}
    
          #we create the "users" collection inside the database
          users_collection = dbname["users"]
          if users_collection.insert_one(new_user_infos):
              print("Registration successful! Proceed.\n")
              connected_user = username
          return dbname
    

To create a version of this function fit to be integrated into our Kivy class, we will apply some little changes.

def register_user(username, mail, password, dbname):
    global connected_user
    #check if the user already exits
    if dbname["users"].find_one({"name":username}):
        return None, "User already exists"
    if len(password) < 7:
       #verify that the password's length is above the required length
       return None, "Not strong enough. Try again:"
    if len(password) >= 7:
        hpwd = hash_password(password)
        #new_user_infos is a dictionary containing all the information about the user that is trying to register
        new_user_infos = {"name":username, "mail":mail, "password":hpwd}
        users_collection = dbname["users"]
        if users_collection.insert_one(new_user_infos):
            #we stored the username as the connected user
            connected_user = username
            return dbname, "Registration succesful!"
    return None, ""
  • This function will take as parameters the username, the mail, the password and the database variable. The username, the mail and the password here will be attributes of our RegistrationScreen class and used in our function.

  • The function returns:

    • the database variable and a message when the operation is successful.

    • None and a message when there is an issue. This will help us know in the class what kind of message needs to be printed on the screen.

Following the logic of our Welcome screen, the definition of the screen should look approximately the same. But here, our class has to be created with a database variable as arguments so that we can access the database variable created at the beginning of the project. As matter of fact, all the screens from now on will be taking dbname as parameter of their constructor.

Of course, you can modify this code to fit your specific needs. In the code, I used comments to help you through each step.

def __init__(self, dbname, **kwargs):
    super(RegisterScreen, self).__init__(**kwargs)
    #this helps us ensure the page is completely blank, white background
    Window.clearcolor = (1, 1, 1, 1)

    #initialising the layout of the window/screen using GridLayout
    self.window = GridLayout()
    self.window.cols = 1
    self.window.size_hint = (0.4, 0.8)
    self.window.pos_hint =  {"center_x":0.5, "center_y":0.5}
    # Add padding and spacing to the GridLayout
    self.window.padding = [15,15,15,15]  # Padding around the grid
    self.window.spacing = [10, 10]  # Spacing between widgets

    #creating elements or widgets
    logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
    logo_image.pos_hint = {"center_x": 0.5}  # Center the image horizontally
    self.window.add_widget(logo_image)
    #creating the attributes of the class
    self.label = Label(text="[b]Register[/b]", font_size=40, color=(0, 0, 0, 1), halign="center",valign="middle", markup=True)
    self.mail = TextInput(hint_text='Mail address', multiline=False, hint_text_color=(0, 0, 0, 1))
    self.username = TextInput(hint_text='Username', multiline=False, hint_text_color=(0, 0, 0, 1))
    self.password = TextInput(hint_text='Input a strong password: at least seven characters', multiline=False, password=True, password_mask='*', hint_text_color=(0, 0, 0, 1))
    self.dbname = dbname
    register_button = Button(text='Register', size_hint=(1, 0.6), bold=True,  background_color="#004aad")
    back_button = Button(text='Back', size_hint=(1, 0.6), bold=True, background_color="#004aad")
    #this variable is supposed to hold the error message to be displayed when there is an issue
    self.error_message = Label(text="", color=(1, 0, 0, 1))  # Empty error message, red color

    #defining some actions and binding
    register_button.bind(on_press=self.create_user)  # Bind to registration function
    back_button.bind(on_press=self.go_to_welcome) #goes back to the welcome page

    #adding elements/widgets to the layout/window
    self.window.add_widget(self.label)
    self.window.add_widget(self.mail)
    self.window.add_widget(self.username)
    self.window.add_widget(self.password)
    self.window.add_widget(Label(text="Already have an account? Go back", font_size=18))
    self.window.add_widget(register_button)
    self.window.add_widget(back_button)
    self.window.add_widget(self.error_message)
    self.add_widget(self.window)

I suggest you play around with the properties of each widget so that you have a better understanding of how they work.

Check out the documentation of Kivy here.

The create_user method takes as parameters self that is a reference to the current instance of the class and instance which is a reference to the object or widget that triggered a specific event.

  • We store the values of our attributes (username, password and mail) in local variables to use in the function for a safer management.

  • We call the register_user function and proceed to some verifications. According to the message, and the content of the data variable, we display a message (in green or in red) and when there is an error, we empty the fields for the user to enter new ones.

      #Note: gcommands is just a file imported and used as a module
      def create_user(self, instance):
          username = self.username.text
          password = self.password.text
          mail = self.mail.text
          dbname = self.dbname
          #we call the register_user function
          data, msg = manage_tasks.gcommands.register_user(username, mail, password, dbname)
          if data != None and msg == "Registration succesful":
              self.error_message.text = "Registration succesful!"
              self.error_message.color = (0, 1, 0, 1)  # Green color for success
              #this method will be used to go from the Registration screen to the main screen with the tasks
              self.go_to_manager(None)
          if data == None and msg == "User already exists":
              self.error_message.text = "User already exists"
              self.error_message.color = (1, 0, 0, 1)  # Red color for error
              # Optionally, clear the fields or leave them for correction
              self.username.text = ""
              self.password.text = ""
          if data == None and msg=="Not strong enough. Try again:":
              self.error_message.text = "Not strong enough. Try again:"
              self.error_message.color = (1, 0, 0, 1)
              self.username.text = ""
              self.password.text = ""
          else:
              self.error_message.text = "Something went wrong"
              self.error_message.color = (1, 0, 0, 1)
              self.username.text = ""
              self.password.text = ""
    

Let’s take a look at what our class looks like now.

class RegisterScreen(Screen):
    def __init__(self, dbname, **kwargs):
        super(RegisterScreen, self).__init__(**kwargs)
        Window.clearcolor = (1, 1, 1, 1)

        self.window = GridLayout()
        self.window.cols = 1
        self.window.size_hint = (0.4, 0.8)
        self.window.pos_hint =  {"center_x":0.5, "center_y":0.5}
        self.window.padding = [15,15,15,15]  # Padding around the grid
        self.window.spacing = [10, 10]  # Spacing between widgets

        logo_image = Image(source='mylogo.png', size_hint=(0.4, None), size=(500, 500), allow_stretch=True)
        logo_image.pos_hint = {"center_x": 0.5}  # Center the image horizontally
        self.window.add_widget(logo_image)
        self.label = Label(text="[b]Register[/b]", font_size=40, color=(0, 0, 0, 1), halign="center",valign="middle", markup=True)
        self.mail = TextInput(hint_text='Mail address', multiline=False, hint_text_color=(0, 0, 0, 1))
        self.username = TextInput(hint_text='Username', multiline=False, hint_text_color=(0, 0, 0, 1))
        self.password = TextInput(hint_text='Input a strong password: at least seven characters', multiline=False, password=True, password_mask='*', hint_text_color=(0, 0, 0, 1))
        self.dbname = dbname
        register_button = Button(text='Register', size_hint=(1, 0.6), bold=True,  background_color="#004aad")
        back_button = Button(text='Back', size_hint=(1, 0.6), bold=True, background_color="#004aad")
        self.error_message = Label(text="", color=(1, 0, 0, 1))  # Empty error message, red color

        register_button.bind(on_press=self.create_user)  # Bind to registration function
        back_button.bind(on_press=self.go_to_welcome) #goes back to the welcome page

        #adding elements/widgets to the window
        self.window.add_widget(self.label)
        self.window.add_widget(self.mail)
        self.window.add_widget(self.username)
        self.window.add_widget(self.password)
        self.window.add_widget(Label(text="Already have an account? Go back", font_size=18))
        self.window.add_widget(register_button)
        self.window.add_widget(back_button)
        self.window.add_widget(self.error_message)

        self.add_widget(self.window)

    def create_user(self, instance):
        username = self.username.text
        password = self.password.text
        mail = self.mail.text
        dbname = self.dbname

        data, msg = manage_tasks.gcommands.register_user(username, mail, password, dbname)
        if data != None and msg == "Registration succesful":
            self.error_message.text = "Registration succesful!"
            self.error_message.color = (0, 1, 0, 1) # Green color for success
            self.go_to_manager(None)
        if data == None and msg == "User already exists":
            self.error_message.text = "User already exists"
            self.error_message.color = (1, 0, 0, 1)  # Red color for error
            # Optionally, clear the fields or leave them for correction
            self.username.text = ""
            self.password.text = ""
        if data == None and msg=="Not strong enough. Try again:":
            self.error_message.text = "Not strong enough. Try again:"
            self.error_message.color = (1, 0, 0, 1)
            self.username.text = ""
            self.password.text = ""
        else:
            self.error_message.text = "Something went wrong"
            self.error_message.color = (1, 0, 0, 1)
            self.username.text = ""
            self.password.text = ""

    def go_to_welcome(self, instance):
        # Navigate to the login screen
        self.manager.current = 'welcome'

    def go_to_manager(self, instance):
        # Navigate to the main screen
        self.manager.current = 'task_list'

B- User Login

Here again, we will first take a look at what the login function should look like if our application were a command line application instead of a Kivy one.

  • With the same considerations as previously, we know that to login, the user has to type in login xxxx. This command is parsed so that x[0] = ‘login‘ and x[1] = ‘xxxx‘. Our function will take the username typed and the database variable as parameters.

  • Since a username has been provided, we will directly use the find_one method of MongoDB to check if the user is registered.

  • If he/she is registered, we now ask for the password. We use the bcrypt.checkpw method to check if the given password (hashed) is equal to the stored password for this user (hashed of course =) !).

  • As long as the password is incorrect, we keep prompting the user to enter a correct password. Our function should look like this:

      def login(username, dbname):
          global connected_user
          users_collection = dbname["users"]
          #the user has entered his/her name, let's see if it exists
          logging_user = users_collection.find_one({"name":username})
          #if found
          if logging_user:
              #we check if the password is correct
              password = getpass.getpass("What's your password? ")
              pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"])
              while not pwd_check:
                  password = getpass.getpass("Password incorrect:\n")
                  pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"])
              print("Login successful!\n")
              connected_user = username
              return dbname
          return dbname
    

Considering how our current function is implemented, I think you might have guessed how the “Kivy conscious“ version of it should be written.

  • The function takes the username, the password (stored as attributes in our class) and the database variable.

  • Using the find_one function, we check if the user is registered. It yes, we check if the password is correct. If the password is incorrect, we return None and an error message and if everything is correct, we return the database variable and the success message.

  • You might have noticed that I didn't specify anything in case the user is not found. The function will return the good values in very specific cases and if something went wrong (anything that I haven’t thought of), this line return None, "" will take care of it. But feel free to specify all events for safety!

def login_user(username, password, dbname):
    global connected_user
    users_collection = dbname["users"]
    #l'utilisateur met son nom
    logging_user = users_collection.find_one({"name":username})
    #rechercher l'utilisateur
    if logging_user:
        pwd_check = bcrypt.checkpw(password.encode('utf-8'), looging_user["password"])
        if not pwd_check:
            return None, "Incorrect password"
        else:
            connected_user = username
            return dbname, "Login successful!"
    return None, ""

Let’s integrate it in our class, shall we? We will use the same logic as for the registration, using the function we just wrote in a method of the class.

def login_action(self, instance):
    username = self.username.text
    password = self.password.text
    dbname = self.dbname

    data, msg = manage_tasks.gcommands.login_user(username, password, dbname)
    if data != None and msg == "Login successful!":
       self.error_message.text = "Login successful!"
       self.error_message.color = (0, 1, 0, 1)  # Green color for success
       self.go_to_manager(None)
    else:
       self.error_message.text = "Invalid username or password!"
       self.error_message.color = (1, 0, 0, 1)  # Red color for error
       # Optionally, clear the fields or leave them for correction
       self.username.text = ""
       self.password.text = ""

Now, this time around, I am not going to show you what the class for the Login screen looks like, I think you have a pretty good idea of that by now. But anyways, here’s how the screen should be integrated to the app.

class ToDoApp(MDApp):
    def __init__(self, dbname, **kwargs):
        super(ToDoApp, self).__init__(**kwargs)
        self.dbname = dbname

    def build(self):
        sm = ScreenManager()

        # Add WelcomeScreen, LoginScreen, and RegisterScreen to ScreenManager
        sm.add_widget(WelcomeScreen(name='welcome'))
        sm.add_widget(LoginScreen(self.dbname, name='login'))
        sm.add_widget(RegisterScreen(self.dbname, name='register'))
        return sm

After execution, you should have nice screens like these:

Tadaaaaaaaaa!!! We now have three pages and we can finally work on those tasks! But that will be the main focus of the next article.

Thank you for working with me today and stay tuned for part 3 where we are going to create the main screen with tasks and implement some task management (add, edit and delete) features!

Toodaloo! 👋🏾