Русский
Русский
English
Статистика
Реклама

Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD


Kivy и Flutter два фреймворка с открытым исходным кодом для кроссплатформенной разработки.

Flutter:


  • создан компанией Google и выпущенный в 2017 году;

  • в качестве языка программирования использует Dart;

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

Kivy:


  • создан сообществом Kivy в 2010 году;

  • в качестве языка программирования использует Python и собственный декларативный язык для разметки UI элементов KV Language;

  • не использует нативные компоненты, рисуя весь интерфейс с помощью OpenGL ES 2.0 и SDL2;


Недавно на просторах Ютуба наткнулся на видео демонстрацию Flutter приложения Facebook Desktop Redesign built with Flutter Desktop. Отличное демонстрационное приложение в стиле material design! И поскольку я один из разработчиков библиотеки KivyMD (набор material компонентов для фреймворка Kivy) мне стало интересно, насколько просто будет сделать такой же красивый интерфейс. К счастью автор оставил ссылку на репозиторий проекта.






Как вы думаете, какое приложение на вышеприведенных скриншотах написано с использованием Flutter и какое с помощью Kivy? Ответить сходу трудно, поскольку ярко выраженных отличий нет. Единственное, что сразу бросается в глаза (нижний скриншот) в Kivy все еще нет нормального сглаживания. И это грустно, но не критично. Сравнивать мы будем отдельные элементы приложения и их исходный код на Dart (Flutter) и Python/KV language (Kivy).

Посмотрим теперь как выглядят компоненты изнутри

StoryCard


Kivy


Разметка карточки на языке KV-Language:


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass StoryCard(MDRelativeLayout):    avatar = StringProperty()    story = StringProperty()    name = StringProperty()    def on_parent(self, *args):        if not self.avatar:            self.remove_widget(self.ids.avatar)

Flutter:


import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class Story extends StatefulWidget {  final String name;  final String avatar;  final String story;  const Story({    Key key,    this.name,    this.avatar,    this.story,  }) : super(key: key);  @override  _StoryState createState() => _StoryState();}class _StoryState extends State<Story> {  @override  Widget build(BuildContext context) {    return Container(      width: 150,      margin: const EdgeInsets.only(top: 30),      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(30),        boxShadow: [          BoxShadow(            color: Colors.black.withOpacity(0.3),            blurRadius: 20,            offset: Offset(0, 10),          ),        ],      ),      child: Stack(        overflow: Overflow.visible,        fit: StackFit.expand,        children: [          ClipRRect(            borderRadius: BorderRadius.circular(30),            child: Image.network(              widget.story,              fit: BoxFit.cover,            ),          ),          if (widget.avatar != null)            Positioned.fill(              top: -30,              child: Align(                alignment: Alignment.topCenter,                child: Container(                  decoration: BoxDecoration(                    borderRadius: BorderRadius.circular(30),                    boxShadow: [                      BoxShadow(                        color: Colors.black.withOpacity(0.4),                        blurRadius: 5,                        offset: Offset(0, 3),                      ),                    ],                  ),                  child: ClipRRect(                    borderRadius: BorderRadius.circular(30),                    child: Image.network(                      widget.avatar,                      fit: BoxFit.cover,                      width: 60,                      height: 60,                    ),                  ),                ),              ),            ),          if (widget.avatar != null)            Positioned.fill(              child: Align(                alignment: Alignment.bottomCenter,                child: Row(                  children: [                    Expanded(                      child: Container(                        padding: const EdgeInsets.all(15),                        decoration: BoxDecoration(                          borderRadius: BorderRadius.circular(30),                          gradient: LinearGradient(                            begin: Alignment.topCenter,                            end: Alignment.bottomCenter,                            colors: [                              Colors.transparent,                              Colors.black,                            ],                          ),                        ),                        child: widget.name != null ? Text(                          widget.name,                          textAlign: TextAlign.center,                          maxLines: 1,                          overflow: TextOverflow.ellipsis,                          style: TextStyle(                            color: Colors.white,                            fontWeight: FontWeight.bold,                          ),                        ) : SizedBox(),                      ),                    ),                  ],                ),              ),            ),        ],      ),    );  }}

Как видим, код на Python и KV-Language получается вдвое короче. Исходный код проекта на Python/Kivy, который рассматривается в этой статье, имеет общий размер 31 килобайт. 3 килобайта из этого объема приходится на Python код, остальное KV-Language. Исходный код на Flutter 54 килобайт. Впрочем, здесь удивляться, кажется, нечему Python один их самый лаконичных языков программирования в мире.

