Kivyとbuildozerでクロスプラットフォームアプリをつくってみた。

この記事は SLP KBIT Advent Calendar 2019 12日目の記事です。

adventar.org

WindowsLinuxAndroidなどで動く簡単なアプリを作りました。ボタンを押すと、入力した文字が反映される程度のものです。

お品書き

Kivy、buildozerとは

Kivy とは、PythonクロスプラットフォームGUIアプリケーションを作ることができるオープンソースライブラリ。画面の大きさが変わってもレイアウトが崩れない所がすごい!
buildozer とは、Kivyで作ったアプリをAndroidなどで動くアプリに変換するツール。
Windowsで作ったアプリをLinux、さらにはAndroidでも動かすことができるなんて素晴らしいですね。
[注]: MaciOSについては今回取り上げませんが、同様にできるかと思います。

実行環境

Kivyでの開発はWindowsでもLinuxでもできますが、buildozerはLinuxでしか使えないので、Androidアプリまでやりたい方はLinuxで構築することをおすすめします。

実行環境は以下の通り。

Windows10
  └ Ubuntu (VM)
    └ Python3.6.9
      (└ venv)
        ├ Kivy 1.11.1
        └ buildozer 0.40.dev0

buildozerは仮想環境ではうまく動かないので、その時だけは仮想なしで実行します。しかし、Kivyの依存関係がややこしいので、buildozerを使う方も使わない方も、まずは仮想環境上で構築しましょう。

Kivyを使う

環境構築

Windows

公式ページに沿って行いました。
Windowsはそれで動かせると思います。

kivyについて調べていると、pygameとcythonを入れている場合が見られたのですがなぜでしょう…依存関係がちょうどいいんですかね。それはともかく、公式にのっとってインストールでOK。

Linux

Linuxは依存関係を揃えるため先に依存関係云々について色々入れます。
VMで新しくマシンを動かしたのですが、最終的にストレージが25GB近くまで膨れ上がったので、buildozerを使うならデフォルトの20GBから増やしておきましょう。

まずは翻訳記事を参考に以下のようにインストール。

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install -y \
    build-essential \
    git \
    python3 \
    python3-dev \
    python3-venv \
    ffmpeg \
    libsdl2-dev \
    libsdl2-image-dev \
    libsdl2-mixer-dev \
    libsdl2-ttf-dev \
    libportmidi-dev \
    libswscale-dev \
    libavformat-dev \
    libavcodec-dev \
    zlib1g-dev

このpython3python3-devなどについては、python3.7-venvのようにマイナーバージョンまで指定ができます。ただし、python3.8以上とpython3.4(もしくは3.5)以下にも未対応だったかと思います。
python-pipについては、venv上でpipが使えるようになるのと、pipをupgradeするとエラーが起きる問題があるので入れません。

最後に

$ pip install cython kivy

これでKivyのインストールは終了です。

実行

今回作ったのはこちら。
ボタン・画面遷移・入力・アクティブバーからなります。 AppScreenShot01 AppScreenShot02

main.py

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty, ListProperty, ObjectProperty
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.lang import Builder
from kivy.config import Config
Config.set('graphics', 'multisamples', '0')
# Config.set('graphics', 'width', '640')
# Config.set('graphics', 'height', '480')


class RootWidget(ScreenManager):
    pass
        
class Testwidget(Screen):
    text  = StringProperty()
    label1 = ObjectProperty()

    def __init__(self, **kwargs):
        super(Testwidget, self).__init__(**kwargs)
        self.text = 'start'

    def buttonClicked(self):
        self.text = 'U'

    def buttonClicked2(self):
        self.text = 'got'

    def buttonClicked3(self):
        pass
    
class SecSc(Screen):
    l0 = ObjectProperty(None)
    t0 = ObjectProperty(None)

    def txtmodify(self):
        self.ids.l0.text = self.ids.t0.text

kv = Builder.load_file("test.kv")

class TestApp(App):
    def __init__(self, **kwargs):
        super(TestApp, self).__init__(**kwargs)
        self.title = "Testing..."

    def build(self):
        return kv

if __name__ == "__main__":
    TestApp().run()


test.kv

#:kivy 1.11.1
#: import FadeTransition kivy.uix.screenmanager.FadeTransition

RootWidget:
    transition: FadeTransition()
    Testwidget:
    SecSc:


<Testwidget@BoxLayout>:
    name: "tstw"
    label1: label1
    BoxLayout:
        orientation: 'vertical'
        size: root.size

        ActionBar:
            ActionView:
                ActionPrevious:
                    title: 'kivyapp'
                    with_previous: False

                ActionButton:
                    text: 'I am just a Button.'

        Label:
            id: label1
            size_hint_y: 1
            font_size: 35
            text: root.text
        
        Button:
            id: button1
            font_size: 35
            text: "01"
            on_press: root.buttonClicked()

        Button:
            id: button2
            font_size: 35
            text: "02"
            on_press: root.buttonClicked2()
            
        Button:
            id: button3
            font_size: 35
            text: "03"
            on_press: 
                root.text = 'that'
                root.manager.get_screen("inputscr").l0.text = root.label1.text
                app.root.current = "inputscr"
    
<SecSc>:
    name: "inputscr"
    l0: l0
    t0: t0
    BoxLayout:
        orientation: 'vertical'
        size: root.size
        ActionBar:
            size_hint_y: 0.1
            ActionView:
                ActionPrevious:
                    title: 'return'
                    with_previous: True
                    on_release: app.root.current = "tstw"

                ActionButton:
                    text: 'No response. Looks dead.'
        
                
        BoxLayout:
            TextInput:
                id: t0
                multiline: True
                text: "I like you"
            
            Label:
                id: l0
                text: ''

            Button:
                id: b0
                text: "modify!"
                on_release: root.txtmodify()

main.py と kvファイルを同じディレクトリに入れて

python main.py

で起動できます。

解説

Linuxではこのように

from kivy.config import Config
Config.set('graphics', 'multisamples', '0')

としなければエラーでVM自体が落ちました。
WindowsはなくてもOKです。コメントアウトしているようにサイズ指定しても問題なく起動できます。


これで土台を作ります。

class RootWidget(ScreenManager):
    pass


このようにスクリーンを追加します。こうすると画面遷移が簡単にできます。

class Testwidget(Screen):
    # 省略
class SecSc(Screen):
    # 省略


この部分ですが、def build(self)の中で1つだけreturnしなければなりません。

kv = Builder.load_file("test.kv")

class TestApp(App):
    def __init__(self, **kwargs):
        super(TestApp, self).__init__(**kwargs)
        self.title = "Testing..."

    def build(self):
        return kv

返す内容はreturn Testwidget()のように[class名]()でもいいですが、画面遷移を使うとどうしてもクラスが2つ以上になってしまいます。そこで、Builder.load_file("test.kv")のように読み込んだkvファイルを返すとうまくいきました。


test.kvについて
main.pyでのクラス名をもとに書いていきます。
土台となるRootWidgetの下に<Testwidget>などを配置しています。

RootWidget:
    transition: FadeTransition()
    Testwidget:
    SecSc:

<Testwidget@BoxLayout>:
    name: "tstw"
    label1: label1
    BoxLayout:
        orientation: 'vertical'

BoxLayoutは、画面をウィジェットの数だけ分割します。
デフォルトでは横で、縦分割にするにはorientation: 'vertical'と書きます。

このmain.pyですが、ObjectPropertyによってkvファイルでもl0t0を使えるようにしています。

class SecSc(Screen):
    l0 = ObjectProperty(None)
    t0 = ObjectProperty(None)

    def txtmodify(self):
        self.ids.l0.text = self.ids.t0.text
        # python側では self.ids.[id] で指定idの操作ができる

合わせてkvファイルにはこのように、id: idと書くことでうまくいきました。

<SecSc>:
    name: "inputscr"
    l0: l0
    t0: t0

