Создание Android-приложения с помощью Python Kivy

Перевод статьи «Build an Android application with Kivy Python framework».

Если вы являетесь Python-разработчиком и подумываете заняться мобильной разработкой, то фреймворк Kivy — это отличный выбор для вас. С помощью Kivy можно создавать платформонезависимые приложения, компилируемые под iOS, Android, Windows, MacOS и Linux.

Содержание

Чтобы понимать, о чем идет речь в этой статье, вы должны быть знакомы с языком Python. Давайте начнем!

Хотите скачать книги по Python в 2 клика? Тогда вам в наш телеграм канал PythonBooks 

Начало работы с Kivy

Во-первых, вам понадобится новая директория для вашего приложения. Убедитесь, что на вашей машине установлен Python, и создайте новый файл Python. Далее в терминале необходимо установить модуль Kivy. Чтобы избежать конфликтов пакетов, давайте сделаем это в виртуальным окружении:

python3 -m venv venv
. ./venv/bin/activate
pip3 install kivy 

После установки Kivy в терминале должно появиться сообщение об успешной установке, которое выглядит так, как показано на скриншотах ниже:

Установка Киви
Установка Киви

В нашей рабочей директории в файле main.py нам нужно импортировать модуль Kivy и указать, какую версию мы хотим получить. Можно использовать Kivy v2.0.0, но если у вас смартфон старше Android v8, то я рекомендую использовать v1.9.0. Во время сборки можно поработать с разными версиями, чтобы увидеть разницу в возможностях и производительности.

Чтобы указать версию, добавьте ее номер сразу после строки import kivy следующим образом:

import kivy

kivy.require('1.9.0')

Создание класса RandomNumber

Теперь создадим класс, который будет определять наше приложение. Я назову его RandomNumber. Этот класс будет наследовать класс App из Kivy. Поэтому необходимо импортировать приложение, добавив from kivy.app import App.

В класс RandomNumber необходимо добавить метод build. Чтобы вернуть пользовательский интерфейс, мы будем использовать функцию build. Пока что я возвращаю его в виде простой метки. Для этого необходимо импортировать Label с помощью строки from kivy.uix.label import Label:

import kivy
from kivy.app import App
from kivy.uix.label import Label

class RandomNumber(App):
  def build(self):
    return Label(text="Random Number Generator")

Теперь скелет нашего приложения готов! Прежде чем двигаться дальше, необходимо инициировать экземпляр класса RandomNumber и запустить его в терминале или IDE, чтобы увидеть интерфейс:

import kivy
from kivy.app import App
from kivy.uix.label import Label

class RandomNumber(App):
  def build(self):
    return Label(text="Random Number Generator")

randomApp = RandomNumber()
randomApp.run()

При запуске экземпляра класса с текстом Random Number Generator должен появиться простой интерфейс или окно, выглядящее как на скриншоте ниже:

Простой интерфейс после выполнения кода
Простой интерфейс после выполнения кода

Вы не сможете запустить текст на Android, пока не закончите сборку.

Аутсорсинг интерфейса

Далее нам потребуется способ аутсорсинга интерфейса. Сначала мы создадим файл Kivy в нашем каталоге, который будет содержать большую часть нашей работы по дизайну.

Примечание по именованию файлов

Вы должны назвать этот файл так же, как и ваш класс, используя строчные буквы и расширение .kv. Kivy автоматически свяжет имя класса и имя файла, но на Android это может не сработать, если они полностью совпадают.

Внутри этого .kv-файла необходимо указать макет приложения, включая такие элементы, как ярлык, кнопки, формы и т.д. Макеты в Kivy бывают разных типов, но выполняют одну и ту же функцию: это контейнеры, используемые для расположения виджетов в соответствии с выбранным макетом.

Подробнее о различных макетах Kivy можно прочитать в руководстве по началу работы.

Применение box layout

Для простоты этого приложения я буду использовать компоновку box. В двух словах, при боксовом расположении виджеты и другие элементы располагаются в одной из двух ориентаций: вертикальной или горизонтальной.

Я добавлю три метки:

  • Для заголовка RandomNumber
  • Для размещения генерируемого случайного числа
  • Кнопка Generate, вызывающая функцию generate

Следует помнить, что эти метки будут располагаться друг над другом.

Мой .kv-файл выглядит так, как показано ниже, но вы можете изменять различные значения в соответствии с вашими требованиями:

<boxLayout>:
    orientation: "vertical"
    Label:
        text: "Random Number"
        font_size: 30
        color: 0, 0.62, 0.96

    Label:
        text: "_"
        font_size: 30

    Button:
        text: "Generate"
        font_size: 15 

Здесь строка 2 задает тип макета, который я использую для своего приложения, а строка 3 — ориентацию, о которой я только что говорил. Остальные строки являются настраиваемыми, поэтому вы можете указать, как должны выглядеть ваши элементы пользовательского интерфейса.

Значения цветов в Kivy