Мы не будем спорить о том, что лучше: описывать UI при помощи DSL языков или прямо в коде. В Kivy, кстати, также можно строить виджеты Python кодом, но это не очень хорошее решение.

TopBar


Flutter:

Kivy:

Реализация этого бара, включая анимацию, на Python/Kivy заняла всего 88 строчек кода. На Dart/Flutter 325 строк и 9 килобайт на диске. Посмотрим, что представляет из себя этот виджет:


Лого, три таба, аватар, три таба и один таб кнопка настроек. Реализация таба с анимированным индикатором:



Анимация индикатора и смена типа курсора мыши реализована в Python файле в одноименном с правилом разметки классе:

from kivy.animation import Animationfrom kivy.properties import StringProperty, BooleanPropertyfrom kivy.core.window import Windowfrom kivymd.uix.boxlayout import MDBoxLayoutfrom kivymd.uix.behaviors import FocusBehaviorclass Tab(FocusBehavior, MDBoxLayout):    icon = StringProperty()    active = BooleanProperty(False)    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")    def on_active(self, instance, value):        Animation(            opacity=value,            width=self.width if value else 0,            d=0.25,            t="in_sine" if value else "out_sine",        ).start(self.ids.separator)

Мы просто анимируем ширину и opacity индикатора в зависимости от состояния кнопки (active). Состояние кнопки устанавливается в главном классе экрана приложения:

class FacebookDesktop(ThemableBehavior, MDScreen):    def set_active_tab(self, instance_tab):        for widget in self.ids.tab_box.children:            if issubclass(widget.__class__, MDBoxLayout):                if widget == instance_tab:                    widget.active = True                else:                    widget.active = False

Подробнее об анимации а Kivy:

Материальный дизайн. Создание анимаций в Kivy
Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

Реализация на Dart/Flutter.

Поскольку кода очень много, я спрятал все под спойлеры:

app_logo.dart
import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class AppLogo extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(10),        boxShadow: [          BoxShadow(            color: Colors.blue.withOpacity(.6),            blurRadius: 5,            spreadRadius: 1,          ),        ],      ),      child: ClipRRect(        borderRadius: BorderRadius.circular(10),        child: Image.asset(          'assets/images/facebook_logo.jpg',          width: 30,          height: 30,        ),      ),    );  }}


avatar.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarAvatar extends StatefulWidget {  @override  _TopBarAvatarState createState() => _TopBarAvatarState();}class _TopBarAvatarState extends State<TopBarAvatar>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.4),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return MouseRegion(      onHover: (event) {        setState(() {          _animationController.forward();        });      },      onExit: (event) {        setState(() {          _animationController.reverse();        });      },      cursor: SystemMouseCursors.click,      child: Padding(        padding: const EdgeInsets.symmetric(horizontal: 15),        child: Container(          decoration: BoxDecoration(            borderRadius: BorderRadius.circular(15),            boxShadow: [              BoxShadow(                color: _animation.value,                blurRadius: 10,                spreadRadius: 0,              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(15),            child: Image.asset(              'assets/images/avatar.jpg',              width: 50,              height: 50,            ),          ),        ),      ),    );  }}


button.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarButton extends StatefulWidget {  final IconData icon;  final bool isActive;  final Function onTap;  const TopBarButton({    Key key,    this.icon,    this.isActive = false,    this.onTap,  }) : super(key: key);  @override  _TopBarButtonState createState() => _TopBarButtonState();}class _TopBarButtonState extends State<TopBarButton>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.6),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  void didUpdateWidget(TopBarButton oldWidget) {    if (widget.isActive) {      _animationController.forward();    } else {      _animationController.reverse();    }    super.didUpdateWidget(oldWidget);  }  @override  Widget build(BuildContext context) {    return GestureDetector(      onTap: widget.onTap,      child: MouseRegion(        cursor: SystemMouseCursors.click,        child: Container(          height: 80,          child: Stack(            alignment: Alignment.center,            children: [              Padding(                padding: const EdgeInsets.symmetric(horizontal: 30),                child: Icon(                  widget.icon,                  color: _animation.value,                ),              ),              Positioned(                bottom: -1,                child: Align(                  alignment: Alignment.bottomCenter,                  child: AnimatedContainer(                    duration: Duration(milliseconds: 50),                    curve: Curves.easeInOut,                    decoration: BoxDecoration(                      color: _animation.value,                      borderRadius: BorderRadius.circular(5),                      boxShadow: [                        BoxShadow(                          color: _animation.value,                          blurRadius: 5,                          offset: Offset(0, 2),                        ),                      ],                    ),                    width: widget.isActive ? 50 : 0,                    height: 4,                  ),                ),              ),            ],          ),        ),      ),    );  }  @override  void dispose() {    _animationController.dispose();    super.dispose();  }}


