Kivy. Сборка пакетов под Android и никакой магии
Во вчерашней статье Python в Mobile development, в которой речь шла о библиотеке KivyMD (коллекции виджетов в стиле Material Design для использования их в кроссплатформенном фреймворке Kivy), в комментариях меня попросили рассказать о процессе сборки пакета для платформы Android. Для многих этот процесс, к сожалению, был и остается чем-то из ряда магического шаманства и не подъёмным для новичков делом. Что ж, давайте разбираться, так ли на самом деле все сложно и действительно ли я маг и волшебник…
Конечно, мог бы! Итак, вы написали свой код на Python и Kivy. Что нужно для того, чтобы это можно было запустить на Android устройствах? Перейдите в репозиторий KivyMD и вы увидите, что в этой инструкции уже давно прописаны шаги, которые позволят вам собрать APK пакет:
1. Загрузите XUbuntu 18.04
2. Установите Virtual Box на свой компьютер.
3. Создайте новую виртуальную машину на основе загруженного образа XUbuntu
4. Запустите виртуальную машину XUbuntu, откройте терминал и выполните нижеследующую команду:
wget https://github.com/HeaTTheatR/KivyMD-data/raw/master/install-kivy-buildozer-dependencies.sh
chmod + x install-kivy-buildozer-dependencies.sh
./install-kivy-buildozer-dependencies.sh
Все! Теперь у вас есть виртуальная машина для сборки APK пакетов для приложений Kivy! Что дальше? Давайте, собственно, займемся сборкой тестового приложения. Создайте в домашнем каталоге вашей виртуальной машины директорию TestKivyMD с пустым файлом main.py:
Далее откройте файл main.py и напишите код нашего тестового приложения, которое будет использовать библиотеку KivyMD:
from kivy.lang import Builder
from kivymd.app import MDApp
KV = """
Screen:
MDToolbar:
title: "My firt app"
elevation: 10
md_bg_color: app.theme_cls.primary_color
left_action_items: [["menu", lambda x: x]]
pos_hint: {"top": 1}
MDRaisedButton:
text: "Hello World"
pos_hint: {"center_x": .5, "center_y": .5}
"""
class HelloWorld(MDApp):
def build(self):
return Builder.load_string(KV)
HelloWorld().run()
Сохраните, откройте терминал в директории с файлом main.py и установите библиотеку KivyMD:
sudo pip3 install kivymd
После установки можно протестировать наш код:
python3 main.py
Результатом работы скрипта будет экран с Toolbar и одной кнопкой «Hello World»:
Дальше нам нужно создать файл спецификации buildozer.spec, который должен располагаться в той жу директории, что и файл main.py:
Если вы не закрывали терминал (если терминал был закрыт, откройте его в директории TestKivyMD), введите команду:
buikdozer init
Эта команда создаст дефолтный файл спецификации. Откройте его и отредактируйте:
[app]
# (str) Title of your application
title = KivyMDTest
# (str) Package name
package.name = kivymd_test
# (str) Package domain (needed for android/ios packaging)
package.domain = com.heattheatr
# (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,jpeg,ttf
# (list) Application version
version = 0.0.1
# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy==1.11.1,kivymd
# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 1
# (list) Permissions
android.permissions = INTERNET,WRITE_EXTERNAL_STORAGE
# (int) Target Android API, should be as high as possible.
android.api = 28
# (int) Minimum API your APK will support.
android.minapi = 21
# (str) Android NDK version to use
android.ndk = 17c
# (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 = True
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a
[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 = 0
# (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
Здесь все понятно поэтому дополнительные комментарии излишне. Почитайте внимательно дефолтную спецификацию, в ней можно указать путь к иконке, пресплеш при загрузке приложения и многое другое. Я оставил лишь то, что нам сейчас нужно для сборки нашего тестового пакета. И, собственно, запускаем процесс сборки командой в терминале:
buildozer android debug
Можете смело идти на кухню и заваривать кофе, потому что в первый раз процесс загрузки и компиляции библиотек займет очень много времени. Все последующие сборки проходят за 10–20 секунд.
Кофе выпит и самое время заглянуть в терминал:
Вуаля! Наше приложение построено! Самое время закинуть его на смартфон и запустить:
Все работает! И оказывается не все так сложно, как казалось.
Также меня спрашивали:
Ни у Flutter ни у React Native нет преимуществ перед языком разметки Kivy Language, которая позволяет создавать и позиционировать лайоуты и виджеты. Как по мне, то, как строится UI во Flutter — это самое настоящее извращение. Придумать это мог только больной на голову человек. Чтобы не быть голословным, давайте посмотрим на код Flutter и код Kivy одного и того же простейшего приложения… Выглядеть оно будет следующим образом:
Ниже я привожу код из статьи Про Flutter, кратко: Основы:
import 'package:flutter/widgets.dart';
main() => runApp(
Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Color(0xFFFFFFFF),
child: App(),
),
),
);
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector( // используется как обычный виджет
onTap: () { // одно из свойств GestureDetector
// Этот метод будет вызван, когда дочерний элемент будет нажат
print('You pressed me');
},
child: Container( // нашей кнопкой будет контейнер
decoration: BoxDecoration( // стилизуем контейнер
shape: BoxShape.circle, // зададим ему круглую форму
color: Color(0xFF17A2B8), // и покрасим его в синий
),
width: 80.0,
height: 80.0,
),
),
);
}
}
class Counter extends StatefulWidget {
// Изменяемое состояние хранится не в виджете, а внутри объекта особого класса,
// создаваемого методом createState()
@override
State createState() => _CounterState();
// Результатом функции является не просто объект класса State,
// а обязательно State<ИмяНашегоВиджета>
}
class _CounterState extends State {
// Внутри него мы наконец-то можем объявить динамические переменные,
// в которых мы будем хранить состояние.
// В данном случае, это счетчик количества нажатий
int counter = 0;
// А дальше все очень просто, мы имплементируем точно такой же метод
// для отрисовки виджетов, который мы использовали в классе Stateless виджета.
@override
Widget build(BuildContext context) {
// И тут практически ничего не изменилось с нашего последнего примера,
// а то что изменилось — я прокомментировал:
return Center(
child: GestureDetector(
onTap: () {
// В момент, когда кнопка нажата, мы увеличиваем значение
// перменной counter.
setState(() {
// setState() необходим для того, чтобы вызвать методы
// жизненного цикла виджета и сказать ему, что пора обновится
++counter;
});
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF17A2B8),
),
width: 80.0,
child: Center(
child: Text( // выводим значение свойства counter
'$counter', // чтобы следить за его изменением
style: TextStyle(fontSize: 30.0),
),
),
),
),
);
}
}
А вот абсолютно тоже самое, но с использованием Kivy и KivyMD:
from kivy.lang import Builder
from kivymd.app import MDApp
KV = """
#:import get_color_from_hex kivy.utils.get_color_from_hex
Screen:
MDCard:
MDLabel:
value: 0
text: str(self.value)
halign: "center"
on_touch_down: self.value += 1
canvas.before:
Color:
rgba: get_color_from_hex("#4eaabe")
Ellipse:
pos: self.center[0] - dp(25), self.center[1] - dp(25)
size: dp(50), dp(50)
"""
class HelloWorld(MDApp):
def build(self):
return Builder.load_string(KV)
HelloWorld().run()
По-моему, вывод очевиден и не нуждается в моем комментировании…
Надеюсь, был вам полезен. Оставляю опрос на тему «Удалось ли вам построить приложение для Андроид»: