Как стать автором
Обновить

Приложение на python kivy для разнообразия рациона питания. От кода и до получения .apk файла для Android

Время на прочтение8 мин
Количество просмотров86K

Изучаю python kivy и для себя решил написал маленькое приложение, чтобы разнообразить свое питание. Решил поделиться. Статья рассчитана на новичков в kivy. Приложение занимает около 100 строк кода.

Цель создания велосипеда приложения:

  1. Избежать частых повторений в питании. Чтобы не употреблять одно и то же блюдо слишком часто.
  2. Не забывать блюда, которые ел, потом забыл и годами к ним не возвращался, потому что банально не помнил. У меня такое бывает.

Интро


Можно не читать, в интро всякая лирика.

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

Скриншоты




Предположим мой рацион состоит из 50 блюд. Например, сегодня ел омлет. Нажимаю на кнопку, и омлет стал на 50 строку в очереди, а перед ним стоят 49 блюд, которые съем, чтобы опять добраться до омлета. Вот и вся логика приложения. (На скриншотах блюда нагенеренные, все совпадения случайны, к моему реальному рациону отношения не имеющие).

Исходный код и пояснения


main.py
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.uix.gridlayout import GridLayout
from kivy.core.window import Window
from kivy.config import ConfigParser
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
from kivy.metrics import dp
from datetime import datetime
import os
import ast
import time


class MenuScreen(Screen):
    def __init__(self, **kw):
        super(MenuScreen, self).__init__(**kw)
        box = BoxLayout(orientation='vertical')
        box.add_widget(Button(text='Дневник питания', on_press=lambda x:
                              set_screen('list_food')))
        box.add_widget(Button(text='Добавить блюдо в дневник питания',
                              on_press=lambda x: set_screen('add_food')))
        self.add_widget(box)


class SortedListFood(Screen):
    def __init__(self, **kw):
        super(SortedListFood, self).__init__(**kw)

    def on_enter(self):  # Будет вызвана в момент открытия экрана

        self.layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
        self.layout.bind(minimum_height=self.layout.setter('height'))
        back_button = Button(text='< Назад в главное меню',
                             on_press=lambda x: set_screen('menu'),
                             size_hint_y=None, height=dp(40))
        self.layout.add_widget(back_button)
        root = RecycleView(size_hint=(1, None), size=(Window.width,
                                                      Window.height))
        root.add_widget(self.layout)
        self.add_widget(root)

        dic_foods = ast.literal_eval(
            App.get_running_app().config.get('General', 'user_data'))

        for f, d in sorted(dic_foods.items(), key=lambda x: x[1]):
            fd = f.decode('u8') + ' ' + (datetime.fromtimestamp(d).strftime('%Y-%m-%d'))
            btn = Button(text=fd, size_hint_y=None, height=dp(40))
            self.layout.add_widget(btn)

    def on_leave(self):  # Будет вызвана в момент закрытия экрана

        self.layout.clear_widgets()  # очищаем список


class AddFood(Screen):

    def buttonClicked(self, btn1):
        if not self.txt1.text:
            return
        self.app = App.get_running_app()
        self.app.user_data = ast.literal_eval(
            self.app.config.get('General', 'user_data'))
        self.app.user_data[self.txt1.text.encode('u8')] = int(time.time())

        self.app.config.set('General', 'user_data', self.app.user_data)
        self.app.config.write()

        text = "Последнее добавленное блюдо:  " + self.txt1.text
        self.result.text = text
        self.txt1.text = ''

    def __init__(self, **kw):
        super(AddFood, self).__init__(**kw)
        box = BoxLayout(orientation='vertical')
        back_button = Button(text='< Назад в главное меню', on_press=lambda x:
                             set_screen('menu'), size_hint_y=None, height=dp(40))
        box.add_widget(back_button)
        self.txt1 = TextInput(text='', multiline=False, height=dp(40),
                              size_hint_y=None, hint_text="Название блюда")
        box.add_widget(self.txt1)
        btn1 = Button(text="Добавить блюдо", size_hint_y=None, height=dp(40))
        btn1.bind(on_press=self.buttonClicked)
        box.add_widget(btn1)
        self.result = Label(text='')
        box.add_widget(self.result)
        self.add_widget(box)


