Kivy yang Berkibar. Gambaran umum tentang kapabilitas kerangka Kivy dan perpustakaan KivyMD



Kivy dan Flutter adalah dua kerangka kerja sumber terbuka untuk pengembangan lintas platform.



Berdebar:



  • Dibuat oleh Google dan dirilis pada 2017;


  • menggunakan Dart sebagai bahasa pemrograman;


  • tidak menggunakan komponen asli, menggambar seluruh antarmuka di dalam mesin grafisnya sendiri;


Kivy:



  • dibuat oleh komunitas Kivy pada tahun 2010;


  • menggunakan Python sebagai bahasa pemrograman dan bahasa deklaratifnya sendiri untuk menandai elemen UI - Bahasa KV;


  • tidak menggunakan komponen asli, menggambar seluruh antarmuka menggunakan OpenGL ES 2.0 dan SDL2;


Baru-baru ini, di ruang terbuka YouTube, saya melihat video demonstrasi aplikasi Flutter - Desain Ulang Desktop Facebook yang dibuat dengan Flutter Desktop . Aplikasi demo hebat dalam gaya desain material! Dan karena saya adalah salah satu pengembang pustaka KivyMD (sekumpulan komponen material untuk kerangka Kivy), saya bertanya-tanya betapa mudahnya membuat antarmuka yang begitu indah. Untungnya, penulis meninggalkan tautan ke repositori proyek .







Aplikasi mana di screenshot di atas yang menurut Anda ditulis menggunakan Flutter dan mana yang menggunakan Kivy? Sulit untuk langsung menjawabnya, karena tidak ada perbedaan yang mencolok. Satu-satunya hal yang langsung menarik perhatian Anda (tangkapan layar bawah) adalah masih belum ada anti-aliasing normal di Kivy. Dan ini menyedihkan, tapi tidak kritis. Kami akan membandingkan elemen individu aplikasi dan kode sumbernya dalam bahasa Dart (Flutter) dan Python / KV (Kivy).



Sekarang mari kita lihat bagaimana tampilan komponen dari dalam ...



StoryCard



Kivy



Markup kartu dalam Bahasa KV:





Kelas dasar 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)

      
      





Berdebar:



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

      
      





Seperti yang Anda lihat, kode dalam Python dan KV-Language dua kali lebih pendek. Kode sumber untuk proyek Python / Kivy yang dibahas dalam artikel ini memiliki ukuran total 31 kilobyte. 3 kilobyte dari jumlah ini adalah kode Python, sisanya adalah KV-Language. Kode sumber Flutter adalah 54 kilobyte. Namun, tampaknya tidak ada yang mengherankan - Python adalah salah satu bahasa pemrograman paling singkat di dunia.



Kami tidak akan berdebat tentang mana yang lebih baik: mendeskripsikan UI menggunakan bahasa DSL atau langsung dalam kode. Di Kivy, Anda juga bisa membuat widget Python dengan kode, tapi ini bukan solusi yang bagus.



TopBar



Berdebar:



Kivy:



Implementasi bilah ini, termasuk animasi, dengan Python / Kivy hanya membutuhkan 88 baris kode. Dart / Flutter memiliki 325 baris dan 9 kilobyte ruang disk. Mari kita lihat apa widget ini:





Logo, tiga tab, avatar, tiga tab, dan satu tab - tombol pengaturan. Implementasi tab dengan indikator animasi:





Animasi indikator dan mengubah jenis kursor mouse diimplementasikan dalam file Python di kelas dengan nama yang sama dengan aturan markup:



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)

      
      





Kami hanya akan menganimasikan lebar dan opasitas indikator tergantung pada status tombol (aktif). Status tombol diatur di kelas utama layar aplikasi:



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

      
      





Pelajari lebih lanjut tentang animasi di Kivy:



Desain Material. Membuat animasi di Kivy

Pengembangan aplikasi seluler dengan Python. Membuat animasi di Kivy. Bagian 2



Implementasi di Dart / Flutter.



Karena ada banyak kode, saya menyembunyikan semuanya di bawah spoiler:



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)



Animasi pergeseran kartu terjadi relatif terhadap widget induk (induk) saat menerima peristiwa fokus dan tidak fokus (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)

      
      







Dan kelas dasar 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")

      
      





Implementasi Python / Kivy - 60 baris kode, implementasi Dart / Flutter - 182 baris kode.



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

      
      







Tapi tidak semuanya sesederhana kelihatannya. Dalam prosesnya, saya menemukan bahwa perpustakaan KivyMD kehilangan tombol dengan tipe "lencana". Omong-omong, proyek Flutter juga menggunakan tombol khusus. Oleh karena itu, untuk membuat toolbar yang tepat, saya harus membuat tombol seperti itu sendiri.





Kelas dasar Python:



from kivy.properties import StringProperty

from kivymd.uix.relativelayout import MDRelativeLayout


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

      
      





Dan sudah membuat toolbar kiri:







Meskipun saya harus membuat tombol kustom seperti "badge", kode toolbar kiri dengan Python / Kivy ternyata lebih pendek - 58 baris kode, implementasi di Dart / Flutter - 97 baris .



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

      
      







Tentu saja, saya tidak meremehkan manfaat kerangka Flutter. Alatnya luar biasa! Saya hanya ingin menunjukkan kepada pengembang Python bahwa mereka dapat melakukan hal yang sama seperti di Flutter, tetapi dalam bahasa pemrograman favorit mereka menggunakan kerangka kerja Kivy dan pustaka KivyMD. Sedangkan untuk platform seluler, perlu disadari bahwa Flutter melampaui Kivy dalam hal kecepatan. Tapi itu artikel lain ... Tautan ke repositori Desain Ulang Desktop Facebook yang dibangun dengan proyek Flutter Desktop dalam implementasi Python / Kivy / KivyMD.






All Articles