KivyMD RecycleView has low FPS & lags - kivy

I am using Python 3.8.12, Kivy 2.0.0 & KivyMD-0.104.2
I am running my app on PyCharm on Ubuntu 18.04 LTS on a ASUS ROG STRIX GL503VD.
My issue is that every time i use a for loop to generate content in KivyMD BottomNavgation using RecycleView, no matter what content (might be even a for i in range(100)), the app is starting to move very slow (when manually resizing it takes around 10 seconds to resize correctly). Also i have FPS under 50 on the tab in which i generated the data while in the empty ones i get #80 FPS.
The issue doesn't seem to happen when i run lets say for i in range(100) in Kivy 2.0.0, but i would prefer to stick to KivyMD.
I've been looking in the last week on the internet for solutions but no luck and is a bit hard to understand kivy documentation regarding RecycleView.
Below is my code, please correct me if i did something out of "good practice". I just started to learn Kivy and i'm a beginner in Python (#1 year).
Thank you very much.
EDIT: Also when deployed to Android it works below 30 FPS on a Xiaomi Redmi Note8 Pro.
main.py
from kivy.lang import Builder
from kivymd.app import MDApp as md
from kivymd.uix.list import TwoLineListItem
import json as js
#from kivy.utils import platform
from kivy.core.window import Window
'''
if platform == "android":
try:
from android.permissions import request_permissions, Permission
request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE])
except:
pass
'''
class MainApp(md):
def build(self):
Window.size = (491, 1064)
self.theme_cls.primary_palette = 'Red'
self.theme_cls.primary_hue = '50'
layouts = Builder.load_file("layouts.kv")
return layouts
def on_start(self):
for key, value in self.read_list().items():
self.root.ids.container1.add_widget(TwoLineListItem(text=f"{key}", secondary_text="Tests : {}".format(len(value))))
self.fps_monitor_start()
def read_list(self):
with open('tests.json', 'r') as read_test:
#82 lines in test_list dict
test_list = js.load(read_test)
return test_list
if __name__ == '__main__':
MainApp().run()
layouts.kv
<MainLayout>
#:import get_color_from_hex kivy.utils.get_color_from_hex
MDGridLayout:
cols: 3
size_hint: 1, 1
MDBottomNavigation:
panel_color: get_color_from_hex("#008080")
text_color_active: get_color_from_hex("#FFFFFF")
MDBottomNavigationItem:
name: 'Item1'
text: 'item_1'
icon: 'language-python'
MDGridLayout:
cols: 1
RecycleView:
MDList:
id: container1
MDBottomNavigationItem:
name: 'Item2'
text: 'item_2'
icon: 'language-cpp'
MDBoxLayout:
RecycleView:
MDList:
id: container2
MDBottomNavigationItem:
name: 'Item3'
text: 'item_3'
icon: 'language-javascript'
MDBoxLayout:
RecycleView:
MDList:
id: container3

You are not actually using the RecycleView. To use a RecycleView, you must have a RecycleLayout as its child, a viewclass, and a data list that indicates the items in the RecycleLayout.
Here is one way to use the RecycleView in your App. First, modify the kv to indicate the RecycleLayout and the viewclass:
MDGridLayout:
cols: 3
size_hint: 1, 1
MDBottomNavigation:
panel_color: get_color_from_hex("#008080")
text_color_active: get_color_from_hex("#FFFFFF")
MDBottomNavigationItem:
name: 'Item1'
text: 'item_1'
icon: 'language-python'
MDGridLayout:
cols: 1
RecycleView:
id: container1
viewclass: 'TwoLineListItem'
RecycleBoxLayout:
default_size: None, dp(75)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
The above moves the container1 id to the RecycleView and adds the needed properties to allow the RecycleView to operate.
Then the on_start() method can be modified to create the data list for the RecycleView:
def on_start(self):
data = []
for key, value in self.read_list().items():
data.append({'text': f"{key}", 'secondary_text': "Tests : {}".format(len(value))})
self.root.ids.container1.data = data
self.fps_monitor_start()

Related

I need to add more than one app.root.current in kivy

I am creating an app in kivy in which basically it gives you a poem once you enter a date and password. For this I have created several windows for each poem. My problem is that i made it so that on_release of the button there is a selective-like structure to show the right poem. The problem is that this only works for the last sentence and not all of them at the same time as supposed to.
Button:
font_name: "Georgiai"
font_size: 20
text: "Enter"
size_hint: 0.5, 0.5
background_color: "#00FFCE"
on_release:
app.root.current = "fifth" if date.text == "01.02.2023" and password.text == "Si" else "fourth"
app.root.current = "seis" if date.text == "02.02.2023" and password.text == "te" else "fourth"
I have tried using multiple on_release: app.root.current but it also does not work. This is the only thing left for it to function so I hope someone can help me. I am quite new in programming
You should move the program logic into Python and not include logic in your kivy file. Arguments can be passed like this:
Button:
on_release: app.give_poem(_date.text, _password.text, self)
# or
# on_release: root.give_poem(_date.text, _password.text, self)
I changed your variables by adding an underscore to avoid confusing your variables with other common objects. I am assuming date and password were ids of other kivy objects.
in your python app or top level widget you can employ your logic. Here is the basic signature that your method should have within your app or your top level widget object. This one just prints the arguments sent and you can decide what to do from there.
def give_poem(self, _date, _password, my_button):
print(f"{self}: {_date}: {_password}: {my_button}")

Why app code does not recognize design code?

I'm fairly new into programming so this may be lousy question.
I'm watching tutorials of kivy programming (https://youtu.be/k4QCoS-hj-s?list=PLCC34OHNcOtpz7PJQ7Tv7hqFBP_xDDjqg) and I'm stuck at design language code.
Basically, I'm trying to write a design language code (my.kv) for an app enter code here(design.py) which should be connected with this lines in design.py, because of "My" in class:
class MyApp(App):
def build(self):
return MyGridLayout()
Also, line in design.py:
class MyGridLayout(Widget):
is connected to first line in my.kv:
<MyGridLayout>
However, it seems design.py does not recognize it, even though they are saved in the same folder.
I was thinking problem may be in different operating systems (Linux-Windows) because of types of brackets, but I don't know what is the Windows parallel to Linux angle brackets.
Any thoughts?
EDIT
I tried what #NameKhan72 proposed:
from kivy.lang import Builder
class MyApp(App):
def build(self):
kv = Builder.load_file('my.kv')
return MyGridLayout()
but still getting error - "no such file or directory: "my.kv"".
EDIT2
I found the mistake. Of course, it was connected to my experience in programming. I didn't put ":" at the end of "". Now there is no error but after opening app I get a black screen.
I tried everything I found on the web but nothing so far.
This is code:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
class MyGridLayout(Widget):
name = ObjectProperty(None)
pizza = ObjectProperty(None)
color = ObjectProperty(None)
def press(self):
name = self.name.text
pizza = self.pizza.text
color = self.color.text
print("Hello " + name + ", you like " + pizza + "pizza, and your favorite color is " + color + ".")
self.name.text =""
self.pizza.text =""
self.color.text =""
class MyApp(App):
def build(self):
return MyGridLayout()
if __name__ == "__main__":
MyApp().run()
This is design code in my.kv:
<MyGridLayout>:
name:name
pizza:pizza
color:color
GridLayout:
cols:1
size: root.width, root.height
GridLayout:
cols:2
Label:
text: "Name"
TextInput:
id: name
multiline:False
Label:
text: "Favorite pizza"
TextInput:
id: pizza
multiline:False
Label:
text: "Favorite color"
TextInput:
id: color
multiline:False
Button:
text: "Submit"
font_size: 32
on_press: root.press()
Is it typo or what? I rewrited the whole code 2x to be sure but still getting black screen.
You need to load the kv-file into python:
from kivy.lang import Builder
class MyApp(App):
def build(self):
kv = Builder.load_file('my.kv')
return MyGridLayout()
The answer my friend is blowing in the wind.
Nope, my problem was saving in wrong file extension. While saving, I writed my.kv but my PC saved it as my.kv.py. That was the reason why code didn't recognize design code.
Thanks for help, surely it'll help me in the future :)
you should use Builder.load_file("filename.kv") or `Builder.load_string(""
""")but to import it usefrom kivy.lang import Builder`

kivy: have tcp event open popup

I'm new to kivy, so apologies if I've missed something really basic here but I am struggling to understand both the behavior I'm getting as well as my inability to get the outcome I'm after.
The goal
The kivy app should be listening for TCP events from an external server, with certain triggers causing various stuffs in the kivy app. In this case, I want the external server to be able to open a popup in the app.
Illustrative Code
I have adopted the following (working) demo code from another SO answer that I can no longer seem to find, so apologies for the plagiarism but it is not intentional.
server.py
import socket
serversocket = socket.socket()
host = 'localhost'
port = 54545
serversocket.bind(('', port))
serversocket.listen(1)
clientsocket,addr = serversocket.accept()
print("got a connection from %s" % str(addr))
while True:
msg = input("> ")
clientsocket.send(msg.encode('utf-8'))
client.py
import socket
class MySocket:
def __init__(self,host="localhost",port=54545):
self.sock = socket.socket()
self.sock.connect((host, port))
def get_data(self):
return self.sock.recv(1024)
main.py
import kivy
from kivy.app import App
from kivy.event import EventDispatcher
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.popup import Popup
from threading import Thread
from client import *
kivy.require('1.9.1')
class MyEventDispatcher(EventDispatcher):
def __init__(self, **kwargs):
self.register_event_type('on_test')
super(MyEventDispatcher, self).__init__(**kwargs)
def do_something(self, value):
self.dispatch('on_test', value)
def on_test(self, *args):
print('I am dispatched', args)
CustomPopup().open()
class CustomPopup(Popup):
pass
class MainScreen(Label):
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
self.sock = MySocket()
Thread(target=self.get_data).start()
def get_data(self):
while True:
self.text = self.sock.get_data().decode('utf-8').strip()
if self.text == 'click':
MyEventDispatcher().do_something(self.text)
class MyApp(App):
def build(self):
return MainScreen()
if __name__ == "__main__":
MyApp().run()
my.kv
<MainScreen>:
Button:
text: 'I do nothing'
<CustomPopup>:
title: 'Popup window'
size_hint: .5, .5
auto_dismiss: False
GridLayout:
cols: 1
Label:
size_hint: .9, .9
halign: 'center'
valign: 'middle'
text: 'message text goes here'
text_size: self.width, None
Button:
text: 'A: Close'
on_release: root.dismiss()
Button:
text: 'B: Close'
on_release: root.dismiss()
This all works in that I can send the trigger word click from the server and the popup will open, it has multiple placeholder buttons that work. Hooray.
The problem, however, is that I seem to require the dummy button on the main app screen, which I do not want. If I simply eliminate the <MainScreen> bit from the .kv file, sending the click keyword leads to:
I am dispatched ('click',)
Segmentation fault (core dumped)
but I can send any other string and it will display on screen as expected.
So, the basic questions are:
How do I get this to work as desired and
Why does placing a non-functional button on the screen make the popup work?
Thanks!

Updating Button's background_color not reflected on the UI sometimes

Well, I'm creating a Kivy app in which you can select only one of many statuses. Every status has his own Button and the selected status has a different background_color from the others.
The problem is that sometimes (apparently random) after clicking a button, two of them stays with his background changed at the same time. The strange thing is that I'm checking the background_color of those elements and it doesn't match with the result that I'm seeing on the screen.
So, the background_color property has one color but another one is being rendered on the screen.
Relevant kv file section:
<StatusButtonsContainer>:
cols: 2
spacing: 8
padding: 0,16,0,0
<StatusButton>:
selected: False
text: self.status_name
on_release: app.on_change_status_click(self.status_name)
font_size: '16'
background_color: self.back_color if self.selected else (0.259, 0.259, 0.259,1)
background_normal: ''
background_down: ''
background_disabled_normal: ''
This is how I'm creating the Button widgets dinamically:
class StatusButtonsContainer(GridLayout):
def __init__(self, **kwargs):
super(StatusButtonsContainer, self).__init__(**kwargs)
for name, color in config.statuses.items():
button = StatusButton(status_name=name, back_color=color)
self.add_widget(button)
class StatusButton(Button):
status_name = StringProperty()
back_color = ListProperty()
And this the function that is executed when the button is pressed:
class ControlsScreen(Screen):
def change_selected_status(self, status):
for button in self.ids.buttons_container.children:
if button.status_name == status:
button.selected = True
button.disabled = True
print('Status ' + button.status_name + ' was selected.')
print('background_color:' + str(button.background_color))
else:
button.selected = False
button.disabled = status in ['printing', 'preparing', 'paused']
print('Status ' + button.status_name + ' was NOT selected.')
print('background_color:' + str(button.background_color))
It's even more weird that this is happening on a Raspberry Pi 3 with Raspbian, but I'm not able to reproduce it on a Windows machine... I double checked that the [input] section in the config is correct and the buttons are being pressed only once.
Versions
Python: 3.6.0
OS: Raspbian GNU/Linux 9 (stretch)
Kivy: 1.10.1
Finally, the problem was that I was updating the UI in a thread different from the main thread.
In my application, the button status can be changed either from the UI directly or from a web socket message. So, when the button's background_color was changed through web socket, the UI update was called by another thread and, for some reason, that was causing the issue.
I've solved it with the #mainthread decorator:
#mainthread
def change_status_ui(self, status):
self.get_screen('Controls').change_selected_status(status)
self.get_screen('Status').change_status(status)
Now, it doesn't matter which thread call this method, it will be executed on the main thread.

Accessing id/widget of different class from a kivy file (.kv) using Kivy's clock?

I've spent the day working with the code from How to access id/widget of different class from a kivy file (.kv)? I've paired it down to the simplest code I could because what I want to do is use Kivy's Clock function to change the text for me instead of clicking the button.
As best as I can understand the code that changes the text should go on Line 38 of the program but all the code I tried stopped executing because it could not access the text to change it. The clock function is working in the code provided.
I've left the button press active but it's the Clock code that I want to change the text. I was wondering if someone knows the solution.
Thanks in advance.
....brad....
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.properties import ObjectProperty
Builder.load_string("""
<SingleScreen>:
id: single_screen
orientation: 'vertical'
Button:
text: "You Can Change Me By Button Press Here But I Really Want To Be Changed By Kivy's Clock Function"
on_release: root.rooted()
Change_Label:
id: gb
<Change_Label>:
_python_access: ChangeLabel
Label:
id: ChangeLabel
text: "I'm Gonna Change."
""")
class SingleScreen(BoxLayout):
def rooted(self):
self.ids.gb._python_access.text = "I Changed It By Clicking!. But That's Not What I Wanted To Program!"
class Change_Label(BoxLayout):
_python_access = ObjectProperty(None)
class OnlyScreen(App):
def build(self):
Clock.schedule_interval(self.Callback_Clock, 3)
return SingleScreen()
def Callback_Clock(self, dt):
print "Hello, world! I'm the clock working...."
# What code goes here that will change the text via Kivy's clock instead of using the button?
if __name__ == '__main__':
OnlyScreen().run()
Please do the following and refer to my example for details:
Snippet
class OnlyScreenApp(App):
def build(self):
Clock.schedule_interval(self.Callback_Clock, 3)
return SingleScreen()
def Callback_Clock(self, dt):
self.root.ids.gb._python_access.text = "I Changed It By Clock! dt=" + str(dt)
Example
main.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.properties import ObjectProperty
class SingleScreen(BoxLayout):
pass
class Change_Label(BoxLayout):
_python_access = ObjectProperty(None)
class TestApp(App):
def build(self):
Clock.schedule_interval(self.Callback_Clock, 3)
return SingleScreen()
def Callback_Clock(self, dt):
self.root.ids.gb._python_access.text = "I Changed It By Clock! dt=" + str(dt)
if __name__ == '__main__':
TestApp().run()
test.kv
#:kivy 1.10.0
<SingleScreen>:
id: single_screen
orientation: 'vertical'
Change_Label:
id: gb
<Change_Label>:
_python_access: ChangeLabel
Label:
id: ChangeLabel
text: "I'm Gonna Change."
Output
Based on ikolim's answer above I wanted to post the solution as one complete python program that relates to the format from which the question was posted. Really appreciate the solution. The complete working (one file) code would be as follows;
import kivy
kivy.require('1.9.1')
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock
from kivy.properties import ObjectProperty
Builder.load_string("""
<SingleScreen>:
id: single_screen
orientation: 'vertical'
Change_Label:
id: gb
<Change_Label>:
_python_access: ChangeLabel
Label:
id: ChangeLabel
text: "I'm Gonna Change."
""")
class SingleScreen(BoxLayout):
pass
class Change_Label(BoxLayout):
_python_access = ObjectProperty(None)
class OneScreen(App):
def build(self):
Clock.schedule_interval(self.Callback_Clock, 3)
return SingleScreen()
def Callback_Clock(self, dt):
self.root.ids.gb._python_access.text = "I Changed It By Clock! dt=" + str(dt)
if __name__ == '__main__':
OneScreen().run()

Resources