def set_screen(name_screen):
    sm.current = name_screen


sm = ScreenManager()
sm.add_widget(MenuScreen(name='menu'))
sm.add_widget(SortedListFood(name='list_food'))
sm.add_widget(AddFood(name='add_food'))


class FoodOptionsApp(App):
    def __init__(self, **kvargs):
        super(FoodOptionsApp, self).__init__(**kvargs)
        self.config = ConfigParser()

    def build_config(self, config):
        config.adddefaultsection('General')
        config.setdefault('General', 'user_data', '{}')

    def set_value_from_config(self):
        self.config.read(os.path.join(self.directory, '%(appname)s.ini'))
        self.user_data = ast.literal_eval(self.config.get(
            'General', 'user_data'))

    def get_application_config(self):
        return super(FoodOptionsApp, self).get_application_config(
            '{}/%(appname)s.ini'.format(self.directory))

    def build(self):
        return sm


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

Я сознательно не использовал kv файлы, так как код дан в учебных целях, для людей, которые знакомы с python. Все написано на голом python. В пояснениях я не буду останавливаться на объяснении python кода, а сразу перейду к специфическим фишкам Kivy.

Поехали:

  • class MenuScreen(Screen):

    Класс отвечает за запуск стартовой странички приложения, его можно назвать как угодно, например StartScreen. И наследует kivy модуль Screen. Приложение состоит из 3-х окон, вот эти окошки и создаются с помощью этого модуля
  • box = BoxLayout(orientation='vertical')

    BoxLayout делит экран на равные части, по умолчанию горизонтально, я написал orientation='vertical', чтобы делить вертикально
  • Button(text='Дневник питания', 
    on_press=lambda x: set_screen('list_food'))
    

    Button — создает кнопки, в on_press задается, какая функция будет запущена при нажатии.
  • .add_widget()
    — добавляет кнопки в слои и в окна
  • self.layout = GridLayout(cols=1, spacing=10, size_hint_y=None)

    Grid Layout чем-то напоминает тег table в html, указывается cols — кол-во колонок или rows — кол-во строк.

    Можно указывать оба параметра или один параметр.
    Экран будет разделен на нужное кол-во отсеков.
  • root = RecycleView(size_hint=(1, None), size=(Window.width,                                                       Window.height))

    RecycleView — модуль, с помощью которого создается вертикальная прокрутка в моем приложении. Особенность RecycleView в том, что он строит скролы с элементами одинаковой ширины и высоты. И работает быстро. А есть модуль ScrollView, он может строить прокрутки с элементами разных размеров, но работает медленнее, чем RecycleView
  • config.get('General', 'user_data')
    — в коде часто встречаются такие строки. Я просто в качестве хранилища данных использовал родное хранилище Config kivy. Ну, пусть будет несколько тысяч блюд, нет смысла городить огород с sqlite и чем-то подобным. Все данные хранятся в одном файлике. Хранится этот файлик в той же папке, что и само приложение, если указать self.directory как в моем коде, но можно указать self.user_data_dir, чтобы этот файлик не уничтожался при перестановке или обновлениях.

Запуск на windows & linux & macos


Принцип для всех операционок одинаковый:

  1. Ставим python3
  2. Ставим kivy
  3. Создаем файл main.py и втыкаем в него целиком вышеуказанный код
  4. Запускаем командой

    python3 main.py

Программа должна заработать.

Сборка apk файла и запуск на телефоне с андроид


