Некоторое время тому назад я решил попробовать написать что-то на Python под Android. Такой странный для многих выбор обусловлен тем, что я люблю Python и люблю Android, а ещё люблю делать необычное (ну хорошо, не самое обычное). В качестве фреймворка был выбран Kivy — фактически, безальтернативный вариант, но он мне очень понравился. Однако, по нему не так уж много информации (нет, документация отличная, но иногда её недостаточно), особенно на русском языке, а некоторые вещи хоть и можно реализовать, но их то ли никто раньше не делал, то ли не счёл нужным поделиться информацией. Ну а я счёл :) И этот пост тому результатом.

Под катом я расскажу обо всех этапах разработки, о том, как развивалась простая идея и как для этого приходилось искать новые возможности, о возникших подводных камнях и багах, о неочевидных решениях и устаревшей документации :) Цель — описать в одном тексте основные пункты, чтобы человеку, решившему написать что-то немного сложнее игры Pong из официального туториала, не приходилось перерывать официальный форум поддержки и StackOverflow и тратить часы на то, что делается за пару минут, если знаешь, как.

0. Если вы впервые слышите о Kivy...


… то всё зависит от того, любите ли вы Python и Android, и интересно ли вам в этом разобраться. Если нет — проще забить :) А если да, то начать нужно с официальной документации, гайдов, и уже упомянутого официального туториала по игре Pong — это даст базовое представление о фреймворке и его возможностях. Я же не буду останавливаться на столь тривиальных вещах (тем более, для понимания базовых принципов туториал отлично подходит) и сразу пойду дальше. Будем считать, что это было вступление :)

1. Немного о моей игре


Для начала нужна была идея. Мне хотелось что-то достаточно простое, чтобы оценить возможности фреймворка, но и достаточно интересное и оригинальное, чтобы не программировать ради программирования (это здорово, но когда это не единственная цель — это ещё лучше). Я неплохо проектирую интерфейсы, но не умею рисовать, поэтому игра должна была быть простая графически, или вообще текстовая. И тут так уж сложилось, что у меня есть заброшенный сайт с цитатами, с которого я когда-то начинал свой путь в web-разработке (я о нём даже писал на Хабре много лет назад). Поэтому идея возникла такая: игра-викторина «Угадай цитату». В русскоязычном Google Play ничего подобного не было, а в англоязычном была пара поделок низкого качества с сотней скачиваний.

Почти сразу же стало понятно, что просто так отгадывать цитату за цитатой — скучно. Так появились первые «фишки», которые, в итоге, и определили итоговую игру. В первую очередь это были тематические пакеты (то есть пакеты цитат, объединённые одной темой или автором) и баллы (которые начисляются за отгадывание цитат и прохождение пакетов, и тратятся на подсказки и разблокировку новых тем), а также статистика, достижения и избранное.

Немного картинок
Так всё начиналось (кликабельно):


Ну ладно, ладно, больше не буду показывать такой ужас :) Кстати, вот так оно выглядит сейчас (тоже кликабельно, скрины взяты с Google Play):

Первые проблемы начались с первого же экрана…

2. Kivy тормоз или я что-то делаю не так?


Один мой друг любит отвечать на такие вопросы «да» :) На самом деле, некоторые вещи в Kivy действительно работают медленно, например, создание виджетов. Но это не значит, что это дело нельзя оптимизировать. Об этом я и расскажу.

Так как цитаты и темы хранятся в БД, то, само собой, кнопки с пакетами генерируются динамически. И вот тут-то я обнаружил, что происходит это очень медленно: примерно полсекунды на список из 20 кнопок. Возможно, это и не очень много при загрузке приложения, но при переходе на главный экран из других внутренних экранов приложения — непозволительно много. Здесь стоит отметить, что кнопка к тому моменту уже представляла собой, на самом деле, набор из нескольких элементов, визуально составляющих одну кнопку:



