Kivy et Flutter sont deux frameworks open source pour le développement multiplateforme.
Battement:
- Créé par Google et publié en 2017;
- utilise Dart comme langage de programmation;
- n'utilise pas de composants natifs, dessinant toute l'interface dans son propre moteur graphique;
Kivy:
- créé par la communauté Kivy en 2010;
- utilise Python comme langage de programmation et son propre langage déclaratif pour le balisage des éléments de l'interface utilisateur - KV Language;
- n'utilise pas de composants natifs, dessinant toute l'interface en utilisant OpenGL ES 2.0 et SDL2;
Récemment, sur les espaces ouverts de YouTube, je suis tombé sur une démonstration vidéo de l'application Flutter - Facebook Desktop Redesign construit avec Flutter Desktop . Excellente application de démonstration dans le style de conception matérielle! Et comme je suis l'un des développeurs de la bibliothèque KivyMD (un ensemble de composants matériels pour le framework Kivy), je me suis demandé à quel point il serait facile de créer une si belle interface. Heureusement, l'auteur a laissé un lien vers le référentiel du projet .
Selon vous, quelle application dans les captures d'écran ci-dessus est écrite avec Flutter et laquelle utilise Kivy? Il est difficile de répondre tout de suite, car il n'y a pas de différences marquées. La seule chose qui attire immédiatement votre attention (capture d'écran du bas) est qu'il n'y a toujours pas d'anti-aliasing normal dans Kivy. Et c'est triste, mais pas critique. Nous comparerons les éléments individuels de l'application et leur code source en langage Dart (Flutter) et Python / KV (Kivy).
Voyons maintenant à quoi ressemblent les composants de l'intérieur ...
StoryCard
Kivy
Balisage de la carte en langage KV:
Classe Python de 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)
Battement:
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(),
),
),
],
),
),
),
],
),
);
}
}
Comme vous pouvez le voir, le code en Python et KV-Language est deux fois plus court. Le code source du projet Python / Kivy abordé dans cet article a une taille totale de 31 kilo-octets. 3 kilo-octets de ce montant sont du code Python, le reste est du langage KV. Le code source de Flutter est de 54 kilo-octets. Cependant, il ne semble pas y avoir de quoi être surpris - Python est l'un des langages de programmation les plus laconiques au monde.
Nous ne discuterons pas de ce qui est le mieux: décrire l'interface utilisateur en utilisant des langages DSL ou directement dans le code. Au fait, dans Kivy, vous pouvez également créer des widgets Python avec du code, mais ce n'est pas une très bonne solution.
Barre du haut
Battement:
Kivy:
L'implémentation de cette barre, y compris l'animation, en Python / Kivy n'a pris que 88 lignes de code. Dart / Flutter a 325 lignes et 9 kilo-octets d'espace disque. Voyons ce qu'est ce widget:
Logo, trois onglets, un avatar, trois onglets et un onglet - le bouton des paramètres. Mise en place d'un onglet avec un indicateur animé:
L'animation de l'indicateur et le changement du type du curseur de la souris sont implémentés dans un fichier Python dans la classe du même nom avec la règle de balisage:
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)
Nous animerons simplement la largeur et l'opacité de l'indicateur en fonction de l'état du bouton (actif). L'état du bouton est défini dans la classe principale de l'écran de l'application:
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
En savoir plus sur l'animation dans Kivy:
Material Design. Création d'animations dans Kivy
Développement d'applications mobiles en Python. Création d'animations dans Kivy. Partie 2
Implémentation dans Dart / Flutter.
Comme il y a beaucoup de code, j'ai tout caché sous des 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,
),
),
),
),
);
}
}
button.dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class TopBarButton extends StatefulWidget {
final IconData icon;
final bool isActive;
final Function onTap;
const TopBarButton({
Key key,
this.icon,
this.isActive = false,
this.onTap,
}) : super(key: key);
@override
_TopBarButtonState createState() => _TopBarButtonState();
}
class _TopBarButtonState extends State<TopBarButton>
with SingleTickerProviderStateMixin {
Animation<Color> _animation;
AnimationController _animationController;
@override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 150),
);
_animation = ColorTween(
begin: Colors.grey.withOpacity(.6),
end: Colors.blue.withOpacity(.6),
).animate(_animationController);
_animation.addListener(() {
setState(() {});
});
super.initState();
}
@override
void didUpdateWidget(TopBarButton oldWidget) {
if (widget.isActive) {
_animationController.forward();
} else {
_animationController.reverse();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
height: 80,
child: Stack(
alignment: Alignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Icon(
widget.icon,
color: _animation.value,
),
),
Positioned(
bottom: -1,
child: Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: Duration(milliseconds: 50),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: _animation.value,
borderRadius: BorderRadius.circular(5),
boxShadow: [
BoxShadow(
color: _animation.value,
blurRadius: 5,
offset: Offset(0, 2),
),
],
),
width: widget.isActive ? 50 : 0,
height: 4,
),
),
),
],
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
widget.dart
import 'package:facebook_desktop/screens/home/components/top_bar/app_logo.dart';
import 'package:facebook_desktop/screens/home/components/top_bar/avatar.dart';
import 'package:facebook_desktop/screens/home/components/top_bar/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class TopBar extends StatefulWidget {
@override
_TopBarState createState() => _TopBarState();
}
class _TopBarState extends State<TopBar> {
int _selectedPage = 0;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 30,
),
child: Row(
children: [
Expanded(
flex: 1,
child: Align(
alignment: Alignment.centerLeft,
child: AppLogo(),
),
),
Expanded(
flex: 6,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TopBarButton(
icon: FeatherIcons.home,
isActive: _selectedPage == 0,
onTap: () {
setState(() {
_selectedPage = 0;
});
},
),
TopBarButton(
icon: FeatherIcons.youtube,
isActive: _selectedPage == 1,
onTap: () {
setState(() {
_selectedPage = 1;
});
},
),
TopBarButton(
icon: FeatherIcons.grid,
isActive: _selectedPage == 2,
onTap: () {
setState(() {
_selectedPage = 2;
});
},
),
TopBarAvatar(),
TopBarButton(
icon: FeatherIcons.users,
isActive: _selectedPage == 3,
onTap: () {
setState(() {
_selectedPage = 3;
});
},
),
TopBarButton(
icon: FeatherIcons.zap,
isActive: _selectedPage == 4,
onTap: () {
setState(() {
_selectedPage = 4;
});
},
),
TopBarButton(
icon: FeatherIcons.smile,
isActive: _selectedPage == 5,
onTap: () {
setState(() {
_selectedPage = 5;
});
},
),
],
),
),
Expanded(
flex: 1,
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
color: Colors.grey.withOpacity(.6),
icon: Icon(FeatherIcons.settings),
onPressed: () {},
),
),
),
],
),
);
}
}
ChatCard (Kivy, Flutter)
|
|
L'animation du décalage de la carte se produit par rapport au widget parent (parent) lors de la réception du focus et des événements non focalisés (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)
Et la classe Python de base:
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")
Implémentation Python / Kivy - 60 lignes de code, implémentation Dart / Flutter - 182 lignes de code.
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
),
),
],
);
}
}
Mais tout n'est pas aussi simple qu'il y paraît. Au cours du processus, j'ai découvert qu'il manquait des boutons de type «badge» dans la bibliothèque KivyMD. À propos, le projet Flutter utilisait également des boutons personnalisés. Par conséquent, pour créer la bonne barre d'outils, j'ai dû créer moi-même de tels boutons.
Classe Python de base:
from kivy.properties import StringProperty
from kivymd.uix.relativelayout import MDRelativeLayout
class BadgeButton(MDRelativeLayout):
icon = StringProperty()
text = StringProperty()
Et déjà créer la barre d'outils de gauche:
même en considérant que je devais créer des boutons personnalisés comme "badge", le code de la barre d'outils de gauche en Python / Kivy s'est avéré être plus court - 58 lignes de code, implémentation dans Dart / Flutter - 97 lignes .
button.dart
import 'package:flutter/material.dart';
class LeftBarButton extends StatelessWidget {
final IconData icon;
final String badge;
const LeftBarButton({
Key key,
this.icon,
this.badge,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Stack(
children: [
Container(
padding: const EdgeInsets.all(10),
child: Icon(
icon,
color: Colors.grey.withOpacity(.6),
),
),
if (badge != null)
Positioned(
top: 5,
right: 2,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Colors.blue,
),
child: Text(
badge,
style: TextStyle(
color: Colors.white,
fontSize: 10,
),
),
),
)
],
),
);
}
}
widget.dart
import 'package:facebook_desktop/screens/home/left_bar/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
class LeftBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(30),
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(.1),
blurRadius: 2,
offset: Offset(0, 4),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
LeftBarButton(
icon: FeatherIcons.mail,
badge: '10',
),
SizedBox(
height: 5,
),
LeftBarButton(
icon: FeatherIcons.search,
),
SizedBox(
height: 5,
),
LeftBarButton(
icon: FeatherIcons.bell,
badge: '20',
),
],
),
);
}
}
Bien sûr, je ne minimise pas les mérites du cadre Flutter. L'outil est merveilleux! Je voulais juste montrer aux développeurs Python qu'ils peuvent faire les mêmes choses que dans Flutter, mais dans leur langage de programmation préféré en utilisant le framework Kivy et la bibliothèque KivyMD. En ce qui concerne les plates-formes mobiles, il convient ici de reconnaître que Flutter surpasse Kivy en termes de vitesse. Mais c'est un autre article ... Lien vers le référentiel de la refonte du bureau Facebook construit avec le projet Flutter Desktop dans l'implémentation Python / Kivy / KivyMD.