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

  • Tutorial

Изучаю 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/blob/master/KivyCompleteVM/ReadMe.txt. Пароль: 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. Добавить возможность фотографировать блюда
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 15

    +3

    У вас код не Python 3, и вообще не pythonic. Начиная с многочисленных нарушений PEP-8, заканчивая названиями переменных btn1, txt1 и бесполезными комментариями типа # очистить список.


    Если делаете что-то для примера, то стоило бы вылизать код, тем более, что он настолько короткий.

      +1
      Замечания приняты. Простите меня лентяя.
      Если кому любопытно, то вот та же программа но уже с kv-файлами:
      https://github.com/Alexmod/FoodOptionKV
      Правда без buildozer.spec и я ее не проверял для сборки apk.
      +2

      Все-таки лучше делать разметку UI именно с помощью KV Language. Структура элементов гораздо четче прослеживается, проще вносить изменения да и код самого UI не мешается под ногами в коде. Например, вот так было бы несколько лучше:


      <ManagerScreens@ScreenManager>:
      
          MenuScreen:
              id: men
          SortedListFood:
              id: list_food
          AddFood:
              id: add_food
      
      <MenuScreen@Screen>:
          name: "menu"
      
          BoxLayout:
              orientation: 'vertical'
              Button:
                  text: "Дневник питания"
                  on_press: app.screen_manager.current = 'list_food'
              Button:
                  text: "Добавить блюдо в дневник питания"
                  on_press: app.screen_manager.current = 'add_food'
      
      <AddFood>:
          name: "add_food"
          _app: app
      
          BoxLayout:
              orientation: 'vertical'
      
              Button:
                  text: '< Назад в главное меню'
                  on_press: app.screen_manager.current = 'menu'
                  size_hint_y: None
                  height: dp(40)
              TextInput:
                  id: field_food
                  multiline: False
                  height: dp(40)
                  size_hint_y: None
                  hint_text: "Название блюда"
              Button:
                  text: "Добавить блюдо"
                  size_hint_y: None
                  height: dp(40)
                  on_press:
                      if field_food.text != '': root.button_clicked(field_food.text); \
                      field_food.text = ''
              Label:
                  id: result_label
      
      <SortedListFood>:
          name: "list_food"
      
          BoxLayout:
              orientation: 'vertical'
      
              Button:
                  text: '< Назад в главное меню'
                  on_press: app.screen_manager.current = 'menu'
                  size_hint_y: None
                  height: dp(40)
      
              RecycleView:
                  id: rv
                  key_viewclass: 'viewclass'
                  key_size: 'height'
                  RecycleBoxLayout:
                      default_size: None, dp(40)
                      default_size_hint: 1, None
                      size_hint_y: None
                      height: self.minimum_height
                      orientation: 'vertical'

      Теперь код Python содержит только логику приложения:


      import os
      import ast
      import time
      
      from datetime import datetime
      
      from kivy.app import App
      from kivy.properties import ObjectProperty
      from kivy.uix.screenmanager import Screen
      from kivy.config import ConfigParser
      from kivy.lang import Builder
      from kivy.factory import Factory
      
      Builder.load_file('ui.kv')
      
      class SortedListFood(Screen):
          def on_enter(self):
              data_foods = self.get_data_foods()
              self.set_list_foods(data_foods)
      
          def get_data_foods(self):
              return ast.literal_eval(
                  App.get_running_app().config.get('General', 'user_data'))
      
          def set_list_foods(self, data_foods):
              for f, d in sorted(data_foods.items(), key=lambda x: x[1]):
                  fd = f.decode('u8') + ' ' + (datetime.fromtimestamp(d).strftime(
                      '%Y-%m-%d'))
                  data = {'viewclass': 'Button', 'text': fd}
                  if data not in self.ids.rv.data:
                      self.ids.rv.data.append({'viewclass': 'Button', 'text': fd})
      
      class AddFood(Screen):
          _app = ObjectProperty()
      
          def set_user_data(self, input_food):
              self._app.user_data = \
                  ast.literal_eval(self._app.config.get('General', 'user_data'))
              self._app.user_data[input_food.encode('u8')] = int(time.time())
      
          def save_user_data(self):
              self._app.config.set('General', 'user_data', self._app.user_data)
              self._app.config.write()
      
          def set_new_food(self, name_food):
              self.ids.result_label.text = \
                  "Последнее добавленное блюдо:  " + name_food
      
          def button_clicked(self, input_food):
              self.set_user_data(input_food)
              self.save_user_data()
              self.set_new_food(input_food)
      
      class FoodOptionsApp(App):
          def __init__(self, **kvargs):
              super(FoodOptionsApp, self).__init__(**kvargs)
      
              self.config = ConfigParser()
              self.screen_manager = Factory.ManagerScreens()
              self.user_data = {}
      
          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 self.screen_manager
      
      if __name__ == '__main__':
          FoodOptionsApp().run()
      +2
      Простите за оффтоп, но овсянку нельзя есть каждый день на протяжении десятилетий. Она содержит фитиновую кислоту, которая препятствует усвоению кальция.
        0
        Я не знаю, возможно это и не оффтоп, так как я разместил эту статью в разделе «Здоровье гика», помимо python.
          0
          актуально для мест с очень жесткой водой, настолько жесткой что самый дорогой гейзеровский фильтр убивается дней за 10
          +3
          Безотносительно кода приложения и гастрономических пристрастий спасибо за наводку на kivy. Будет, с чем поиграть на досуге.
            +1
            Спасибо за коммент. Статья писалась как раз ради того, чтобы показать, что на kivy можно очень быстро получать кросс-платформенные приложения для Android, Windows, Linux и MacOS, а гастрономия и пример кода – это лишь образец, чтобы быстро запустить и проверить работоспособность.
            Про то как создать приложение kivy для IOS ждем статью от HeaTTheatR
            +4

            Присоединяюсь к willmore за наводку на kivy. Прекрасный учебный материал.

              +1
              Ухх, оказывается, есть люди, которым такие же мысли приходят в голову. Я уж давно делал подобное приложение, даже с обменом блюдами через сеть.
              «Чо куснуть» называется в Маркете.
              Только никому оно не нужно, без раскрутки, как собственно, обычно последние годы.
                +1
                можно так и менюшки с прайсом для кафе делать, с интерактивностью на планшетах.
                  0
                  Да, легко.
                  0
                  Подскажите, а с блютуз как он уживается? можно ли пример кода сервера и клиента голубого зуба?

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

                Самое читаемое