Первым моим побуждением было тем или иным образом закешировать их, и, действительно, опыт показал, что если создать все виджеты заранее, и сохранить их как свойство объекта StartScreen, то всё (кроме первой генерации) работает достаточно быстро. Однако же, данные в кнопках нужно периодически обновлять (хотя бы то же количество отгаданных цитат). Да и загрузку новых пакетов я уже тогда планировал. Конечно, не проблема реализовать и это, но я решил не изобретать велосипед и подумать.

Сначала стоило убедиться, что проблема именно в создании виджетов, поэтому я за несколько минут набросал простенькое приложение на два экрана, в каждом из которых генерировался набор строк из лейбла и чекбокса количеством 50 шт. :)

Исходный код тестового приложения
main.py:
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty, StringProperty
from kivy.clock import Clock

from time import time

class ListScreen(Screen):

    items_box = ObjectProperty(None)

    def on_enter(self):
        start = time()
        for i in range(0,50):
            self.items_box.add_widget(ListItem('Item '+str(i)))
        self.items_box.bind(minimum_height=self.items_box.setter('height'))
        print time()-start

    def on_leave(self):
        self.items_box.clear_widgets()

class ListItem(BoxLayout):

    title = StringProperty('')

    def __init__(self, title, **kwargs):
        super(ListItem, self).__init__(**kwargs)
        self.title = title

class ListApp(App):

    sm = ScreenManager()
    screens = {}

    def build(self):
        self.__create_screens()
        ListApp.sm.add_widget(ListApp.screens['list1'])
        Clock.schedule_interval(self._switch, 1)
        return ListApp.sm

    def _switch(self, *args):
        ListApp.sm.switch_to(ListApp.screens['list1' if ListApp.sm.current != 'list1' else 'list2'])

    def __create_screens(self):
        ListApp.screens['list1'] = ListScreen(name='list1')
        ListApp.screens['list2'] = ListScreen(name='list2')

if __name__ == '__main__':
    ListApp().run()

list.kv:

<ListScreen>:
    items_box: items_box
    BoxLayout:
        orientation: "vertical"
        AnchorLayout:
            size_hint_y: 0.1
            padding: self.width*0.1, self.height*0.05
            Label:
                font_size: root.height*0.05
                text: "Some list"
        ScrollView:
            size_hint_y: 0.9
            size: self.size
            BoxLayout:
                id: items_box
                orientation: "vertical"
                padding: self.width*0.1, 0
                size_hint_y: None

<ListItem>:
    orientation: "horizontal"
    size_hint_y: None
    height: app.sm.height*0.1
    Label:
        font_size: app.sm.height*0.025
        text: root.title
        size_hint_x: 0.9
        text_size: self.size
        valign: "middle"
    CheckBox
        size_hint_x: 0.1

Запустил на своём стареньком Moto G (gen3) и получил:

11-28 11:44:09.525  1848  2044 I python  : 0.5793800354
11-28 11:44:10.853  1848  2044 I python  : 0.453143119812
11-28 11:44:12.544  1848  2044 I python  : 0.633069992065
11-28 11:44:13.697  1848  2044 I python  : 0.369570970535
11-28 11:44:14.988  1848  2044 I python  : 0.594089031219

И далее в том же духе. Поиск по этому вопросу ничего не дал, поэтому я обратился к разработчикам. И получил ответ: «Создание виджетов относительно медленное, особенно в зависимости от того, что они содержат. Для создания больших списков лучше использовать RecycleView». Здесь хочу пояснить, почему я вообще описываю этот момент, ведь описание RecycleView есть в документации. Да, действительно, есть, но мало кто способен изучить и запомнить всю документацию перед тем, как начнёт разработку, и найти нужный инструмент бывает непросто, особенно если он нигде не описан в контексте решения конкретной проблемы. Теперь же он описан :)

Исходный код тестового приложения с RecycleView
main.py:
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty
from kivy.clock import Clock

from time import time

class ListScreen(Screen):

    recycle_view = ObjectProperty(None)
    items_box = ObjectProperty(None)

    def on_enter(self):
        start = time()
        for i in range(0,50):
            self.recycle_view.data.append({'title': 'item'+str(i)})
        print time()-start

    def on_leave(self):
        self.recycle_view.data = []

