'kivy screen freeze when sending a requests over internet

I am totally new in kivy and I was working this as a practice project. I searched a lot but can't find the solution. Basically the problem is when I send a requests over internet my kivy screen freeze for some time. Because of this my spinner(MDSpinner) is not working. Please kindly anyone help me.

My main.py code:

from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.datatables import MDDataTable
from kivy.uix.screenmanager import ScreenManager, Screen
import requests
import shutil
from bs4 import BeautifulSoup as Soup
import re
import os



#Not Real Url for some security reason
Login_URL = "http://some_url.com/login"
Back_URL = "http://some_url.com/info"
Profile_URL = "http://some_url.com/profile"

SESSION = None
sm = ScreenManager()

stu_name = None
stu_id = None


def login(uname_passw):
    
    uname,passw = uname_passw.split(":::")

    s = requests.Session()

    data = {'email': uname, 'password': passw}

    try:
        r = s.post(Login_URL, data=data)
        text = r.text
    except:
        text = "<div>Net Problem > Login > </div>"

    soup = Soup(text, 'html.parser')

    error = soup.find(class_="error_msg")
    if error is not None:
        error_msg = error.get_text()
        error_msg = error_msg.split(".")[0] + "."

        return error_msg

    else:
        global SESSION
        SESSION = s

        return True


def get_info(session):
    s = SESSION
    url = Back_URL

    try:

        r = s.get(url)
        text = r.text
    except:
        text = "<div>Net Problem > Get_Info > </div>"

    soup = Soup(text, "html.parser")

    tables = soup.find_all(class_="table table-striped table-responsive table-bordered table-hover")
    stu_basic_info = tables[0]
    stu_drop_change = tables[1]

    basic_tds = stu_basic_info.find_all("td")

    basic_finfo = []
    for td in basic_tds:
        t = td.get_text()
        if "Admission Fee" in str(t):
            t = t + soup.find(id="sum_admission_fee").get("value")
        basic_finfo.append(t)

    drop_tds = stu_drop_change.find_all("td")

    drop_change_finfo = []
    for td in drop_tds:
        t = td.get_text()
        drop_change_finfo.append(t)

    basic_info = {}

    for i in basic_finfo:
        if "Attempted credit" in i:
            break
        txt = i.split(":")
        points = txt[0]
        answer = txt[1]

        pattern = '[A-z0-9.]+'

        s1 = re.findall(pattern, points)
        s1 = " ".join(s1)

        s2 = re.findall(pattern, answer)
        s2 = " ".join(s2)

        basic_info[s1] = s2

    total_fee = basic_info['Total Fee']
    discount = basic_info['Waiver Discount']

    payable = str(int(total_fee) - int(discount))

    basic_info['Payable Amount'] = payable

    d = []
    for i in drop_change_finfo:
        c = " ".join(re.findall(pattern, i))
        d.append(c)

    drop_change_finfo = d

    numbers = []
    names = []
    for i in drop_change_finfo:
        r = " ".join(re.findall('[0-9]+', i))
        if r != '':
            numbers.append(r)
        r2 = " ".join(re.findall('[A-z]+', i))
        if r2 != '':
            names.append(r2)

    extra_info = {}
    for name, num in zip(names, numbers):
        extra_info[name] = num

    return basic_info, extra_info


class LoginScreen(Screen):
    def on_pre_enter(self, *args):
        dir_list = os.listdir(os.getcwd())

        for file in dir_list:
            if "img" in file:
                os.remove(file)
    
    

    def user_login(self):
        self.ids.spinner1.active = True
        
        
        uname = self.ids.uname.text
        passw = self.ids.passw.text
        
        uname_passw = f"{uname}:::{passw}"
            
            
        self.user_login_var = login(uname_passw)

        if self.user_login_var is not True:
            msg = self.ids.msg
            msg.text = str(self.user_login_var)
            msg.theme_text_color = "Error"
            msg.font_style = "Subtitle1"
            msg.pos_hint = {'center_x': 0.5, 'center_y': 0.6}
        else:
            sm.switch_to(InfoScreen(name='info'))


class InfoScreen(Screen):

    def on_pre_enter(self, *args):
        self.info_dict, self.extra_info = get_info(SESSION)

    def on_enter(self, *args):

        global stu_name
        global stu_id

        stu_name, stu_id = self.info_dict['Student Name'], self.info_dict['Student ID']

        nickname = stu_name.split(" ")
        nickname = nickname[-1]
        self.ids.tool.title = f"Hi, {nickname}"

        a = []
        for item in self.info_dict.items():
            a.append(item)
        for item in self.extra_info.items():
            r, t = item
            if "SL NO" in r:
                pass
            else:
                a.append(item)

        table = MDDataTable(
            pos_hint={'center_x': 0.5, 'center_y': 0.5},
            size_hint=(1, 0.7),
            rows_num=21,
            column_data=[
                ("Name", dp(30)),
                ("Details", dp(30))
            ],
            row_data=a
        )

        self.ids.spinner.active = False
        self.add_widget(table)

    def change_screen(self, name):
        if name == 'profile':
            sm.switch_to(ProfileScreen(name='profile'))
        else:
            sm.switch_to(LoginScreen(name='login'))


