Разработка мобильных приложений на Python. Библиотека KivyMD

17428

Разработка мобильных приложений на Python. Библиотека KivyMD

Разработка мобильных приложений на Python. Библиотека KivyMD
Разработка мобильных приложений на Python. Библиотека KivyMD

Приветствую! Сегодня речь снова пойдет о библиотеке KivyMD — наборе виджетов для кроссплатформенной разработки на Python в стиле Material Design. Что-то похожего на туториал по разработке мобильных приложений на Python для новичков здесь не будет, так что если впервые слышите о фреймворке Kivy, вряд ли вам будет все это интересно. Ну, а мы погнали под кат!

На днях скачал из Google Play демонстрационное приложение Flutter UIKit:

И сейчас мы с вами попробуем повторить один экран из этого приложения. Давайте сразу посмотрим на результаты: слева — Flutter, справа — Kivy & KivyMD.

 

 

 

 

 

 

 

 

 

 

 

Некоторые элементы UI отличаются, не в силу каких-то технических особенностей, из-за которых нельзя было получить идентичный результат, а просто я посчитал, что так будет более органичней (например, черный Toolbar, по моему мнению, совсем не смотрится).

Итак! Что бросается в глаза, глядя на экран, который мы будем воспроизводить? Прозрачный фон переднего layout. В Kivy такую возможность предоставляет FloatLayout, который позволяет размещать в себе виджеты и контроллы один над другим следующим образом:

Схематично наш экран будет выглядеть так:

Разметка этого экрана довольно простая:

Почему я говорю о FloatLayout, если наш экран унаследован от Screen?

<ProductScreen@Screen>:

    ...

Просто потому, что Screen —> RelativeLayout —> FloatLayout.

Все виджеты во FloatLayout позиционируются от нижнего левого угла, то есть, на экране им автоматически присваивается позиция (0, 0). В разметке не сложно проследить порядок добавления элементов на экран сверху вниз:

Если кто-то обратил внимание, то позицию мы указали только одному виджету:

MDToolbar:
    ...
    pos_hint: {"top": 1}

Каждому виджету в Kivy помимо конкретных координат (x, y) можно указать подсказку позиции:

pos_hint: {"top": 1}  # верхняя граница экрана
pos_hint: {"bottom": 1}  # нижняя граница экрана
pos_hint: {"right": 1}  # правая граница экрана
pos_hint: {"center_y": .5}  # центр экрана по вертикали
pos_hint: {"center_x": .2}  # отступ в 20 % по горизонтали от левой границы экрана
...
...

Так вот, нижнее фоновое изображение…

BoxLayout:
        size_hint_y: None
        height: root.height - toolbar.height

        FitImage:
            source: "smokestackheather.jpeg"

… благодаря виджету FitImage (библиотека KivyMD), автоматически растягивается на все выделенное ему пространство с сохранением пропорций изображения:

По умолчанию каждому виджету и лайоуту в Kivy предоставляется 100 % пространства, если не указанно иное. Например, если вы захотите добавить на экран одну кнопку, вы, очевидно сделаете следующее:

from kivy.app import App
from kivy.lang import Builder

KV = """
Button:
    text: "Button"
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(KV)


MyApp().run()

И получите результат:

Кнопка заняла 100 % пространства. Чтобы разместить кнопку по центру экрана, нужно, во-первых, задать ей необходимый размер и, во-вторых, указать, где она будет находится:

from kivy.app import App
from kivy.lang import Builder

KV = """
Button:
    text: "Button"
    size_hint: None, None
    size: 100, 50
    pos_hint: {"center_y": .5, "center_x": .5}
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(KV)


MyApp().run()

Теперь картина изменилась:

Также можно указать свойство size_hint, от 0 до 1, (эквивалент 0-100%), то есть, подсказка размера:

from kivy.app import App
from kivy.lang import Builder

KV = """
BoxLayout:

    Button:
        text: "Button"
        size_hint_y: .2

    Button:
        text: "Button"
        size_hint_y: .1

    Button:
        text: "Button"
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(KV)


MyApp().run()

Или тоже самое, но подсказка ширины (size_hint_x):

from kivy.app import App
from kivy.lang import Builder

KV = """
BoxLayout:

    Button:
        text: "Button"
        size_hint_x: .2

    Button:
        text: "Button"
        size_hint_x: .1

    Button:
        text: "Button"
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(KV)


MyApp().run()

MDToolbar имеет высоту в 56dp, не может занимать все пространство, и если ему не подсказать, что его место сверху, то он автоматически прилипнет к нижней части экрана:

Список карточек — OrderProductLayout (о нем мы поговорим ниже) — это ScrollView с элементами MDCard и он занимает всю высоту экрана, но благодаря padding (значения отступов в лайоутах) кажется, что он находится чуть выше центра экрана. Ну а MDBottomAppBar по умолчанию кидает якорь к нижней границе экрана. Поэтому только MDToolbar мы указали, где его место.

Теперь давайте посмотрим, что представляет из себя виджет OrderProductLayout:

Как видим, это четыре карточки, вложенные в ScrillView. В отличие от родительского экрана, который унаследован от FloatLayout, здесь все виджеты читаются сверху вниз.

Это очень удобно, поскольку прослеживается четкая иерархия виджетов, древовидная структура и с одного взгляда понятно, какой виджет/контролл какому лайоуту принадлежит. В Kivy наиболее частым используемым лайоутом является BoxLayout — коробка, которая позволяет размещать в себе виджеты по вертикали либо по горизонтали (по умолчанию — последнее):

Более наглядно это видно из следующей схемы, где используется BoxLayout горизонтальной ориентации:

Мы запретили BoxLayout использовать 100% пространства — size_hint_y: None и сказали — твоя высота будет ровно такой, какой будет высота самого высокого элемента, вложенного в тебя — height: self.minimum_height.

Список изображений:

Если бы мы захотели использовать вертикальную прокрутку списка, нам нужно было бы изменить GridLayout следующим образом:

ScrollView:

      GridLayout:
          size_hint_y: None
          height: self.minimum_height
          cols: 1

Заменить строки (rows) на столбцы (cols) и указать в minimum не ширину, а высоту:

from kivy.app import App
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.uix.button import Button

KV = """
ScrollView:

    GridLayout:
        id: box
        size_hint_y: None
        height: self.minimum_height
        spacing: "5dp"
        cols: 1
"""


class MyApp(App):
    def build(self):
        return Builder.load_string(KV)

    def on_start(self):
        for i in range(20):
            self.root.ids.box.add_widget(
                Button(
                    text=f"Label {i}",
                    size_hint_y=None,
                    height=dp(40),
                )
            )


MyApp().run()

Следующие карты — выбор цвета и размера (они практически идентичны):

Отличительной особенностью языка разметки Kv Language является не только четкая структура виджетов, но и то, что этот язык поддерживает некоторые возможности языка Python. А именно: вызов методов, создание/изменение переменных, логические, I/O и математические операции…

Вычисление значения value, объявленного в Label

Label:
          value: 0
          text: str(self.value)

… происходит непосредственно в самой разметке:

MDIconButton:
           on_release: label_value.value -= 1 if label_value.value > 0 else 0

И я никогда не поверю, что вот это (код Flutter)…

… логичнее и читабельнее кода Kv Language:

Вчера меня спрашивали, как у Kivy обстоят дела со средой разработки, есть ли автокомплиты, хотрелоад и прочие прелести? С автокомплитами все отлично, если пользоваться PyCharm:

Насчет хотрелоад… Python — интерпретируемый язык. Kivy использует Python. Соответственно, чтобы увидеть результат, не нужна компиляция кода, запустил — увидел/протестирвал. Как я уже говорил, Kivy не использует нативные API для рендера UI, поэтому позволяет эмулировать различные модели устройств и платформ с помощью модуля screen. Достаточно запустить ваш проект с нужными параметрами, чтобы на компьютере открылось окно тестируемого приложения так, как если бы оно было запущено на реальном устройстве. Звучит странно, но поскольку Kivy абстрагируется от платформы в отрисовке UI, это позволяет не использовать тяжелые и медленные эмуляторы для тестов. Это касается только UI. Например, тестовое приложение, описываемое в этой статье тестировалось с параметрами -m screen:droid2, portrait, scale=.75.

Слева — запущено на мобильном устройстве, справа — на компьютере:

 

 

 

 

 

 

 

 

 

 

Полный список параметров модуля screen:

devices = {
    # device: (name, width, height, dpi, density)
    'onex': ('HTC One X', 1280, 720, 312, 2),
    'one': ('HTC One', 1920, 1080, 468, 3),
    'onesv': ('HTC One SV', 800, 480, 216, 1.5),
    's3': ('Galaxy SIII', 1280, 720, 306, 2),
    'note2': ('Galaxy Note II', 1280, 720, 267, 2),
    'droid2': ('Motorola Droid 2', 854, 480, 240, 1.5),
    'xoom': ('Motorola Xoom', 1280, 800, 149, 1),
    'ipad': ('iPad (1 and 2)', 1024, 768, 132, 1),
    'ipad3': ('iPad 3', 2048, 1536, 264, 2),
    'iphone4': ('iPhone 4', 960, 640, 326, 2),
    'iphone5': ('iPhone 5', 1136, 640, 326, 2),
    'xperiae': ('Xperia E', 480, 320, 166, 1),
    'nexus4': ('Nexus 4', 1280, 768, 320, 2),
    'nexus7': ('Nexus 7 (2012 version)', 1280, 800, 216, 1.325),
    'nexus7.2': ('Nexus 7 (2013 version)', 1920, 1200, 323, 2),

    # taken from design.google.com/devices
    # please consider using another data instead of
    # a dict for autocompletion to work
    # these are all in landscape
    'phone_android_one': ('Android One', 854, 480, 218, 1.5),
    'phone_htc_one_m8': ('HTC One M8', 1920, 1080, 432, 3.0),
    'phone_htc_one_m9': ('HTC One M9', 1920, 1080, 432, 3.0),
    'phone_iphone': ('iPhone', 480, 320, 168, 1.0),
    'phone_iphone_4': ('iPhone 4', 960, 640, 320, 2.0),
    'phone_iphone_5': ('iPhone 5', 1136, 640, 320, 2.0),
    'phone_iphone_6': ('iPhone 6', 1334, 750, 326, 2.0),
    'phone_iphone_6_plus': ('iPhone 6 Plus', 1920, 1080, 400, 3.0),
    'phone_lg_g2': ('LG G2', 1920, 1080, 432, 3.0),
    'phone_lg_g3': ('LG G3', 2560, 1440, 533, 3.0),
    'phone_moto_g': ('Moto G', 1280, 720, 327, 2.0),
    'phone_moto_x': ('Moto X', 1280, 720, 313, 2.0),
    'phone_moto_x_2nd_gen': ('Moto X 2nd Gen', 1920, 1080, 432, 3.0),
    'phone_nexus_4': ('Nexus 4', 1280, 768, 240, 2.0),
    'phone_nexus_5': ('Nexus 5', 1920, 1080, 450, 3.0),
    'phone_nexus_5x': ('Nexus 5X', 1920, 1080, 432, 2.6),
    'phone_nexus_6': ('Nexus 6', 2560, 1440, 496, 3.5),
    'phone_nexus_6p': ('Nexus 6P', 2560, 1440, 514, 3.5),
    'phone_samsung_galaxy_note_4': ('Samsung Galaxy Note 4',
                                    2560, 1440, 514, 3.0),
    'phone_samsung_galaxy_s5': ('Samsung Galaxy S5', 1920, 1080, 372, 3.0),
    'phone_samsung_galaxy_s6': ('Samsung Galaxy S6', 2560, 1440, 576, 4.0),
    'phone_sony_xperia_c4': ('Sony Xperia C4', 1920, 1080, 400, 2.0),
    'phone_sony_xperia_z_ultra': ('Sony Xperia Z Ultra', 1920, 1080, 348, 2.0),
    'phone_sony_xperia_z1_compact': ('Sony Xperia Z1 Compact',
                                     1280, 720, 342, 2.0),
    'phone_sony_xperia_z2z3': ('Sony Xperia Z2/Z3', 1920, 1080, 432, 3.0),
    'phone_sony_xperia_z3_compact': ('Sony Xperia Z3 Compact',
                                     1280, 720, 313, 2.0),
    'tablet_dell_venue_8': ('Dell Venue 8', 2560, 1600, 355, 2.0),
    'tablet_ipad': ('iPad', 1024, 768, 132, 1.0),
    'tablet_ipad_mini': ('iPad Mini', 1024, 768, 163, 1.0),
    'tablet_ipad_mini_retina': ('iPad Mini Retina', 2048, 1536, 326, 2.0),
    'tablet_ipad_pro': ('iPad Pro', 2732, 2048, 265, 2.0),
    'tablet_ipad_retina': ('iPad Retina', 2048, 1536, 264, 2.0),
    'tablet_nexus_10': ('Nexus 10', 2560, 1600, 297, 2.0),
    'tablet_nexus_7_12': ('Nexus 7 12', 1280, 800, 216, 1.3),
    'tablet_nexus_7_13': ('Nexus 7 13', 1920, 1200, 324, 2.0),
    'tablet_nexus_9': ('Nexus 9', 2048, 1536, 288, 2.0),
    'tablet_samsung_galaxy_tab_10': ('Samsung Galaxy Tab 10',
                                     1280, 800, 148, 1.0),
    'tablet_sony_xperia_z3_tablet': ('Sony Xperia Z3 Tablet',
                                     1920, 1200, 282, 2.0),
    'tablet_sony_xperia_z4_tablet': ('Sony Xperia Z4 Tablet',
                                     2560, 1600, 297, 2.0)TodoList()
        app.run()

}

Ну, и, наконец, финальный результат — запуск на мобильном устройстве…

Единственное, что огорчает, это скорость запуска. У того же Flutter она просто феноменальная!

Надеюсь, был кому-то полезен, до новых встреч!

источник