class ListApp(App):

    sm = ScreenManager()
    screens = {}

    def build(self):
        self.__create_screens()
        ListApp.sm.add_widget(ListApp.screens['list1'])
        Clock.schedule_interval(self._switch, 1)
        return ListApp.sm

    def _switch(self, *args):
        ListApp.sm.switch_to(ListApp.screens['list1' if ListApp.sm.current != 'list1' else 'list2'])

    def __create_screens(self):
        ListApp.screens['list1'] = ListScreen(name='list1')
        ListApp.screens['list2'] = ListScreen(name='list2')

if __name__ == '__main__':
    ListApp().run()

list.kv:
<ListScreen>:
    recycle_view: recycle_view
    items_box: items_box
    BoxLayout:
        orientation: "vertical"
        AnchorLayout:
            size_hint_y: 0.1
            padding: self.width*0.1, self.height*0.05
            Label:
                font_size: root.height*0.05
                text: "Some list"
        RecycleView:
            id: recycle_view
            size_hint: 1, 0.9
            viewclass: "ListItem"
            RecycleBoxLayout:
                id: items_box
                orientation: "vertical"
                padding: self.width*0.1, 0
                default_size_hint: 1, None
                size_hint: 1, None
                height: self.minimum_height

<ListItem@BoxLayout>:
    orientation: "horizontal"
    size_hint: 1, None
    height: app.sm.height*0.1
    title: ''
    Label:
        font_size: app.sm.height*0.025
        text: root.title
        size_hint_x: 0.9
        text_size: self.size
        valign: "middle"
    CheckBox
        size_hint_x: 0.1

11-29 13:11:58.196 13121 13203 I python  : 0.00388479232788
11-29 13:11:59.192 13121 13203 I python  : 0.00648307800293
11-29 13:12:00.189 13121 13203 I python  : 0.00288391113281
11-29 13:12:01.189 13121 13203 I python  : 0.00324606895447
11-29 13:12:03.188 13121 13203 I python  : 0.00265002250671

Более чем в 100 раз быстрее. Впечатляет, не правда ли?

В завершение следует упомянуть, что RecycleView — не панацея. Он не подходит, если размер элемента зависит от содержимого (например, Label, размер которого меняется в зависимости от количества текста).

3. Сервисы. Автозапуск и перезапуск


Следующая проблема, с которой я столкнулся, не поддавалась решению так долго, что я уже малодушно подумывал счесть данный фреймворк непригодным и забить :) Проблема была с сервисами (в Android так называется процессы, выполняющиеся в фоновом режиме). Создать сервис не так уж и сложно — немного сбивает с толку устаревшая документация, но и только. Однако, в большинстве случаев, много ли толку от сервиса, который, во-первых, не запускается автоматически при загрузке телефона, а во-вторых, не перезапускается, если «выбросить» приложение свайпом из диспетчера задач? По-моему, нет.

На тот момент по этой теме была всего лишь одна статья в официальной wiki, но она, хоть и называлась «Starting Kivy service on bootup», на самом деле всего лишь рассказывала, как при загрузке телефона запустить приложение, но не его сервис (да, такое тоже бывает полезно, но значительно реже, как по мне). Ту статью я, в итоге, переписал, а здесь расскажу подробности.

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

from time import sleep

if __name__ == '__main__':
    while True:
        print "myapp service"
        sleep(5)

Из приложения мы запускаем его методом основного класса при помощи PyJnius:


from jnius import autoclass
# ... #
class GuessTheQuoteApp(App):
# ... #
    def __start_service(self):
        service = autoclass('com.madhattersoft.guessthequote.ServiceGuessthequoteservice')
        mActivity = autoclass('org.kivy.android.PythonActivity').mActivity
        service.start(mActivity, "")

Если APK собран правильно, при запуске приложения сервис будет стартовать, но этого недостаточно.

Для начала, попробуем сделать так, чтобы он перезапускался при остановке приложения (например, при снятии его из диспетчера задач). Конечно, можно было бы использовать startForeground, но это уже не совсем фоновое выполнение задачи :) Для него потребуется, как минимум, уведомление — это не всегда подходит. В данном случае идеально подходит флаг START_STICKY, но мы же пишем на Python, что делает задачу не столь тривиальной — по крайней мере, при помощи PyJnius она уже не решается.

Честно говоря, она вообще решается достаточно криво, поскольку я пока не готов становиться одним из разработчиков Python4Android, благодаря которому всё это счастье вообще работает. А изменения нужно вносить именно в код Python4Android. А конкретно, нам нужен файл .buildozer/android/platform/build/dists/guessthequote/src/org/kivy/android/PythonService.java в котором в функции startType() мы меняем флаг START_NOT_STICKY на START_STICKY:

public int startType() {
    return START_STICKY;
}

Ура, сервис рестартится. Всё? Конечно, нет :) Потому что он тут же валится с ошибкой:

E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.os.Bundle android.content.Intent.getExtras()' on a null object reference

Проблема в функции onStartCommand(Intent intent, int flags, int startId), поскольку после перезапуска intent у нас null. Что ж, перепишем и её:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (pythonThread != null) {
        Log.v("python service", "service exists, do not start again");
        return START_NOT_STICKY;
    }
    if (intent != null) { 
        startIntent = intent;
        Bundle extras = intent.getExtras();
        androidPrivate = extras.getString("androidPrivate");
        androidArgument = extras.getString("androidArgument");
        serviceEntrypoint = extras.getString("serviceEntrypoint");
        pythonName = extras.getString("pythonName");
        pythonHome = extras.getString("pythonHome");
        pythonPath = extras.getString("pythonPath");
        pythonServiceArgument = extras.getString("pythonServiceArgument");

        pythonThread = new Thread(this);
        pythonThread.start();

        if (canDisplayNotification()) {
            doStartForeground(extras);
        }
    } else {
        pythonThread = new Thread(this);
        pythonThread.start();
    }

    return startType();
}

Увы:

F DEBUG   : Abort message: 'art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: GetStringUTFChars received NULL jstring'

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

@Override
public void run(){
    String package_root = getFilesDir().getAbsolutePath();
    String app_root =  package_root + "/app";
    File app_root_file = new File(app_root);
    PythonUtil.loadLibraries(app_root_file);
    this.mService = this;

    if (androidPrivate == null) {
        androidPrivate = package_root;
    }
    if (androidArgument == null) {
        androidArgument = app_root;
    }
    if (serviceEntrypoint == null) {
        serviceEntrypoint = "./service/main.py"; // hardcoded
    }
    if (pythonName == null) {
        pythonName = "guessthequoteservice"; // hardcoded
    }
    if (pythonHome == null) {
        pythonHome = app_root;
    }
    if (pythonPath == null) {
        pythonPath = package_root;
    }
    if (pythonServiceArgument == null) {
        pythonServiceArgument = app_root+":"+app_root+"/lib";
    }

    nativeStart(
        androidPrivate, androidArgument,
        serviceEntrypoint, pythonName,
        pythonHome, pythonPath,
        pythonServiceArgument);
    stopSelf();
}

Теперь всё.

Перейдём к автозапуску сервиса при запуске телефона. После предыдущей проблемы это будет уже проще. (На самом деле же всё было наоборот — я очень долго не мог понять, что именно нужно добавить, поскольку информации об этом не было вообще нигде, и сами разработчики тоже не знали, как решить данную задачу. И только разобравшись параллельно с вопросом перезапуска, я понял, что нужно сделать.)

Для начала понадобится разрешение RECEIVE_BOOT_COMPLETED — это просто. А затем — BroadcastReceiver, его придётся добавить в AndroidManifest вручную, но это тоже не проблема. Проблема в том, что в нём писать :)

Решение для запуска приложения (не сервиса) выглядит так:

package com.madhattersoft.guessthequote;

import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
import org.kivy.android.PythonActivity;

public class MyBroadcastReceiver extends BroadcastReceiver {
    public void onReceive(Context context, Intent intent) {
        Intent ix = new Intent(context, PythonActivity.class);
        ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(ix);
    }
}

Сначала я попытался просто переписать его для сервиса:

package com.madhattersoft.guessthequote;

import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
import com.madhattersoft.guessthequote.ServiceGuessthequoteservice;

public class MyBroadcastReceiver extends BroadcastReceiver {
    public void onReceive(Context context, Intent intent) {
        Intent ix = new Intent(context, ServiceGuessthequoteservice.class);
        ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startService(ix);
    }
}

Ага, разогнался :)

E AndroidRuntime: java.lang.RuntimeException: Unable to start service com.madhattersoft.guessthequote.ServiceGuessthequoteservice@8c96929 with Intent { cmp=com.madhattersoft.guessthequote/.ServiceGuessthequoteservice }: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.os.Bundle.getString(java.lang.String)' on a null object reference

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


package import com.madhattersoft.guessthequote;

import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
import com.madhattersoft.guessthequote.ServiceGuessthequoteservice;

public class MyBroadcastReceiver extends BroadcastReceiver {
    public void onReceive(Context context, Intent intent) {
        String package_root = context.getFilesDir().getAbsolutePath();
        String app_root =  package_root + "/app";
        Intent ix = new Intent(context, ServiceGuessthequoteservice.class);
        ix.putExtra("androidPrivate", package_root);
        ix.putExtra("androidArgument", app_root);
        ix.putExtra("serviceEntrypoint", "./service/main.py");
        ix.putExtra("pythonName", "guessthequoteservice");
        ix.putExtra("pythonHome", app_root);
        ix.putExtra("pythonPath", package_root);
        ix.putExtra("pythonServiceArgument", app_root+":"+app_root+"/lib");
        ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startService(ix);
    }
}

Фух :)

Локализация и мультиязычность


В целом, для локализации можно использовать gettext, или же поступить ещё проще — создать папку lang, в ней по файлу на каждый язык (например, en.py и ru.py), определить там все слова и фразы в виде переменных/констант, и далее подключить нужный модуль. Примерно так:

if autoclass('java.util.Locale').getDefault().getLanguage() in ('ru', 'uk', 'be'):
    import lang.ru as lang
else:
    import lang.en as lang
GuessTheQuote.lang = lang

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

app.lang.some_phrase

Это, в общем-то, довольно тривиально, а основное, о чём я хотел рассказать в аспекте локализации — как задать константы в res/values/strings.xml и отдельных локализациях. Зачем это нужно? Как минимум, чтобы задать название приложения на разных языках, а также чтобы прописать такие константы, как app_id для сервисов Google Play и facebook_app_id для сервисов Facebook.

По-умолчанию P4A генерирует strings.xml следующего содержания:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Guess The Quote</string>
    <string name="private_version">1517538478.81</string>
    <string name="presplash_color">#2EBCB2</string>
    <string name="urlScheme">kivy</string>
</resources>

При этом название приложения и цвет фона экрана загрузки можно задать в buildozer.spec. На первый взгляд, этого достаточно, но это только в том случае, если приложение одноязычное, и дополнительные строковые константы не нужны, а это как-то минималистично :) Конечно, никто не запрещает вручную прописать всё необходимое, но при следующей сборке оно затрётся. Также можно вручную создать папки с локализациями, например, values-ru, но они при новых сборках они не будут обновляться. Поэтому лучше ещё раз подправить P4A, а именно, файл .buildozer/android/platform/build/dists/guessthequote/build.py следующим образом:

# оригинальный код, в текущей версии P4A начинается на строке 370

render(
        'strings.tmpl.xml',
        'res/values/strings.xml',
        args=args,
        url_scheme=url_scheme,
        )

# заменяем на усовершенствованный :)

local_args = {'be': argparse.Namespace(**vars(args)), 'ru': argparse.Namespace(**vars(args)), 'uk': argparse.Namespace(**vars(args))}
for key in local_args:
    local_args[key].name = u'Угадай цитату!' # ну захардкодил, да, ну не готов я пока сделать свой форк P4A и buildozer, чтобы сделать это через передачу параметра