Значения цветов в Kivy не являются обычными значениями RGB — они нормализованы. Для понимания нормализации цвета необходимо знать, что распределение значений цвета обычно зависит от освещенности. Оно зависит от таких факторов, как условия освещения, эффект объектива и другие.

Чтобы избежать этой проблемы, Kivy использует соглашение (1, 1, 1). Это представление Kivy для RGB (255, 255, 255). Для преобразования обычных значений RGB в соглашение Kivy вам нужно разделить все ваши значения на 255. Таким образом вы получите значения в диапазоне от 0 до 1.

Создание остальной части пользовательского интерфейса

В файле main.py оператор импорта Label больше не нужен, поскольку файл Kivy позаботится о вашем пользовательском интерфейсе. Однако необходимо импортировать boxlayout, который будет использоваться в файле Kivy.

В свой основной файл импортируйте BoxLayout и отредактируйте файл main.py так, чтобы в методе build возвращалась функция return BoxLayout():

from kivy.uix.boxlayout import BoxLayout

Если выполнить приведенную выше команду, то должен появиться простой интерфейс, содержащий заголовок, заполнитель _ и кнопку generate, на которую можно нажать:

Генерируется приложение случайных чисел

Обратите внимание, что для работы Kivy-файла не нужно импортировать ничего дополнительного. В принципе, когда вы запускаете приложение, оно возвращает boxlayout, ища внутри Kivy-файла файл с тем же именем, что и ваш класс.

Не забывайте, что это простой интерфейс, а вы можете сделать свое приложение настолько мощным, насколько захотите. За идеями обязательно обратитесь к документации по языку Kv.

Функция для генерации случайных чисел

Теперь, когда наше приложение почти готово, нам понадобится простая функция для генерации случайных чисел, когда пользователь нажимает кнопку generate. Это случайное число будет выводиться в интерфейс приложения. Для этого нам потребуется изменить несколько вещей в наших файлах.

Во-первых, импортируйте модуль random, который будет использоваться для генерации случайных чисел, и создайте функцию или метод, вызывающий сгенерированное число. Для импорта модуля random используйте оператор import random.

В данной демонстрации я буду использовать диапазон от 0 до 2000. Генерировать случайное число очень просто с помощью функции random.randint(0, 2000). Мы добавим его в наш код в ближайшее время.

Далее мы создадим еще один класс, который будет представлять собой нашу собственную версию макета коробки. Наш класс будет наследовать класс box layout, в котором находится метод генерации случайных чисел и их отображения в интерфейсе:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

В этом классе нужно создать метод generate, который будет не только генерировать случайные числа, но и манипулировать меткой, управляющей отображением случайного числа в файле Kivy.

Чтобы разместить этот метод, необходимо сначала внести изменения в файл .kv. Поскольку класс MyRoot унаследовал box layout, можно сделать MyRoot элементом верхнего уровня в файле .kv:

<MyRoot>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

Обратите внимание, что все спецификации пользовательского интерфейса по-прежнему располагаются с отступами в макете Box Layout.

После этого необходимо добавить к метке идентификатор для хранения сгенерированных чисел, чтобы ими можно было легко манипулировать при вызове функции generate. Необходимо указать связь между id в этом файле и другим идентификатором в основном коде вверху, непосредственно перед строкой BoxLayout:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

Строка random_label: random_label в основном означает, что метка с идентификатором random_label будет отображена на random_label в файле main.py, так что любое действие относительно random_label будет отображено на метку с указанным именем.

Теперь можно создать метод генерации случайного числа в файле main.py:

def generate_number(self):
    self.random_label.text = str(random.randint(0, 2000))

Обратите внимание, как метод класса манипулирует атрибутом text метки random_label, присваивая ему новое случайное число, сгенерированное функцией random.randint(0, 2000).

Поскольку генерируемое случайное число является целым, для превращения его в строку необходимо выполнить приведение типов. В противном случае при выполнении метода в терминале будет выдана ошибка типа.

Теперь класс MyRoot должен выглядеть так, как показано ниже:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

    def generate_number(self):
        self.random_label.text = str(random.randint(0, 2000))

Поздравляем! Вы закончили работу с основным файлом приложения.

Ручное тестирование приложения

Осталось только убедиться, что при нажатии на кнопку generate вызывается эта функция. Для этого достаточно добавить строку on_press: root.generate_number() в часть выбора кнопки в файле .kv:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15
            on_press: root.generate_number()

Теперь можно запустить это приложение:

Компиляция нашего приложения для Android, Windows и iOS

Перед компиляцией нашего приложения для Android у меня есть плохие новости для пользователей Windows. Для компиляции приложения под Android вам потребуется Linux или macOS. Однако не обязательно иметь отдельный дистрибутив Linux — вместо этого можно использовать виртуальную машину.

Для компиляции и генерации полноценного приложения Android в формате .apk мы будем использовать инструмент Buildozer.

Установим Buildozer через терминал, используя одну из приведенных ниже команд:

pip3 install buildozer
//
pip install buildozer