その他諸々kv言語については、以下のような詳しい記事を見てくださればわかりやすいと思います。

qiita.com


buildozerを使う

作ったアプリをAndroidでも動かしてみます。
主な流れはこの記事を参考にさせていただきました。ありがとうございます。

invasivecat.hatenablog.com

環境構築

VM上のUbuntuにて。
以下の記事のように、pythonのリンクを標準のpython2からpython3へ張り直します。

https://toc.tocu.co.jp/blog/tips/item/2129

$ cd /usr/bin/
$ cp python python-bk
    # バックアップをとる
$ unlink ./python
$ ln -s /usr/bin/python3.X ./python
    # 任意のバージョンにリンクを貼り直す
$ python -V
    # 確認してOKならバックアップを消す

次に、aptでpipを入れてpip install -U pipするとエラーが出るので

$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
$ sudo python get-pip.py

で大丈夫かと思います。 次に、必要なライブラリを入れます。

$ sudo pip install -U pip
$ sudo pip install -U setuptools
$ sudo pip install cython kivy
$ sudo dpkg --add-architecture i386
$ sudo apt update
$ sudo apt install -y git zip unzip openjdk-8-jdk autoconf libtool pkg-config libncurses5-dev libncursesw5-dev libtinfo5 cmake ccache python2.7 python2.7-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 ant automake libltdl-dev aidl lld libffi-dev

buildozer(開発版)のインストール。

$ sudo pip install git+http://github.com/kivy/buildozer

CrystaX NDKのインストール。
https://www.crystax.net/en/downloadここからcrystax-ndk-10.3.2-linux-x86_64.tar.xzをDL、解凍します。

Javaの設定を変更してあげます。

$ sudo update-alternatives --config java

で、Java 8 を選択。
また、以下でJAVA_HOMEとPATHを設定します。

$ vi ~/bashrc
# 以下を追記する
export PATH=$PATH:~/.local/bin/
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

以上で事前準備は終了です。

実行

まずは設定ファイルを書き換えましょう。
main.pyのあるディレクトリが作業ディレクトリになります。

$ cd /path/to/directory/
$ buildozer init

これで、buildozer.specが生成されますので、編集します。

  • package.name は 作業ディレクトリ名。
  • package.domain は デフォルト以外に変更したほうがよさそう。
  • requirements は python3, kivy, 他importしてるやつ。pythonのマイナーバージョンは指定できないのでそのままにしておきます。
  • android.sdk = 24にします。
  • android.ndk_path = /path/to/crystax-ndk-10.3.2/のようにCrystaxの解凍先を指定しておきます。
  • android.arch は 使うandroidアーキテクチャを指定します。Xperia XZなら、Snapdragon 820 MSM8996 なので、armeabi-v7aだと確認できます。

さて、設定ファイルを編集したら、コピーして別の場所に保存しておきましょう。作成が失敗したときに書き直すのは面倒なので。

いよいよ実行です。
USBデバッグをオンにしたandroidをPCに接続します。
接続が確認できたら、次のコマンドを打ちapkファイルを作成・実行しましょう!

$ buildozer android debug deploy run

私の環境(Core i5 第5世代・70Mbps前後)では15分ぐらいかかりました。コーヒーでも飲んで待ちましょー。

作成中は、ずらーっとログが出てくるのですが、途中で

[WARNING]: Doing some hacky stuff to link properly

と出たり、3つぐらいエラーが出ますが、処理が止まらない限りは大丈夫です。
うまく生成できなかったら、ログを読んで適宜修正します。

感想

Kivyは奥深いですね。まだ全然理解できていないので精進しようと思います。
C#Java、Swiftなどを学ばなくても、pythonのみで複数OS上で動くアプリを作れる手軽さは本当にすごい。
ただ、日本語の情報は多いとは言えないので、エラーにぶつかったときは英語のwebページから参考にしないといけないかもしれません。翻訳サイトにはくそお世話になりました。


参考サイト