for i in os.listdir('res'):
    if i[:6] == 'values':
        render(
            'strings.tmpl.xml',
            'res/'+i+'/strings.xml',
            args=(args if i == 'values' else local_args[i[7:10]]),
            url_scheme=url_scheme,
        )

# и ещё один фрагмент, в текущей версии P4A начиная со строки 388

with open(join(dirname(__file__), 'res',
                'values', 'strings.xml')) as fileh:
    lines = fileh.read()

with open(join(dirname(__file__), 'res',
                'values', 'strings.xml'), 'w') as fileh:
    fileh.write(re.sub(r'"private_version">[0-9\.]*<',
                        '"private_version">{}<'.format(
                            str(time.time())), lines))

# тоже заменяем на аналогичный в цикле

for i in os.listdir('res'):
    if i[:6] == 'values':
        with open(join(dirname(__file__), 'res',
                        i, 'strings.xml')) as fileh:
            lines = fileh.read()

        with open(join(dirname(__file__), 'res',
                        i, 'strings.xml'), 'w') as fileh:
            fileh.write(re.sub(r'"private_version">[0-9\.]*<',
                                '"private_version">{}<'.format(
                                    str(time.time())), lines))

Ну а все необходимые вам строковые константы нужно прописать в файле .buildozer/android/platform/build/dists/guessthequote/templates/strings.tmpl.xml

Продолжение следует


Если статья покажется сообществу интересной, во второй части я опишу самые интересные вещи: покупки в приложении, интеграцию сервисов Google Play Games и Facebook SDK, и подготовку release version с последующей публикацией в Google Play, а также подготовлю проект на Github с модулями для реализации описанных задач. Если вам интересны ещё какие-то подробности — напишите в комментариях, постараюсь по возможности осветить.