Теперь мы установим некоторые необходимые для Buildozer зависимости. Я использую Linux Ergo, поэтому буду использовать команды, специфичные для Linux. Выполнять эти команды следует поочередно:

sudo apt update
sudo apt install -y git zip unzip openjdk-13-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev libssl-dev

pip3 install --upgrade Cython==0.29.19 virtualenv 

# Добавьте следующую строку в конец вашего файла ~/.bashrc 
export PATH=$PATH:~/.local/bin/

После выполнения указанных команд запустите buildozer init. Вы должны увидеть результат, аналогичный результату на скриншоте:

Успешная инициализация Buildozer
Успешная инициализация Buildozer

Приведенная выше команда создает файл buildozer .spec, который можно использовать для внесения спецификаций в приложение, включая название приложения, иконку и т.д. Файл .spec должен выглядеть следующим образом:

[app]

# (str) Title of your application
title = My Application

# (str) Package name
package.name = myapp

# (str) Package domain (needed for android/ios packaging)
package.domain = org.test

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas

# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png

# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec

# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin

# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg

# (str) Application versioning (method 1)
version = 0.1

# (str) Application versioning (method 2)
# version.regex = __version__ = \['"\](.*)['"]
# version.filename = %(source.dir)s/main.py

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy

# (list) Garden requirements
#garden_requirements =

# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png

# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY

#
# OSX Specific
#

#
# author = © Copyright Info

# change the major version of python used by the app
osx.python_version = 3

# Kivy version to use
osx.kivy_version = 1.9.1

#
# Android specific
#

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0

# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF

# (list) Permissions
#android.permissions = INTERNET

# (int) Target Android API, should be as high as possible.
#android.api = 27

# (int) Minimum API your APK will support.
#android.minapi = 21

# (int) Android SDK version to use
#android.sdk = 20

# (str) Android NDK version to use
#android.ndk = 19b

# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21

# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True

# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =

# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =

# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False

# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity

# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"

# (list) Pattern to whitelist for the whole project
#android.whitelist =

# (str) Path to a custom whitelist file
#android.whitelist_src =

# (str) Path to a custom blacklist file
#android.blacklist_src =

# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar

# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =

# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =

# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =

# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"

# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes 
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =

# (list) packaging options to add 
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes 
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_gradle_repositories =

# (list) Java classes to add as activities to the manifest.
#android.add_activities = com.example.ExampleActivity

# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME

# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png

# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =

# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard

# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so

# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False

# (list) Android application meta-data to set (key=value format)
#android.meta_data =

# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =

# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =

# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a

# (int) overrides automatic versionCode computation (used in build.gradle)
# this is not the same as app version and should only be edited if you know what you're doing
# android.numeric_version = 1

#
# Python for android (p4a) specific
#

# (str) python-for-android fork to use, defaults to upstream (kivy)
#p4a.fork = kivy

# (str) python-for-android branch to use, defaults to master
#p4a.branch = master

# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =

# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =

# (str) Filename to the hook for p4a
#p4a.hook =

# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2

# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =


#
# iOS specific
#

# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master

# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0

# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"

# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s


[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1

# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin

#    -----------------------------------------------------------------------------
#    List as sections
#
#    You can define all the "list" as [section:key].
#    Each line will be considered as a option to the list.
#    Let's take [app] / source.exclude_patterns.
#    Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
#    This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


#    -----------------------------------------------------------------------------
#    Profiles
#
#    You can extend section / key with a profile
#    For example, you want to deploy a demo version of your application without
#    HD content. You could first change the title to add "(demo)" in the name
#    and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
#    Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

Если необходимо указать такие параметры, как значок, требования или экран загрузки, то следует отредактировать этот файл.

После внесения всех необходимых изменений в приложение запустите команду buildozer -v android debug из каталога app для сборки и компиляции приложения. Это может занять некоторое время, особенно если у вас медленное устройство.

После завершения процесса в терминале должно появиться несколько логов, один из которых подтверждает, что сборка прошла успешно:

В каталоге bin также должна находиться APK-версия приложения. Это исполняемый файл приложения, который будет установлен и запущен на телефоне:

Поздравляем! Если вы следовали этому руководству шаг за шагом, то у вас на телефоне должно быть простое приложение генератора случайных чисел. Поэкспериментируйте с ним, подкорректируйте некоторые значения, а затем заново его скомпилируйте. Запуск повторной сборки займет не так много времени, как первая сборка.

Заключение

Как видите, создать мобильное приложение на Python достаточно просто, особенно, если вы хорошо знакомы с фреймворком или модулем, с которым работаете.

Если вы хотите упаковать приложение для других платформ, то можете ознакомиться с шагами здесь. Это достаточно просто, и вам не понадобится переписывать логику приложения под другую платформу. И еще, имейте в виду, что для экосистемы Apple вам понадобится компьютер Mac.

Дальше можете ознакомиться с модулем Kivy и его виджетами. Невозможно познать все и сразу. Нужно только найти проект и как можно раньше начать работать. Счастливого кодинга!