KIVY FOR DESPERATE MOBILE CODERS
Marco Federighi, Coder @ Nephila
About me:
- Backend Developer
- m.federighi@nephila.it
Kivy
Cross-platform Python Framework for NUI Development
http://kivy.org/docs/gettingstarted/installation.html
Yes, but why Kivy?
- Python
- Cross-Platform
- Mit License
- OpenGL ES 2
- Multi-touch support
Kivy app life cycle
Yet another Hello world App
import kivy
kivy.require('1.7.2')
from kivy.app import App
from kivy.uix.label import Label
class DoAlmostNothingApp(App):
def build(self):
self.title = 'Hello Django Beer!'
return Label(text='Stupid label')
if __name__ == '__main__':
DoAlmostNothingApp().run()
$ python main.py
Run your application (Desktop)
So, to make your App:
- Subclass from the App base class
- Override its build method and return a Widget instance (the root of the widget tree)
- Make an instance of your new class and call its method run
The Kv language
- Separation between the logic of your application and its User Interface
- A set of rules (similar to the CSS rules): root rule, class rule, dynamic classes rule, template rules (deprecated)
- Keywords: self, root, app, args
- Bind widget properties to each other or to callbacks in a natural manner
The Kv language
# Syntax of a rule definition. Note that several Rules can share the same
# definition (as in CSS). Note the braces: they are part of the definition.
<Rule1,Rule2>:
# .. definitions ..
<Rule3>:
# .. definitions ..
# Syntax for creating a root widget
RootClassName:
# .. definitions ..
# Syntax for creating a dynamic class
<NewWidget@BaseClass>:
# .. definitions ..
# Syntax for create a template
[TemplateName@BaseClass1,BaseClass2]:
# .. definitions ..
Python
Kv lang
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
MyRootWidget:
BoxLayout:
Button:
Button:
It's the same thing!
Where to place your Kv code:
- AppName (class) -> appname.kv
- Builder.load_file('path/to/file.kv')
- Builder.load_string(kv_string)
Python
Kivy
class PythonClass(object):
def __init__(self, num=1.0):
super(PythonClass, self).__init__()
self.num = num
class KivyClass(EventDispatcher):
num = NumericProperty(1.0)
It's not the same thing!
- It needs explicit logic to make operation and check (and it's ok!)
- "Just" attributes!
- Check and validation on values
- Observer Pattern
- Optimize memory management
Properties
Events
Kivy handles:
- Clock events: schedule_interval, schedule_once
- Input events: on_touch_down, on_touch_up, on_touch_move
- Class events: EventDispatcher + Properties
Mobile development
Ingredients: Kivy + Plyer + Buildozer
- Python + Kivy: the source code
- Plyer: Wrapper for platform-dependent APIs (https://github.com/kivy/plyer)
- Buildozer: Tool to create package your application for mobile devices (https://github.com/kivy/buildozer)
Buildozer
It does all the work for you for build your app
buildozer init
To create a buildozer config in your project folder:
buildozer -v android debug
To make the build for Android:
Hint: Edit properly the buildozer.spec
The .apk will be put in /bin/ folder created by buildozer
Mobile development
Simple messageboard app
- Server: Django + Django Rest Framework
- Client: Python + Kivy + Plyer
- Build: Buildozer
Recipe
Hint: check your libs version and dependencies as descrived in docs
The .kv file
<MenuScreen>:
BoxLayout:
orientation: 'vertical'
Button:
id: 'login_btn'
text: 'Login'
on_press: root.goto()
Button:
text: 'Settings'
on_press: root.manager.current = 'settings'
<SettingsScreen>:
BoxLayout:
orientation: 'vertical'
Label:
text: 'Host:'
TextInput:
id: host
multiline: False
Label:
text: 'Port:'
TextInput:
id: port
multiline: False
Button:
text: 'Back to menu'
size_hint_y: None
height: '48dp'
on_press: root.save_config(host.text, port.text); root.manager.current = 'menu'
<CameraScreen>:
BoxLayout:
orientation: 'vertical'
Button:
text: 'Snapshot'
on_press: root.make_photo()
size_hint_y: None
height: '48dp'
Button:
id: back_button
text: 'Back'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'messages'
<MessageScreen>:
BoxLayout:
orientation: 'vertical'
Button:
id: btn_send
text: 'Send'
size_hint_y: None
height: '48dp'
on_press: root.send_message(message_text.text); message_text.text=""
Button:
id: btn_makephoto
text: 'Photo'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'photo'
TextInput:
id: message_text
multiline: True
Button:
text: 'Back to menu'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'menu'
<LoginScreen>:
orientation: 'vertical'
GridLayout:
rows: 1
cols: 1
GridLayout:
rows: 3
cols: 2
Label:
text: 'Username:'
TextInput:
id: username
multiline: False
Label:
text: 'Password:'
TextInput:
id: password
password: True
multiline: False
Button:
id: send_button
text: 'Send'
size_hint_y: None
height: '48dp'
on_press: root.login(username.text, password.text)
Button:
id: back_button
text: 'Back to the menu'
size_hint_y: None
height: '48dp'
on_press: root.manager.current = 'menu'
The .py - 1
....
from plyer import camera
import base64
import json
import urllib
import os
__version__ = '1.0.0'
Builder.load_file('./client.kv')
default_config = {'host': 'localhost', 'port': 8000}
...
The .py - 2
....
class MenuScreen(Screen):
def goto(self):
if not os.path.exists(os.path.join(App.get_running_app().user_data_dir, "usr_auth")):
if not os.path.exists(os.path.join(App.get_running_app().user_data_dir, "client_config")):
with open(os.path.join(App.get_running_app().user_data_dir, "client_config"), 'wb') as f:
json.dump(default_config, f)
sm.current = 'login'
else:
sm.current = 'messages'
class SettingsScreen(Screen):
def save_config(self, host, port):
if host and port:
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'wb') as f:
config = {'host': host, 'port': port}
json.dump(config, f)
...
The .py - 3
....
class CameraScreen(Screen):
def make_photo(self):
try:
self.add_widget(CameraLayout())
except:
cam = Camera(resolution=(640, 480), play=True)
if cam.texture:
cam.texture.save()
sm.current = 'messages'
class CameraLayout(FloatLayout):
def __init__(self, **kwargs):
super(CameraLayout, self).__init__(**kwargs)
self.lblCam = Label(text="Click to take a picture!")
self.add_widget(self.lblCam)
def on_touch_down(self, e):
try:
camera.take_picture('/storage/sdcard0/snapshot.jpg', self.done)
except:
pass
def done(self, e):
sm.current = 'messages'
...
The .py - 4
....
class MessageScreen(Screen):
def send_message(self, text):
if text.strip():
self.pb = None
def loading(request, current_size, total_size):
self.pb.value += current_size
if self.pb.value >= total_size:
self.popup.dismiss()
if os.path.exists('/storage/sdcard0/snapshot.jpg'):
os.remove('/storage/sdcard0/snapshot.jpg')
with open(os.path.join(App.get_running_app().user_data_dir, 'usr_auth'), 'rb') as f:
auth_data = json.load(f)
headers = {
'Content-type': 'application/x-www-form-urlencoded',
'Authorization': 'Token {0}'.format(auth_data['token'])
}
data_to_send = {'message': text}
if os.path.exists('/storage/sdcard0/snapshot.jpg'):
data_to_send['photo'] = base64.b64encode(open('/storage/sdcard0/snapshot.jpg', 'rb').read())
params = urllib.urlencode(data_to_send)
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'rb') as ff:
config = json.load(ff)
req = UrlRequest('http://{0}:{1}/messages/'.format(config['host'], config['port']), req_body=params,
req_headers=headers, timeout=10, on_progress=loading)
self.pb = ProgressBar() #100
self.popup = Popup(title='Sending message', content=self.pb, size_hint=(0.7, 0.3))
self.popup.open()
...
The .py - 5
....
class LoginScreen(Screen):
def login(self, user, pwd):
if user.strip() and pwd.strip():
def loading(request, current_size, total_size):
self.pb.value += current_size
if self.pb.value >= total_size:
self.popup.dismiss()
def save_auth(req, result):
if 'token' in result:
with open(os.path.join(App.get_running_app().user_data_dir, 'usr_auth'), 'wb') as f:
json.dump(result, f)
sm.current = 'messages'
params = json.dumps({'username': user, 'password': pwd})
headers = {'Content-type': 'application/json',
'Accept': 'application/json'}
with open(os.path.join(App.get_running_app().user_data_dir, 'client_config'), 'rb') as ff:
config = json.load(ff)
req = UrlRequest('http://{0}:{1}/api-token-auth/'.format(config['host'], config['port']), req_body=params,
req_headers=headers, timeout=10, on_success=save_auth, on_progress=loading)
self.pb = ProgressBar()
self.popup = Popup(title='Get token from the server...', content=self.pb, size_hint=(0.7, 0.3))
self.popup.open()
...
The .py - 6
....
sm = ScreenManager(transition=SlideTransition())
sm.add_widget(MenuScreen(name='menu'))
sm.add_widget(SettingsScreen(name='settings'))
sm.add_widget(MessageScreen(name='messages'))
sm.add_widget(CameraScreen(name='photo'))
sm.add_widget(LoginScreen(name='login'))
class ClientApp(App):
def build(self):
self.title = 'Message Board'
return sm
def on_pause(self):
return True
def on_resume(self):
pass
if __name__ == '__main__':
ClientApp().run()
...
Sources:
- Server: https://github.com/DjangoBeer/message-board
- Client: https://github.com/DjangoBeer/message-board-client (The Kivy app for Android)
Extras:
Me and my colleagues realized a simple demo of a 2d horizontal shooter, it has ugly graphic and no sound, but it's made in Kivy and it runs on Android.
Source: https://github.com/nephilahacks/spider-eats-the-kiwi
Thank you!
https://github.com/fmarco
deck
By Marco Federighi
deck
- 1,962