[Python, Разработка под MacOS, Разработка под Linux, Разработка под Windows] Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD 
    
    
        
    
    
    
    
            
    
        
            
                
                                    
                
                                    
                
                    
                
            
        
    
    
        
            
                
                
                    
                           
                    
                        Автор 
                        Сообщение 
                    
                                        
                        
                            
                                
                                
                                                                                                            news_bot ®
                                                                        
                                                                                                                                                
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                        
                                                                                                                
                                            Сообщений: 27286                                        
                                                                                                                                                
                                                             
                            
                                
                             
                         
                        
                            
                                
                                    
                                        
                                        
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 StringProperty
from kivymd.uix.relativelayout import MDRelativeLayout
class 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 Animation
from kivy.properties import StringProperty, BooleanProperty
from kivy.core.window import Window
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors import FocusBehavior
class 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
SPL
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
SPL
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
SPL
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
SPL
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 Window
from kivy.properties import StringProperty
from FacebookDesktop.components.cards.fake_card import FakeCard
class 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
SPL
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
SPL
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 StringProperty
from kivymd.uix.relativelayout import MDRelativeLayout
class BadgeButton(MDRelativeLayout):
    icon = StringProperty()
    text = StringProperty()
И уже создать левую панель инструментов:

Даже учитывая, что мне пришлось создавать кастомные кнопки типа «badge», код левой панели инструментов на Python/Kivy получился короче — 58 строк кода, реализация на Dart/Flutter — 97 строк.
button.dart
SPL
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
SPL
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
===========
Похожие новости:
- [Python, JavaScript, C#, Логические игры] 10 лучших игр по программированию, которые улучшат ваши навыки (перевод)
 
- [.NET, Оболочки, API, C#, Разработка под Windows] WinUI 3 — Новая эра разработки под Windows
 
- [Open source, Виртуализация, Разработка под Linux, Разработка под Windows] Шпаргалка по pip, 6 заблуждений насчет AIOps, бесплатный онлайн-курс, а еще про Windows-программы на Linux
 
- [Информационная безопасность, Python, Тестирование веб-сервисов] Пишем расширение для Burp Suite с помощью Python
 
- [Python, PostgreSQL] Обрезаем большую таблицу PostgreSQL в production
 
- [Python, Программирование, Машинное обучение] Взлом reCAPTCHA v2
 
- [Python, Научно-популярное, Биотехнологии, Будущее здесь] Визуализация и анализ белков в Biopython (перевод)
 
- [Python, Microsoft Azure, Тестирование веб-сервисов, Облачные сервисы] HTTP атака на Azure
 
- [Python, Программирование, Программирование микроконтроллеров] Маленькие Python для маленьких embedded-программистов: CircuitPython и MicroPython для MeowBit
 
- [Программирование, C++, C, Разработка под Linux] Введение в неблокирующие алгоритмы (перевод)
Теги для поиска: #_python, #_razrabotka_pod_macos (Разработка под MacOS), #_razrabotka_pod_linux (Разработка под Linux), #_razrabotka_pod_windows (Разработка под Windows), #_kivy, #_kivymd, #_python, #_python, #_razrabotka_pod_macos (
Разработка под MacOS
), #_razrabotka_pod_linux (
Разработка под Linux
), #_razrabotka_pod_windows (
Разработка под Windows
)
                                        
                                        
                                        
                                     
                                    
                                    
                                                                    
                                                                                             
                         
                        
                            
                                                                    
                                                             
                         
                    
                    
                
                
            
        
    
    
    
    
    
            
    
            
    
        
    
    
        
                        Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    
    
        
        Текущее время: 31-Окт 05:45
Часовой пояс: UTC + 5 
            
    
                
| Автор | Сообщение | 
|---|---|
| news_bot ® 
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                         | |
|  Kivy и Flutter — два фреймворка с открытым исходным кодом для кроссплатформенной разработки. Flutter: 
 
 
 Kivy: 
 
 
 Недавно на просторах Ютуба наткнулся на видео демонстрацию 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 StringProperty from kivymd.uix.relativelayout import MDRelativeLayout class 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 Animation from kivy.properties import StringProperty, BooleanProperty from kivy.core.window import Window from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.behaviors import FocusBehavior class 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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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 Window from kivy.properties import StringProperty from FacebookDesktop.components.cards.fake_card import FakeCard class 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.dartSPLimport '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.dartSPLimport '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 StringProperty from kivymd.uix.relativelayout import MDRelativeLayout class BadgeButton(MDRelativeLayout): icon = StringProperty() text = StringProperty() И уже создать левую панель инструментов:  Даже учитывая, что мне пришлось создавать кастомные кнопки типа «badge», код левой панели инструментов на Python/Kivy получился короче — 58 строк кода, реализация на Dart/Flutter — 97 строк. button.dartSPLimport '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.dartSPLimport '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 =========== Похожие новости: 
 Разработка под MacOS ), #_razrabotka_pod_linux ( Разработка под Linux ), #_razrabotka_pod_windows ( Разработка под Windows ) | |
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 31-Окт 05:45
Часовой пояс: UTC + 5 
