Flutter Effects – Membuat Download Button

Untuk teman-teman yang akan baru memulai belajar Flutter silahkan masuk ke artikel ini dulu ya teman-teman ➡ Aplikasi Pertamaku “Halo Semuaaa…“. Jika sudah yuk lanjut baca artikel ini…

Jangan lupa baca artikel sebelumnya ya teman-temanPenggunaan Widget TabBar


Pada artikel ini kita akan membuat button yang memicu proses download, menampilkan proses download dan lalu memberi akses ke aset yang telah selesai di download. Hal tersebut sangat membantu untuk menunjukan ke pengguna kemajuan dari proses download yang dilakukan dan proses tersebut akan ditampilkan secara visual pada button itu sendiri berdasarkan status download aplikasi. Berikut langkah-langkahnya:

1. Mendefinisikan Widget Stateful Baru

Widget button yang kita buat membutuhkan perubahan visual dari waktu ke waktu selama proses download terjadi. Oleh karena itu, kita butuh mengimplementasikan custom widget stateful pada button tersebut.

Definisikan widget stateful baru yang diberi nama DownloadButton.

@immutable
class DownloadButton extends StatefulWidget {
 const DownloadButton({
   Key? key,
 }) : super(key: key);

 @override
 _DownloadButtonState createState() => _DownloadButtonState();
}

class _DownloadButtonState extends State<DownloadButton> {
 @override
 Widget build(BuildContext context) {
   // TODO:
   return SizedBox();
 }
}

2. Mendefinisikan Button Menampilkan Perkiraan Status

Presentasi yang ditampilkan secara visual oleh button akan berdasarkan pada status download yang sedang berjalan. Dalam hal ini, kita akan mendefinisikan perkiraan status download, lalu memperbaharui DownloadButton yang selanjutnya diubah ke  DownloadStatus dan menetukan tampilan Duration berapa lama status download tersebut bergerak dari mulai sampai selesai.

enum DownloadStatus {
 notDownloaded,
 fetchingDownload,
 downloading,
 downloaded,
}

@immutable
class DownloadButton extends StatefulWidget {
 const DownloadButton({
   Key? key,
   required this.status,
   this.transitionDuration = const Duration(milliseconds: 500),
 }) : super(key: key);

 final DownloadStatus status;
 final Duration transitionDuration;

 @override
 _DownloadButtonState createState() => _DownloadButtonState();
}

3. Menampilkan Button Shape

Selanjutnya kita akan membuat visual download button berubah bentuk ke status download. Ketika status button notDownloaded dan downloaded, Button akan berbentuk persegi panjang berwarna abu-abu. Kemudian ketika button statusnya fetchingDownload dan downloading, maka button akan berbentuk lingkaran transparan.

Berdasarkan DownloadStatus terkini, buat sebuah AnimatedContainer dengan ShapeDecoration yang menampilkan persegi panjang bulat atau lingkaran.

Kita harus pertimbangkan saat mendefinisikan pohon widget shape’s di dalam method lokal _buildXXXX() sehingga method build() tetap dalam bentuk sederhana dan mudah untuk dimodifikasi. Selain itu konfigurasi pohon widget shape agar menerima widget child, sehingga dapat kita gunakan untuk menampilkan teks pada langkah selanjutnya.

class _DownloadButtonState extends State<DownloadButton> {
 bool get _isDownloading => widget.status == DownloadStatus.downloading;

 bool get _isFetching => widget.status == DownloadStatus.fetchingDownload;

 bool get _isDownloaded => widget.status == DownloadStatus.downloaded;

 @override
 Widget build(BuildContext context) {
   return _buildButtonShape(
     child: SizedBox(),
   );
 }

 Widget _buildButtonShape({
   required Widget child,
 }) {
   return AnimatedContainer(
     duration: widget.transitionDuration,
     curve: Curves.ease,
     width: double.infinity,
     decoration: _isDownloading || _isFetching
         ? ShapeDecoration(
             shape: const CircleBorder(),
             color: Colors.white.withOpacity(0.0),
           )
         : const ShapeDecoration(
             shape: StadiumBorder(),
             color: CupertinoColors.lightBackgroundGray,
           ),
     child: child,
   );
 }
}

Dari cupllikan kode di atas mungkin teman-teman bertanya-bertanya mengapa kita membutuhkan widget ShapeDecoration pada lingkaran transparan, itu dikarenakan lingkaran transparan tersebut tidak terlihat. Kemudian dapat kita lihat, AnimatedContainer di mulai dengan persegi panjang bersudut lengkung. Saat DownloadStatus berubah menjadi fetchingDownloadAnimatedContainer perlu dianimasikan dari persegi panjang bersudut lengkung ke lingkaran penuh dan satu-satunya cara untuk menerapkan animasi tersebut adalah dengan mendefinisikan bentuk awal yaitu persegi panjang bersudut lengkung kemudian bentuk akhirnya lingkaran penuh. Namun pada saat bentuk akhirnya kita buat lingkaran penuhnya menjadi transparan sedikit pudar, supaya terlihat bagus.


4. Menampilkan Button Text

Pada langkah ini DownloadButton menampilkan GET selama tahap notDownloaded dan menampilkan OPEN selama tahap downloaded sehingga tidak ada teks diantaranya.

Cuplikan kode di bawah ini memperlihatkan penambahan widget untuk menampilkan teks selama setiap tahap download dan memberikan efek buram pada teks di antara tahap tersebut. Tambahkan pohon widget teks sebagai child dari pohon widget shape.

class _DownloadButtonState extends State<DownloadButton> {
 @override
 Widget build(BuildContext context) {
   return _buildButtonShape(
     child: _buildText(),
   );
 }

 Widget _buildText() {
   final text = _isDownloaded ? 'OPEN' : 'GET';
   final opacity = _isDownloading || _isFetching ? 0.0 : 1.0;

   return Padding(
     padding: const EdgeInsets.symmetric(vertical: 6),
     child: AnimatedOpacity(
       duration: widget.transitionDuration,
       opacity: opacity,
       curve: Curves.ease,
       child: Text(
         text,
         textAlign: TextAlign.center,
         style: Theme.of(context).textTheme.button?.copyWith(
           fontWeight: FontWeight.bold,
           color: CupertinoColors.activeBlue,
         ),
       ),
     ),
   );
 }
}

5. Menampilkan Animasi Spinner Ketika Proses Download

Pada langkah ini, selama tahap fetchingDownload DownloadButton akan menampilkan radial spinner. Spinner ini mulai masuk dari tahap notDownloaded dan menghilang pada tahap fetchingDownload.

class _DownloadButtonState extends State<DownloadButton> {
 @override
 Widget build(BuildContext context) {
   return Stack(
     children: [
       _buildButtonShape(
         child: _buildText(),
       ),
       _buildDownloadingProgress(),
     ],
   );
 }

 Widget _buildDownloadingProgress() {
   return Positioned.fill(
     child: AnimatedOpacity(
       duration: widget.transitionDuration,
       opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
       curve: Curves.ease,
       child: _buildProgressIndicator(),
     ),
   );
 }

 Widget _buildProgressIndicator() {
   return AspectRatio(
     aspectRatio: 1.0,
     child: CircularProgressIndicator(
       backgroundColor: Colors.white.withOpacity(0.0),
       valueColor: AlwaysStoppedAnimation(
         CupertinoColors.lightBackgroundGray
       ),
       strokeWidth: 2.0,
       value: null,
     ),
   );
 }
}

6. Menampilkan Progress dan Stop Button Saat Downloading

Setelah tahap fetchingDownload adalah tahap downloading. Selama tahap downloading, DownloadButton mengganti radial progress spinner dengan growing radial progress bar. DownloadButton juga menampilkan ikon stop button sehingga pengguna dapat membatalkan proses download yang sedang berjalan.

@immutable
class DownloadButton extends StatefulWidget {
 const DownloadButton({
   Key? key,
   this.progress = 0.0,
 }) : super(key: key);

 final double progress;

 @override
 _DownloadButtonState createState() => _DownloadButtonState();
}

class _DownloadButtonState extends State<DownloadButton> {
 @override
 Widget build(BuildContext context) {
   return Stack(
     children: [
       _buildButtonShape(
         child: _buildText(),
       ),
       _buildDownloadingProgress(),
     ],
   );
 }

 Widget _buildDownloadingProgress() {
   return Positioned.fill(
     child: AnimatedOpacity(
       duration: widget.transitionDuration,
       opacity: _isDownloading || _isFetching ? 1.0 : 0.0,
       curve: Curves.ease,
       child: Stack(
         alignment: Alignment.center,
         children: [
           _buildProgressIndicator(),
           if (_isDownloading)
             const Icon(
               Icons.stop,
               size: 14.0,
               color: CupertinoColors.activeBlue,
             ),
         ],
       ),
     ),
   );
 }


 Widget _buildProgressIndicator() {
   return AspectRatio(
     aspectRatio: 1.0,
     child: TweenAnimationBuilder<double>(
       tween: Tween(begin: 0.0, end: widget.progress),
       duration: const Duration(milliseconds: 200),
       builder: (BuildContext context, double progress, Widget? child) {
         return CircularProgressIndicator(
           backgroundColor: _isDownloading ? CupertinoColors.lightBackgroundGray : Colors.white.withOpacity(0.0),
           valueColor:
             AlwaysStoppedAnimation(_isFetching ? CupertinoColors.lightBackgroundGray : CupertinoColors.activeBlue),
           strokeWidth: 2.0,
           value: _isFetching ? null : progress,
         );
       },
     ),
   );
 }
}

7. Menambahkan Button tap Untuk Melakukan Callbacks

Detail terakhir yang dibutuhkan DownloadButton adalah button behavior. Tombol tersebut harus melakukan berbagai hal saat pengguna mengetuknya. Oleh karena itu, akan kita tambahkan properti widget callback untuk memulai proses download, membatalkan download, dan membuka download.

Terakhir, gabungkan pohon widget DownloadButton yang ada dengan widget GestureDetector, dan teruskan event ketukan tersebut ke properti callback yang sesuai.

@immutable
class DownloadButton extends StatefulWidget {
 const DownloadButton({
   Key? key,
   required this.onDownload,
   required this.onCancel,
   required this.onOpen,
 }) : super(key: key);

 final VoidCallback onDownload;
 final VoidCallback onCancel;
 final VoidCallback onOpen;

 @override
 _DownloadButtonState createState() => _DownloadButtonState();
}

class _DownloadButtonState extends State<DownloadButton> {
 void _onPressed() {
   switch (widget.status) {
     case DownloadStatus.notDownloaded:
       widget.onDownload();
       break;
     case DownloadStatus.fetchingDownload:
       // do nothing.
       break;
     case DownloadStatus.downloading:
       widget.onCancel();
       break;
     case DownloadStatus.downloaded:
       widget.onOpen();
       break;
   }
 }

 @override
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: _onPressed,
     child: Stack(
       children: [
         _buildButtonShape(
           child: _buildText(),
         ),
         _buildDownloadingProgress(),
       ],
     ),
   );
 }
}

Selamat! Teman-teman sudah memiliki tombol yang akan mengubah tampilannya tergantung pada tahap mana tombol tersebut berada: not downloaded, fetching download, downloading, dan downloaded. Sekarang, pengguna dapat mengetuk untuk memulai proses download, ketuk untuk membatalkan proses download yang sedang berlangsung, dan ketuk untuk membuka hasil download yang sudah selesai.

Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya:


Jika ada pertanyaan silahkan komen dan jika artikel ini dirasa bermanfaat, jangan lupa like dan sharenya ya teman-teman. ??????? Sampai bertemu di artikel selanjutnya.
Terima Kasih, Assalamu’alaykum… Salam KODINGINDONESIA

Referensi : https://flutter.dev//

Anton Prafanto

Konten developer kodingindonesia.com & staf pengajar tetap di Universitas Mulawarman Samarinda

all author posts