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.