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