Kivy esvoaçante. Visão geral dos recursos da estrutura Kivy e da biblioteca KivyMD



Kivy e Flutter são duas estruturas de código aberto para desenvolvimento de plataforma cruzada.



Flutter:



  • Criado pelo Google e lançado em 2017;


  • usa Dart como linguagem de programação;


  • não usa componentes nativos, desenhando toda a interface dentro de seu próprio motor gráfico;


Kivy:



  • criado pela comunidade Kivy em 2010;


  • usa Python como linguagem de programação e sua própria linguagem declarativa para marcar elementos de interface do usuário - KV Language;


  • não usa componentes nativos, desenhando toda a interface usando OpenGL ES 2.0 e SDL2;


Recentemente, nos espaços abertos do YouTube, me deparei com uma demonstração em vídeo do aplicativo Flutter - Facebook Desktop Redesign construído com Flutter Desktop . Excelente aplicativo de demonstração em estilo design de material! E como sou um dos desenvolvedores da biblioteca KivyMD (um conjunto de componentes materiais para a estrutura Kivy), me perguntei como seria fácil fazer uma interface tão bonita. Felizmente, o autor deixou um link para o repositório do projeto .







Qual aplicativo nas imagens acima você acha que foi escrito usando Flutter e qual deles está usando Kivy? É difícil responder de imediato, uma vez que não existem diferenças pronunciadas. A única coisa que imediatamente chama sua atenção (imagem inferior) é que ainda não há anti-aliasing normal no Kivy. E isso é triste, mas não crítico. Compararemos elementos individuais do aplicativo e seu código-fonte na linguagem Dart (Flutter) e Python / KV (Kivy).



Agora vamos ver como os componentes se parecem por dentro ...



StoryCard



Kivy



Marcação de cartão em KV-Language:





Classe Python base:



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(),
                      ),
                    ),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}

      
      





Como você pode ver, o código em Python e KV-Language é duas vezes mais curto. O código-fonte do projeto Python / Kivy discutido neste artigo tem um tamanho total de 31 kilobytes. 3 kilobytes desta quantidade são código Python, o resto é KV-Language. O código-fonte do Flutter é de 54 kilobytes. No entanto, não parece haver nada para se surpreender - Python é uma das linguagens de programação mais lacônicas do mundo.



Não discutiremos o que é melhor: descrever a IU usando linguagens DSL ou diretamente no código. A propósito, no Kivy também é possível construir widgets Python com código, mas essa não é uma solução muito boa.



Barra superior



Flutter:



Kivy:



A implementação desta barra, incluindo animação, em Python / Kivy levou apenas 88 linhas de código. Dart / Flutter tem 325 linhas e 9 kilobytes de espaço em disco. Vamos ver o que é este widget:





Logo, três guias, um avatar, três guias e uma guia - o botão de configurações. Implementação de uma guia com um indicador animado:





A animação do indicador e a mudança do tipo do cursor do mouse são implementadas em um arquivo Python na classe de mesmo nome com a regra de marcação:



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)

      
      





Vamos simplesmente animar a largura e a opacidade do indicador dependendo do estado do botão (ativo). O estado do botão é definido na classe principal da tela do aplicativo:



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

      
      





Saiba mais sobre animação em Kivy:



Material Design. Criação de animações em Kivy

Desenvolvimento de aplicativos mobile em Python. Criação de animações em Kivy. Parte 2:



Implementação em Dart / Flutter.



Como há muito código, escondi tudo sob spoilers:



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,
            ),
          ),
        ),
      ),
    );
  }
}

      
      







botão.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)



A animação da mudança do cartão ocorre em relação ao widget pai (pai) ao receber eventos de foco e desfocados (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)

      
      







E a classe base do 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")

      
      





Implementação Python / Kivy - 60 linhas de código, implementação Dart / Flutter - 182 linhas de código.



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
          ),
        ),
      ],
    );
  }
}

      
      







Mas nem tudo é tão simples quanto parece. No processo, descobri que faltavam botões com o tipo "emblema" na biblioteca KivyMD. A propósito, o projeto Flutter também usou botões personalizados. Portanto, para criar a barra de ferramentas certa, tive que fazer esses botões sozinho.





Classe Python base:



from kivy.properties import StringProperty

from kivymd.uix.relativelayout import MDRelativeLayout


class BadgeButton(MDRelativeLayout):
    icon = StringProperty()
    text = StringProperty()

      
      





E já crie a barra de ferramentas esquerda:







Mesmo considerando que tive que criar botões customizados como "badge", o código da barra de ferramentas esquerda em Python / Kivy acabou sendo mais curto - 58 linhas de código, implementação em Dart / Flutter - 97 linhas .



botão.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',
          ),
        ],
      ),
    );
  }
}

      
      







Claro, não estou menosprezando os méritos da estrutura Flutter. A ferramenta é maravilhosa! Eu só queria mostrar aos desenvolvedores Python que eles podem fazer as mesmas coisas que no Flutter, mas em sua linguagem de programação favorita usando a estrutura Kivy e a biblioteca KivyMD. Quanto às plataformas móveis, aqui vale reconhecer que o Flutter supera o Kivy em termos de velocidade. Mas isso é outro artigo ... Link para o repositório do Facebook Desktop Redesign construído com o projeto Flutter Desktop na implementação Python / Kivy / KivyMD.






All Articles