widget.dart
import 'package:facebook_desktop/screens/home/components/top_bar/app_logo.dart';import 'package:facebook_desktop/screens/home/components/top_bar/avatar.dart';import 'package:facebook_desktop/screens/home/components/top_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class TopBar extends StatefulWidget {  @override  _TopBarState createState() => _TopBarState();}class _TopBarState extends State<TopBar> {  int _selectedPage = 0;  @override  Widget build(BuildContext context) {    return Container(      color: Colors.white,      padding: const EdgeInsets.symmetric(        horizontal: 30,      ),      child: Row(        children: [          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerLeft,              child: AppLogo(),            ),          ),          Expanded(            flex: 6,            child: Row(              mainAxisAlignment: MainAxisAlignment.center,              children: [                TopBarButton(                  icon: FeatherIcons.home,                  isActive: _selectedPage == 0,                  onTap: () {                    setState(() {                      _selectedPage = 0;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.youtube,                  isActive: _selectedPage == 1,                  onTap: () {                    setState(() {                      _selectedPage = 1;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.grid,                  isActive: _selectedPage == 2,                  onTap: () {                    setState(() {                      _selectedPage = 2;                    });                  },                ),                TopBarAvatar(),                TopBarButton(                  icon: FeatherIcons.users,                  isActive: _selectedPage == 3,                  onTap: () {                    setState(() {                      _selectedPage = 3;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.zap,                  isActive: _selectedPage == 4,                  onTap: () {                    setState(() {                      _selectedPage = 4;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.smile,                  isActive: _selectedPage == 5,                  onTap: () {                    setState(() {                      _selectedPage = 5;                    });                  },                ),              ],            ),          ),          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerRight,              child: IconButton(                color: Colors.grey.withOpacity(.6),                icon: Icon(FeatherIcons.settings),                onPressed: () {},              ),            ),          ),        ],      ),    );  }}


ChatCard (Kivy, Flutter)

Анимация сдвига карточки происходит относительно родительского виджета (parent) при получении событий фокуса и анфокуса (on_enter, on_leave):

on_enter: Animation(x=root.parent.x + dp(12), d=0.4, t="out_cubic").start(root)on_leave: Animation(x=root.parent.x + dp(24), d=0.4, t="out_cubic").start(root)


И базовый класс Python:

from kivy.core.window import Windowfrom kivy.properties import StringPropertyfrom FacebookDesktop.components.cards.fake_card import FakeCardclass ChatCard(FakeCard):    avatar = StringProperty()    text = StringProperty()    name = StringProperty()    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")

Реализация Python/Kivy 60 строк кода, реализация Dart/Flutter 182 строки кода.

chat_card.dart
import 'package:ezanimation/ezanimation.dart';import 'package:facebook_desktop/components/user_tile.dart';import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class ChatCard extends StatefulWidget {  final String image;  final String name;  final String message;  final EdgeInsets padding;  const ChatCard({    Key key,    this.image,    this.name,    this.message,    this.padding,  }) : super(key: key);  @override  _ChatCardState createState() => _ChatCardState();}class _ChatCardState extends State<ChatCard> {  EzAnimation _animation;  @override  void initState() {    _animation = EzAnimation(      0.0,      -5.0,      Duration(milliseconds: 200),      curve: Curves.easeInOut,      context: context,    );    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return Transform.translate(      offset: Offset(_animation.value, 0),      child: MouseRegion(        cursor: SystemMouseCursors.click,        onEnter: (event) {          _animation.start();        },        onExit: (event) {          _animation.reverse();        },        child: Padding(          padding: widget.padding ?? const EdgeInsets.all(15),          child: Container(            width: 250,            padding: const EdgeInsets.all(15),            decoration: BoxDecoration(              color: Colors.white,              borderRadius: BorderRadius.circular(10),              boxShadow: [                BoxShadow(                  color: Colors.black.withOpacity(.1),                  blurRadius: 15,                  offset: Offset(0, 8),                ),              ],            ),            child: Column(              crossAxisAlignment: CrossAxisAlignment.start,              children: [                UserTile(                  name: widget.name,                  image: widget.image,                  trailing: Icon(                    FeatherIcons.messageSquare,                    color: Colors.blue,                    size: 14,                  ),                ),                SizedBox(                  height: 10,                ),                Text(                  widget.message,                  style: TextStyle(color: Colors.grey, fontSize: 12),                  maxLines: 3,                  overflow: TextOverflow.ellipsis,                ),              ],            ),          ),        ),      ),    );  }  @override  void dispose() {    _animation.dispose();    super.dispose();  }}


user_tile.dart
import 'package:facebook_desktop/screens/home/components/section.dart';import 'package:flutter/material.dart';class UserTile extends StatelessWidget {  final String name;  final String image;  final Widget trailing;  const UserTile({    Key key,    this.name,    this.image,    this.trailing,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return Row(      crossAxisAlignment: CrossAxisAlignment.start,      children: [        Container(          margin: const EdgeInsets.only(right: 10),          decoration: BoxDecoration(            color: Colors.white,            borderRadius: BorderRadius.circular(10),            boxShadow: [              BoxShadow(                color: Colors.black.withOpacity(.1),                blurRadius: 5,                offset: Offset(0, 2),              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(5),            child: Image(              image: NetworkImage(                image,              ),              fit: BoxFit.cover,              height: 50,              width: 50,            ),          ),        ),        Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: [            SectionTitle(              title: name,            ),            SizedBox(              height: 5,            ),            Text(              '12 min ago',              style: TextStyle(color: Colors.grey),            ),          ],        ),        if (trailing != null)        Expanded(          child: Align(            alignment: Alignment.topRight,            child: trailing          ),        ),      ],    );  }}


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


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass BadgeButton(MDRelativeLayout):    icon = StringProperty()    text = StringProperty()

И уже создать левую панель инструментов:



Даже учитывая, что мне пришлось создавать кастомные кнопки типа badge, код левой панели инструментов на Python/Kivy получился короче 58 строк кода, реализация на Dart/Flutter 97 строк.

button.dart
import 'package:flutter/material.dart';class LeftBarButton extends StatelessWidget {  final IconData icon;  final String badge;  const LeftBarButton({    Key key,    this.icon,    this.badge,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return GestureDetector(      child: Stack(        children: [          Container(            padding: const EdgeInsets.all(10),            child: Icon(              icon,              color: Colors.grey.withOpacity(.6),            ),          ),          if (badge != null)            Positioned(              top: 5,              right: 2,              child: Container(                padding: const EdgeInsets.all(3),                decoration: BoxDecoration(                  borderRadius: BorderRadius.circular(100),                  color: Colors.blue,                ),                child: Text(                  badge,                  style: TextStyle(                    color: Colors.white,                    fontSize: 10,                  ),                ),              ),            )        ],      ),    );  }}


widget.dart
import 'package:facebook_desktop/screens/home/left_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class LeftBar extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      margin: const EdgeInsets.all(30),      padding: const EdgeInsets.all(5),      decoration: BoxDecoration(        color: Colors.white,        borderRadius: BorderRadius.circular(50),        boxShadow: [          BoxShadow(            color: Colors.grey.withOpacity(.1),            blurRadius: 2,            offset: Offset(0, 4),          )        ],      ),      child: Column(        mainAxisSize: MainAxisSize.min,        children: [          LeftBarButton(            icon: FeatherIcons.mail,            badge: '10',          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.search,          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.bell,            badge: '20',          ),        ],      ),    );  }}


Безусловно я не умаляю достоинств фреймворка Flutter. Инструмент замечательный! Я всего лишь хотел показать Python разработчикам, что они могут делать те же самые вещи, что и во Flutter, но на их любимом языке программирования с помощью фреймворка Kivy и библиотеки KivyMD. Что касается мобильных платформ, то здесь стоит признать, что Flutter превосходит Kivy в скорости работы. Но это уже уже другая статья Ссылка на репозиторий проекта Facebook Desktop Redesign built with Flutter Desktop в реализации Python/Kivy/KivyMD.

Источник: habr.com
К списку статей
Опубликовано: 12.03.2021 12:15:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Разработка под macos

Разработка под linux

Разработка под windows

Kivy

Kivymd

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru