Belajar Flutter Basic #6: Stateless & Stateful Widget

Belajar Flutter Basic #6: Stateless & Stateful Widget

Pendahuluan

Bahasan mengenai stateless dan stateful widget akan kita bahas dengan menyelesaikan sebuah case sederhana agar lebih mudah dimengerti. Jadi ceritanya, saat masih kecil saya sering dapat tugas untuk belanja ke warung, dimana datar belanjaan ibu itu pasti lebih dari 1 item. Ketika langkah pertama dimulai, maka saya mulai untuk menghafalkan nama nama barang tersebut diiringi langkah berikutnya hingga tiba didepan warung yang dituju.

Sesampainya ditujuan, item yang dihafalkan selama perjalanan lenyap seketika tanpa menyisakan jejak. Berangkat dari masa kecil itulah, saat ini kita akan membuat sebuah aplikasi sederhana dengan fungsi untuk menampung daftar belanjaan.

Meskipun sederhana, case ini akan membantu kita untuk memahami bagaimana cara menggunakan stateless dan stateful widget, memisahkan setiap component program menjadi beberapa file agar lebih maintenable, membuat models untuk meng-handle data, membuat event untuk menambahkan data ke dalam list yang sudah ada, dan lain sebagainya.

Adapun hasil yang akan kita capai adalah seperti gambar dibawah ini.

Baca Juga: Belajar Flutter Basic #5: Http Request

Stateful vs Stateless Widget

Hal yang unik dan menarik perhatian dari Flutter adalah stateless dan stateful widget, dimana kedua bagian ini memegang perannya masing-masing. Stateless widget adalah widget statis dimana seluruh konfigurasi yang dimuat didalamnya telah diinisiasi sejak awal. Sedangkan Stateful widget berlaku sebaliknya dimana sifatnya adalah dinamis, sehingga widget ini dapat diperbaharui kapanpun dibutuhkan berdasarkan user actions atau ketika terjadinya perubahan data.

Untuk lebih jelas mengenai peran keduanya, maka pada artikel ini kita akan mengimplementasikan kedua jenis widget tersebut kedalam case yang telah dijelaskan sebelumnya. Pertama, buat project baru terlebih dahulu dengan command berikut & tunggu hingga proses instalasinya selesai:

flutter create dw_shopping_bag

Kemudian buka file main.dart dan hapus seluruh code yang ada, lalu tambahkan code berikut:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Shopping Bag",
      theme: ThemeData(
          primarySwatch: Colors.pink,
          accentColor: Colors.purpleAccent,
          textTheme: ThemeData.light().textTheme.copyWith(
              title: TextStyle(fontSize: 15, fontWeight: FontWeight.bold))),
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Daftar Belanjaan"),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            //NANTINYA BERISI WIDGET
          ],
        ),
      ),
    );
  }
}

Selanjutnya buat file lib/models/cart.dart (red: buat folder models jika belum ada) yang berfungsi untuk meng-handle data yang kita miliki, tambahkan code:

import 'package:flutter/foundation.dart';

class Cart {
  final String id;
  final String title;
  final double harga;
  final int qty;

  //BUAT CONSTRUCTOR DIMANA SECARA DEFAULT CLASS INI AKAN MEMINTA DATA TERSEBUT
  Cart({
    @required this.id, 
    @required this.title, 
    @required this.harga, 
    @required this.qty
  });
}

Penjelasan: Jadi model ini akan menampung object data yang kita miliki, dimana properti yang ada adalah id, title, harga dan qty. Adapun ke-4 properti ini bersifat required, maka perlu untuk meng-import package flutter/foundation.dart agar dapat menggunakan fungsi @required.

Create Product Lists

Tugas pertama kita adalah membuat component yang berisi widget untuk menampilkan product lists yang telah ditambahkan, buat folder components dan didalamnya buat file product_lists.dart, kemudian tambahkan code berikut:

import 'package:flutter/material.dart';
import '../models/cart.dart'; //IMPORT MODEL CART.DART

class ProductList extends StatelessWidget {
  final List<Cart> carts; //DEFINISIKAN VARIABLE CARTS YANG BERFUNGSI UNTUK MENAMPUNG LIST DATA, DIMANA TIPENYA MENGGUNAKAN LIST DAN STRUKTUR DATANYA MERUJUK PADA MODEL Cart

  ProductList(this.carts); //BUAT CONSTRUCTOR UNTUK MEMINTA DATA

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 400,
      //APABILA CARTS KOSONG
      child: carts.isEmpty ? Column(children: <Widget>[
        //MAKA TAMPILKAN WIDGET TEXT UNTUK MEMBERIKAN INFORMASI BAHWA TIDAK ADA
        Text(
          "No Transaction Data",
          style: Theme.of(context).textTheme.title,
        ),
      //SELAIN ITU, MAKA DATA CARTS AKAN DILOOPING MENGGUNAKAN LISTVIEW.BUILDER
      ],): ListView.builder(
        itemBuilder: (context, index) { //DIDALAM BUILDER INI WIDGET AKAN DILOOPING BERDASARKAN JUMLAH DATA, DAN INDEX ARRAYNYA AKAN DIUPDATE KE DALAM VARIABLE INDEX
          return Card(
            child: Row(
              children: <Widget>[
                Container(
                  margin: EdgeInsets.symmetric(vertical: 10, horizontal: 15),
                  padding: EdgeInsets.all(10),
                  decoration: BoxDecoration(
                      border: Border.all(
                          color: Theme.of(context).primaryColor, width: 2)),
                  child: Text(
                    carts[index].qty.toString(),
                    style: TextStyle(
                      color: Theme.of(context).primaryColor,
                      fontWeight: FontWeight.bold,
                      fontSize: 20
                    ),
                  ),
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(carts[index].title, style: Theme.of(context).textTheme.title,),
                    Text('Harga: Rp' + carts[index].harga.toStringAsFixed(0), style: TextStyle(fontSize: 12, color: Colors.grey),),
                  ],
                )
              ],
            ),
          );
        },
        itemCount: carts.length, //JUMLAH ITEMNYA BERDASARKAN JUMLAH DATA YANG ADA DI DALAM CARTS
      ),
    );
  }
}

Untuk menggunakan component diatas, kita perlu meng-import-nya pada file main.dart, buka file tersebut dan tambahkan code:

import './components/product_lists.dart';

Masih dengan file yang sama, perhatian pada bagian body, tambahkan widget berikut:

//[.. code sebelumnya ..]
body: SingleChildScrollView(
  child: Column(
    children: <Widget>[
      ProductList(_carts), //TAMBAHKAN BAGIAN INI
    ],
  ),
),
//[.. code sebelumnya ..]

Ets, jangan terburu-buru untuk pindah ke file lain, karena kita masih bekerja dengan main.dart, masukkan code berikut ini untuk mendefinisikan variable _carts dimana variable ini untuk menampung data yang telah ditambahkan oleh user, data ini dikirim ke widget ProductList(_carts) karena pada widget tersebut membutuhkan list data yang akan ditampilkan. Buat kamu yang pernah belajar Vue.js, kurang lebih ini mirip seperti passing props antar component, dimana component utama mengelola untuk setiap perubahan data-nya dan component anak hanya menerima data untuk ditampilkan.

//[.. CODE SEBELUMNYA ..]
class _HomeState extends State<Home> {
  final List<Cart> _carts = [
    //SEBAGAI PERMULAAN, KITA TAMBAHKAN DUA BUAH DATA DUMMY
    Cart(id: 'DW1', title: 'Sabun Mandi', harga: 15000, qty: 1),
    Cart(id: 'DW2', title: 'Shampoo', harga: 17000, qty: 2),
  ];
  //[.. CODE SETELAHNYA ..]
}

Create Dashboard

Melangkah pada bagian selanjutnya,tugas kita adalah membuat dashboard widget yang berfungsi untuk menampilkan total item yang akan dibeli dan total harga dari semua item tersebut. Bagian ini akan kita pisahkan juga component-nya agar lebih mudah dalam mengelolanya. pada folder components, buat file dashboard.dart dan tambahkan code:

import 'package:flutter/material.dart';
import '../models/cart.dart';

class Dashboard extends StatelessWidget {
  final List<Cart> _listCart;  //SEPERTI BIASA DEFINISIKAN VARIABLE UNTUK MENAMPUNG DATA LIST CART

  Dashboard(this._listCart); //BUAT CONSTRUCTOR UNTUK MEMINTA DATA LIST CARTNYA

  //BUAT GETTER UNTUK MENDAPATKAN TOTAL ITEM
  int get totalItem {
    //DIMANA DATANYA KITA DAPATKAN DARI HASIL SUM QTY
    return _listCart.fold(0, (sum, item) {
      return sum += item.qty;
    });
  }

  //BUAT GETTER UNTUK MENDAPATKAN TOTAL PRICE
  double get totalPrice {
    //DAN DATANYA KITA DAPATKAN DARI SUM HARGA
    return _listCart.fold(0, (sum, item) {
      return sum += item.harga;
    });
  }


  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 6, //TINGKAT KETEBALAN SHADOW DARI CARD
      margin: EdgeInsets.all(10),    
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround, //AGAR KEDUA ELEMEN DIDALAMNYA DI SET SEHINGGA MEMILIKI JARAK ANTAR KEDUANYA DAN MEMENUHI CARD
          children: <Widget>[
            Column(children: <Widget>[
              //MENAMPILKAN TOTAL ITEM
              Text("Total Item", style: Theme.of(context).textTheme.title,),
              SizedBox(height: 4,),
              Text(totalItem.toString() + " pcs", style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),),
            ],),
            
            Column(children: <Widget>[
             	//MENAMPILKAN TOTAL BELANJA
              Text("Total Belanja", style: Theme.of(context).textTheme.title,),
              SizedBox(height: 4,),
              Text(totalPrice.toStringAsFixed(0), style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),)
            ],)
          ],
        ),
      ),
    );
  }
}

Saatnya untuk menggunakan component ini, buka file main.dart dan import terlebih dahulu:

import './components/dashboard.dart';

Kemudian gunakan widget dari component yang telah dibuat, masih dengan file main.dart, tambahkan code berikut pada bagian body

body: SingleChildScrollView(
  child: Column(
    children: <Widget>[
      Dashboard(_carts), //TAMBAHKAN CODE INI, KITA LETAKKAN DIATAS WIDGET SEBELUMNYA KARENA POSISINYA JUGA AKAN DIRENDER SEBELUM LIST KERANJANG
      ProductList(_carts)
    ],
  ),
),

Add New Data

Tidak lengkap rasanya jika pengguna tidak memiliki akses untuk menambahkan list barang yang akan di beli, untuk membuatnya maka kita perlu membuat sebuah component lagi yang berisi form input. pada folder components, buat file add_new_item.dart kemudian tambahkan code:

import 'package:flutter/material.dart';

class AddNewItem extends StatefulWidget {
  final Function addNew; //KITA MENDEFINISIKAN SEBUAH FUNGSI DENGAN NAMA addNew
  AddNewItem(this.addNew); //DIMANA FUNGSI INI DI-PASSING DARI main.dart, SEHINGGA KITA MEMBUAT CONSTRUCTOR UNTUK MEMINTA FUNGSINYA

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

class _AddNewItemState extends State<AddNewItem> {
  //BUAT CONTROLLER UNTUK MENG-HANDLE TEXTFIELD INPUT
  final titleController = TextEditingController();
  final hargaController = TextEditingController();
  final qtyController = TextEditingController();

  //METHOD INI AKAN BERJALAN KETIKA TOMBOL DARI WIDGET FLATBUTTON DITEKAN
  void saveNewItem() {
    //MENGAMBIL VALUE DARI MASING-MASING CONTROLLER INPUTAN DAN MENYIMPANNYA KE DALAM VARIABLE BARU
    final title = titleController.text;
    final harga = hargaController.text;
    final qty = int.parse(qtyController.text);

    //CEK JIKA TIDAK SESUAI RULE DARI KONDISI IFNYA
    if (title.isEmpty || harga.isEmpty || qty <= 0) {
      return; //MAKA STOP TIDAK MELAKUKAN APA-APA
    }
    //JIKA SESUAI, MAKA FUNGSI addNew DIJALANKAN DENGAN MENGIRIMKAN BEBERAPA PARAMETER UNTUK DITAMBAHKAN KE CART PADA MAIN.DART
    widget.addNew(title, double.parse(harga), qty);
    //KARENA NANTINYA AKAN MENGGUNAKAN MODAL, MAKA GUNAKAN NAVIGATOR POP UNTUK MENUTUP MODAL
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Container(
        padding: EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(labelText: 'Nama Barang'),
              controller: titleController, //CONTROLLER INI SERUPA DENGAN NAME PADA INPUTAN HTML
            ),
            TextField(
              decoration: InputDecoration(labelText: 'Harga'),
              controller: hargaController,
              keyboardType: TextInputType.number, //KEYBOARDNYA HANYA AKAN MENAMPILKAN ANGKA
            ),
            TextField(
              decoration: InputDecoration(labelText: 'Qty'),
              controller: qtyController,
              keyboardType: TextInputType.number,
            ),
            FlatButton(
              child: Text('Tambah'),
              onPressed: saveNewItem, //KETIKA DITEKAN JALANKAN METHOD saveNewItem
              textColor: Colors.pink,
            )
          ],
        ),
      ),
    );
  }
}

Seperti biasa, component diatas akan kita import lagi pada file main.dart, buka file tersebut dan tambahkan code:

import './components/add_new_item.dart';

Jangan beranjak dulu, karena di file yang sama, tambahkan code:

//[.. CODE SEBELUMNYA ..]
class _HomeState extends State<Home> {
  //BAGIAN INI SUDAH DITAMBAHKAN SEBELUMNYA
  final List<Cart> _carts = [
    Cart(id: 'DW1', title: 'Sabun Mandi', harga: 15000, qty: 1),
    Cart(id: 'DW2', title: 'Shampoo', harga: 17000, qty: 2),
  ];

	//METHOD INI AKAN MENJALANKAN MODAL BOTTOM SHEET, DIMANA MODAL INI AKAN TAMPIL DARI BAWAH
  void _openModal(BuildContext context) {
    showModalBottomSheet(
        context: context,
        builder: (_) {
          return AddNewItem(_addNewItem); //DAN ISI DARI MODAL TERSEBUT ADALAH COMPONENT AddNewItem. PASTINYA PARAMETER YANG DIKIRIM ADALAH SEBUAH FUNGSI BERNAMA _addNewItem, MAKA PERLU KITA DEFINISIKAN SELANJUTNYA
        });
  }

  //FUNGSI INI UNTUK MEMANIPULASI DENGAN MENAMBAHKAN DATA BARU KE DALAM CART
  void _addNewItem(String title, double harga, int qty) {
    //BUAT FORMAT DATANYA DENGAN REFERENSI MENGGUNAKAN MODAL Cart
    final newItem = Cart(id: DateTime.now().toString(), title: title, harga: harga, qty: qty);
    setState(() {
      _carts.add(newItem); //SET STATE-NYA UNTUK MENAMBAHKAN DATA BARU TERSEBUT
    });
  }
  
  //FUNGSI INI UNTUK MENGHAPUS SEMUA DATA PADA VARIABLE CARTS
  void _resetCarts() {
    setState(() {
      _carts.clear(); //SET STATENYA KEMUDIAN CLEAR
    });
  }
  
  //[.. CODE SETELAHNYA ..]
}

Sampai pada tahap ini, semua hal yang dibutuhkan telah selesai dibuat, maka tugas selanjutnya adalah membuat tombol untuk membuat modal Bottom Sheet. Pada file main.dart, tepat dibawah body tambahkan floatingActionButton:

//[.. CODE SEBELUMNYA ..]
body: SingleChildScrollView(
  child: Column(
    children: <Widget>[
      Dashboard(_carts),
      ProductList(_carts),
    ],
  ),
),
//TAMBAHKAN CODE INI
floatingActionButton: FloatingActionButton(
    child: Icon(Icons.add),
    onPressed: () => _openModal(context), //KETIKA DITEKAN MENJALANKAN FUNGSI _openModal
  ),
);

Masih dengan file yang sama, kita juga akan menambahkan tombol untuk menjalankan method _resetCarts(). tambahkan code berikut:

//[.. CODE SEBELUMNYA ..]
appBar: AppBar(
  title: Text("Daftar Belanjaan"),
  actions: <Widget>[
    //TAMBAHKAN CODE INI
    FlatButton(child: Icon(Icons.clear, color: Colors.white,), onPressed: () => _resetCarts(),)
  ],
),
//[.. CODE SEBELUMNYA ..]

Semuanya sudah selesai, untuk menjalankannya tekan F5 atau pada VsCode ke menu debug > Start Debugging.

Baca Juga: Belajar Flutter Basic #4: Form Validation

Kesimpulan

Aplikasi sederhana yang telah kita buat setidaknya telah memberikan kita gambaran bagaimana penggunaan stateless dan stateful widget, bagaimana membuat fungsi, bagaimana mengirimkan data antar component, dan lain sebagainya.

Adapun dokumentasi code-nya dapat ditemukan di Github.

Category:
Share:

Comments