class ProfileScreen(Screen):

    def on_pre_enter(self, *args):
        self.ids.welcome.text = f"Welcome {stu_name}"

        s = SESSION
        url = Profile_URL + stu_id

        try:
            r = s.get(url)
            self.text = r.text
        except:
            self.text = "<div>Net Problem > Profile > </div>"

    def on_enter(self, *args):

        soup = Soup(self.text, 'html.parser')

        tables = soup.find_all("table")

        stu_data = tables[1]
        img_data = tables[2]
        academic_data = tables[3]

        img_url = img_data.find("img").get("src")

        response = requests.get(img_url, stream=True)

        img_file = f"img_{str(stu_id)}.png"

        with open(img_file, 'wb') as out_file:
            shutil.copyfileobj(response.raw, out_file)

        self.ids.bg_image.source = img_file

    def change_screen(self):
        sm.switch_to(InfoScreen(name='info'))


class SuLoginApp(MDApp):
    def build(self):
        global sm

        sm.add_widget(LoginScreen(name='login'))
        sm.add_widget(InfoScreen(name='info'))
        sm.add_widget(ProfileScreen(name='profile'))

        return sm


SuLoginApp().run()

sulogin.kv code:



<LoginScreen>:
    MDSpinner:
        id:spinner1
        size_hint: None, None
        size: dp(30), dp(30)
        pos_hint: {'center_x': .5, 'center_y': .5}
        active: False
    MDLabel:
        id:msg
        text: "Student Login"
        halign: "center"
        pos_hint:{'center_x':0.5,'center_y':0.8}
        font_style:'H2'
    MDTextField:
        id:uname
        text: "CSE2201025001"
        hint_text: "Username"
        helper_text_mode: "on_focus"
        pos_hint:{'center_x':0.5,'center_y':0.5}
        size_hint:None,0.1
        width:root.width*0.8
        mode: "rectangle"
        helper_text_mode: "on_error"
        required: True
    MDTextField:
        id:passw
        text:"123456"
        hint_text: "Password"
        helper_text_mode: "on_focus"
        pos_hint:{'center_x':0.5,'center_y':0.38}
        size_hint:None,0.1
        width:root.width*0.8
        mode: "rectangle"
        helper_text_mode: "on_error"
        required: True
    
    MDRectangleFlatButton:
        text: "Login"
        pos_hint:{'center_x':0.5,'center_y':0.2}
        size_hint:None,0.1
        width:root.width*0.5
        on_press: root.user_login()


        
<InfoScreen>:
    BoxLayout:
        orientation: 'vertical'

        MDToolbar:
            id:tool
            elevation: 10
            right_action_items: [['account-circle', lambda x:root.change_screen('profile') ]]
            left_action_items: [['logout', lambda x:root.change_screen('login') ]]
        Widget:
        MDSpinner:
            id:spinner
            size_hint: None, None
            size: dp(30), dp(30)
            pos_hint: {'center_x': .5, 'center_y': .5}
            active: True

<ProfileScreen>:
    
    MDCard:
        elevation: 10
        radius: [36, ]

        FitImage:
            id: bg_image
            size_hint_y: .35
            pos_hint: {"top": 1}
            radius: 36, 36, 0, 0
    MDLabel:
        id:welcome
        halign:'center'
        font_style:'H6'
    
    MDRectangleFlatButton:
        text:"Back"
        on_press:root.change_screen()
        pos_hint: {'center_x': .5, 'center_y': .2}


Solution 1:[1]

In order to keep your UI active while fetching/sending data you may use thread.

First create a new method like init_login in .py for creating a new thread,

    def init_login(self):
        """Function to start a new thread each time."""
        self.new_thread = threading.Thread(target = self.user_login) # Now call that function from this a new thread.
        self.new_thread.start() # You can use this thread instance later if you want.

    ...
    def user_login(self, *args):
        self.ids.spinner1.active = True
        
        
        uname = self.ids.uname.text
        passw = self.ids.passw.text
    ...

Then call that method init_login instead of calling directly user_login in .kv as,

    ...
    MDRectangleFlatButton:
        text: "Login"
        pos_hint:{'center_x':0.5,'center_y':0.2}
        size_hint:None,0.1
        width:root.width*0.5
        on_press: root.init_login()
    ...

I did it this way just to keep the method user_login as it is (almost), you may try the other ways.

Solution 2:[2]

You shouldn't do the UI-related task in the main thread because the openGL operations should be done in the main thread. Otherwise, use @mainthread decorator.

Check this document - https://github.com/kivy/kivy/wiki/Working-with-Python-threads-inside-a-Kivy-application

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ApuCoder
Solution 2 rpi_guru