Комментарии (19)


  1. ShashkovS
    12.02.2018 08:56
    +1

    Спасибо, это ценно!
    В прошлом году я предлагал своим школьникам написать что-нибудь на kivy. И один из них в итоге даже сделал относительно успешное приложение и даже сумел залить его в магазины приложений google и apple. Но стрясти с него адаптированных инструкций мне пока не удалось :) А у самого пока руки не доходят.


  1. vin2809
    12.02.2018 09:22
    +1

    Я вообще ЗА написание приложений на Python.
    Спасибо за труд.


  1. Ormgair
    12.02.2018 09:26

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


    1. s_a_p Автор
      12.02.2018 09:34

      Мне не совсем понятно, что означает «во время работы» и зачем это может быть нужно — ведь разрешения можно запросить при установке?


      1. Ormgair
        12.02.2018 09:57

        Начиная с 6 андроида (sdk 23), а это около половины устройств, надо запрашивать некоторые разрешения при необходимости, а не при установке. Например написали Вы какой-нибудь аналог обычного блокнота, но с возможностью чтения контактов пользователя. При чтении контактов приложение должно проверить наличие разрешения и, в случае необходимости, запросить у пользователя разрешение на чтение. Это я и имел ввиду под выражением "во время работы"


        1. s_a_p Автор
          12.02.2018 09:59

          Понятно. С такой задачей пока не сталкивался, так что подсказать не могу.


  1. domix32
    12.02.2018 11:01

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


    1. s_a_p Автор
      12.02.2018 19:46
      +1

      Согласен, время запуска (особенно первого) — это действительно значимый минус. Хотя, зачастую, приложения на Unity, например, запускаются примерно столько же по времени, но никто же не говорит, что Unity так уж плох :)


  1. HeaTTheatR
    12.02.2018 12:40

    Спасибо за статью (две подряд за два дня о Kivy это, конечно, круто). Насчет создания виджетов. Здесь, как Linux меню Home: первая генерация действительно заметна для глаза. Но все последующие вызовы происходят мгновенно.


  1. HeaTTheatR
    12.02.2018 13:00

    Возможно, вы это имели в виду:
    image
    Пункт 'Плагины' ещё не открывался, и окно не было создано, поэтому видно небольшую задержку.
    Пункт 'Мои группы' уже был открыт, окно создано, поэтому открывается мгновенно.


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


    1. s_a_p Автор
      12.02.2018 19:59

      Я начал готовить свою статью за несколько дней до выхода вашей, и забавно, что и у вас, и у меня рассматривается вопрос создания виджетов и работы с RecycleView. Думаю, это говорит в пользу того, что, с одной стороны, вопрос действительно актуальный, а с другой — неочевидный: как я и писал в статье, разработчику, ещё не изучившему все тонкости, просто неоткуда узнать о том, как это сделать правильно. Ну, точнее, в случае с RecycleView теперь уже есть откуда (я не только о наших с вами статьях, я и на StackOverflow описывал свой опыт). Но очень много ещё не документированного, особенно в том, что не касается конкретно Kivy. Те же сервисы — ушло несколько дней на дебаггинг, чтобы понять в чём проблема, а будь это нормально документированно — ушло бы несколько минут. Я уже не говорю про in-app purchases, Google Play Games и Facebook SDK, которые я собираюсь описать во второй части. ИМХО именно в этом на данный момент одна из основных проблем Kivy и P4A — многие вещи сделать можно, но нигде не написано, как, а тратить своё время на глубокий дебаггинг многие не готовы — и это нормально, если уж позиционировать фреймворк как полноценный инструмент для разработки под Android. Но сам инструмент правда хороший, и поэтому я пытаюсь сделать хоть какой-то вклад в решение данной проблемы своими статьями :)


    1. s_a_p Автор
      14.02.2018 04:26

      Кстати, на счёт первой генерации и последующих: это действительно так. Однако, я, наоборот, прописываю в on_leave удаление всех динамически создающихся виджетов, и в on_pre_enter каждый раз создаю их заново. Ведь если этого не делать, созданные виджеты будут продолжать висеть в памяти (что и подтверждается вышеописанным эффектом). А если их реально много, хотя бы десятки, в итоге мы получим весьма нерациональное использование памяти. Я не прав?


      1. HeaTTheatR
        14.02.2018 10:55

        Ну, насчет памяти я не беспокоюсь, память нынче дешевая и её много :) Да и не так уж много созданный виджет её отнимает.


  1. P0rt
    12.02.2018 17:49

    Сначала стило убедиться

    опечатка, исправьте, пожалуйста.


    1. s_a_p Автор
      12.02.2018 19:48

      Спасибо, исправил.


  1. OrangeGrunge
    12.02.2018 19:49

    Спасибо за статью, друг мой! Продолжайте писать.
    Если будет возможность — осветите, пожалуйста тему распараллеливания процессов.
    Когда я делал простенькое приложение для отправки почтового сообщения, столкнулся с тем, что не так-то просто сделать корректно работающий поп-ап, уведомляющий о том, что сообщение в настоящий отправляется. Он выскакивал уже по факту отправки, хотя в коде функция его вызова была раньше, чем функция непосредственно отправки.
    В сети есть информация по этому вопросу, но я так и не разобрался: кто-то пишет, что здесь сработает threading, кто-то пишет, что нужно что-то другое.
    Было бы здорово, если бы кто-то объяснил, что да как.


    1. s_a_p Автор
      12.02.2018 20:05

      Threading + Clock должны помочь. Мне это в моём приложении, в итоге, не понадобилось, но я немного с ними игрался, и это работает. Посмотрите на StackOverflow, там есть несколько вопросов (и ответов) на эту тему.


    1. HeaTTheatR
      12.02.2018 20:53

      Можно костыли: вешаете на функцию отправки декоратор (вся работа с передачей данных на сервер должна быть реализована в потоке), вызываете функцию отправки сообщения на сервер, далее через Clock вызываете функцию, которая отслеживает получение данных функцией, которая работает с передачей данных. Это схематично. А вообще же в Python 3.6 есть async await, которые можно использовать в Kivy не прибегая к костылям, описаным выше.


    1. HeaTTheatR
      12.02.2018 21:00

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