Итак, у нас есть файл с кодом программы, написанный на python. Как теперь создать приложение, чтобы его можно было запустить на телефоне с андроидом? Раньше это был достаточно мудреный процесс, требующий навыков и танцев с бубном. Теперь это не проблема.
Вот пошаговая инструкция:

  1. Скачиваем готовую виртуальную машину от разработчиков kivy, в которой уже все настроено. https://github.com/Zen-CODE/kivybits/tree/master/KivyCompleteVM. Пароль: kivy
  2. Запускаем ее в Virtual Box.
  3. Открываем терминал и вводим следующие команды:

    # Ставим последнюю версию python-for-android
    cd /home/kivy/Repos
    rm -fr python-for-android/
    git clone https://github.com/kivy/python-for-android.git
    cd ~
    mkdir Project
    cd Project
    git clone https://github.com/Alexmod/FoodOptions.git
    cd FoodOptions
    buildozer android debug
    # Первый раз эта команда будет долго тянуть 100500 всяких библиотек,
    # но в следующие разы выполняться за секунды. 
    
  4. Последняя команда создает папку bin в той же директории, в bin вы найдете файл foodoptions-0.1-debug.apk, который можно закинуть на телефон, установить и наслаждаться приложением


Как закинуть apk файл на телефон?

Можно, конечно, сделать это как угодно, отправить себе по почте, куда-нибудь выложить, закинуть в телеграмм и т.д., а потом скачать приложение на телефон.

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

sudo apt install adb

После установки заходим в папку bin и вводим команду

adb install -r foodoptions-0.1-debug.apk 

И можно примерно через минутку увидеть на телефоне приложение после того, как увидим
Success в консоли.

kivy@kivy-complete:~/Project/FoodOptions/bin$ adb install -r foodoptions-0.1-debug.apk 
342 KB/s (10083019 bytes in 28.730s)
Success
kivy@kivy-complete:~/Project/FoodOptions/bin$ 

Если вдруг приложение падает или ведет себя не так, как ожидалось, то есть вот такая команда для просмотра ошибок

adb logcat| grep python

Русское имя приложения

Если вы захотите, чтобы ваше приложение называлось по-русски, например, «Дневник питания», то надо внести изменения в файл:

.buildozer/android/platform/build/dists/foodoptions/templates/strings.tmpl.xml

В тег appName прописывается русское название приложения, эта папка создается после первого запуска buildozer android debug. После того как файл отредактируете, вернитесь назад в папку FoodOptions и запустите buildozer android debug повторно. Файл соберется по-новой. После установки на телефон имя программы будет написано на русском.

О файле buildozer.spec

Вот мой файл с гитхаба: buildozer.spec
Именно этот файл указывает buildozer-у, как именно собрать пакет.

Там множество разных вариаций. Кому интересно, то введите внутри виртуалки команду:

cd /tmp
buildozer init

Будет создан дефолтный файл buildozer.spec с кучей комментариев и пояснений. Например, если вы хотите какую-нибудь свою иконку для приложения, то указываете в строке:

icon.filename = %(source.dir)s/data/icon.png

свой файл с иконкой. И приложение соберется уже с вашей иконкой.

Если вам надо подгрузить какой-нибудь специфический модуль, который не входит в официальную библиотеку python, то это делается в строке requirements =. В общем, рассказ о файле buildozer.spec может занять целую статью, а то и две.

Загрузка приложения в Google Play

Надо зарегаться, пройти все процедуры, получить ключи. И дальше запускать:

sudo apt install zipalign
buildozer android release
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore /path/keystore bin/apk-unsigned.apk apkname
zipalign -v 4 bin/apk-apkname-unsigned.apk bin/apk-apkname-release.apk

Полученный файл apk-apkname-release.apk заливать в Google Play.

Ссылки



В принципе любой человек, который умеет программировать на python, сможет изменить приложение и легко добавить следующее:

  1. Добавить дизайн, чтобы приложение стало красивое
  2. Использовать kv-файлы, чтобы код стал более легким. Я бы привел такую аналогию: те кто знаком с веб-программированием, представьте себе код без html темплейтов и с html темплейтами. Вынос в kv-файлы кнопок, слоев и прочего — это что-то вроде jinja2 для веб-программиста. Логика остается в .py файлах, а фенечки — в kv-файлах.
  3. Добавить подсчет калорий, белка, углеводов, жиров (БЖУ)
  4. Добавить возможность фотографировать блюда
Теги:
Хабы:
+28
Комментарии15

Публикации

Изменить настройки темы

Истории

Работа

Data Scientist
60 вакансий
Python разработчик
132 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн