python

Kivy. Сборка пакетов под Android и никакой магии

  • понедельник, 9 декабря 2019 г. в 00:26:50
https://habr.com/ru/post/479236/
  • Python
  • Разработка мобильных приложений




Во вчерашней статье 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<Counter> createState() => _CounterState();
  // Результатом функции является не просто объект класса State,
  // а обязательно State<ИмяНашегоВиджета>
}

class _CounterState extends State<Counter> {
  // Внутри него мы наконец-то можем объявить динамические переменные,
  // в которых мы будем хранить состояние.

  // В данном случае, это счетчик количества нажатий
  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()


По-моему, вывод очевиден и не нуждается в моем комментировании…
Надеюсь, был вам полезен. Оставляю опрос на тему «Удалось ли вам построить приложение для Андроид»: