Belajar Flutter Basic #8: State Management & Http Request

Belajar Flutter Basic #8: State Management & Http Request

Pendahuluan

Mengelompokkan data ke dalam sebuah state berdasarkan kelompok data tersebut menjadi pilihan yang menarik untuk digunakan dengan tujuan agar lebih maintenable & mudah digunakan kembali dimanapun data tersebut dibutuhkan. Flutter sebagai sebuah framework modern juga memiliki library yang dapat mengelola state sesuai dengan keingingan penggunanya.

Selain itu state management kerap kali digunakan untuk menyimpan seluruh logic yang berkaitan dengan http request dengan tujuan yang sama yakni agar logic tersebut juga bersifat re-usable. Seri belajar Flutter basic kali ini akan mengajak kita untuk mendalami bagaimana cara menerapkan state management pada Flutter.

Baca Juga: Belajar Flutter Basic #7: Navigation Aplikasi Parawisata

Create Simple Inventory Apps

Case yang akan diangkat adalah sebuah aplikasi sederhana untuk memonitoring data barang dengan beberapa fitur diantaranya: mengurangi stock barang ketika digeser kekiri, refresh data dengan pull down, CRUD (Create Read Update Delete) data barang. Adapun preview-nya kurang lebih seperti gambar berikut.

Buat project baru dengan command:

flutter create dw_inventory

Buka file main.dart dan modifikasi menjadi:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import './pages/product_page.dart';
import './providers/products.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //
    return MultiProvider(
      providers: [
        //BERFUNGSI UNTUK ME-LOAD PROVIDER Products
        //JIKA MENGGUNAKAN LEBIH DARI 1 PROVIDER CUKUP PISAHKAN DENGAN COMMAND DI DALAM ARRAY providers.
        ChangeNotifierProvider.value(
          value: Products(),
        ),
        // ChangeNotifierProvider.value(
        //   value: ProviderLain(),
        // ),
      ],
      child: MaterialApp(
        title: 'Daengweb.id',
        theme: ThemeData(
            primarySwatch: Colors.blue,
            primaryColor: Colors.pink,
            accentColor: Colors.yellow),
        //ROUTING UNTUK MENGATUR SETIAP PAGE YANG DI-LOAD
        routes: {
          '/': (ctx) => ProductPage(),
        },
      ),
    );
  }
}

Penjelasan: Ada dua hal yang perlu di perhatikan pada bagian ini, pertama adalah routing-nya yang berada pada block code routes dan yang kedua adalah state management menggunakan providers.

Sebelum membuat providers Products, terlebih dahulu install beberapa package berikut. Buka file pubspec.yaml dan tambahkan kedua line dibawah ini:

dependencies:
  flutter:
    sdk: flutter
  provider: ^3.0.0+1 //tambahkan line ini
  intl: ^0.15.8 //tambakan line ini
  http: ^0.12.0+2 //tambahkan line ini

Note: Pastikan sejajar dengan kata Flutter dan tepat dibawah dependecies, karena posisi code sangat berpengaruh.

Ketika di-save, secara otomatis VsCode akan mengunduh package tersebut. Jika belum, dapat dilakukan secara manual dengan command:

flutter pub get

Selanjutnya, buat file products.dart di dalam folder lib/providers dan tambahkan code:

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

// CLASS INI AKAN MENG-HANDLE FORMAT DATA YANG DIINGINKAN
class ProductItem {
  final String id;
  final String title;
  final int stock;
  final String description;

  ProductItem(
      {@required this.id,
      @required this.title,
      @required this.stock,
      @required this.description});
}

//CLASS INI BERTINDAK UNTUK MENG-HANDLE STATE MANAGEMENT
class Products with ChangeNotifier {
  List<ProductItem> _items = []; //KITA INISIASI DATA AWAL KOSONG DENGAN TIPE LIST DAN DIDALAMNYA MEMILIKI FORMAT DATA SESUAI DENGAN PRODUCT ITEM.
  //PROPERTY DENGAN PREFIX _ MENDANDAKAN BAHWA DIA BERSIFAT PRIVATE

  //MAKA KITA BUAT LAGI SEBUAH GETTER YANG BERISI DATA DARI PROPERTY _ITEMS, DIMANA GETTERS INI YANG AKAN DIAKSES SECARA PUBLIK. 
  List<ProductItem> get items {
    return [..._items];
  }

  //METHOD INI BERFUNGSI UNTUK MENGAMBIL DATA DARI SERVER
  Future<void> fetchProduct() async {
    //DIMANA BACKEND YG DIGUNAKAN ADALAH FIREBASE
    const url = 'https://dw-xxxx.firebaseio.com/products.json';
    final response = await http.get(url); //MENGGUNAKAN AWAIT UNTUK MENUNGGU PROSESNYA SEBELUM MELANJUTKAN KE CODE SELANJUTNYA
    final convertData = json.decode(response.body) as Map<String, dynamic>; //DECODE DATA DAN UBAH FORMATNYA DENGNA FORMAT MAP DAN KEYNYA ADALAH STRING, VALUENYA DYNAMIC
    final List<ProductItem> newData = []; //INISIASI LIST DATA BARU YANG KOSONG
    //JIKA HASIL DECODE KOSONG MAKA HENTIKAN PROSES
    if (convertData == null) {
      return;
    }
    //JIKA TIDAK KOSONG, INSERT DATA YANG DIDAPATKAN DARI SERVER KEDALAM NEW DATA
    convertData.forEach((key, value) {
      newData.add(ProductItem(
          id: key,
          title: value['title'],
          stock: value['stock'],
          description: value['description']));
    });
    //KEMUDIAN SEMUA DATA YANG ADA DI DALAM NEW DATA KITA MASUKKAN KE STATE _items
    _items = newData;
    notifyListeners(); //BERFUNGSI UNTUK MEMBERITAHUKAN BAHWA ADA DATA BARU SEHINGGA WIDGET AKAN DI RE-RENDER
  }
}

Tugas selanjutnya adalah membuat file product_page.dart dan tempatkan di dalam folder lib/pages.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/products.dart'; //IMPORT PROVIDER products YANG TELAH DIBUAT TADI
import '../components/product_list.dart';
import '../components/side_bar.dart';

class ProductPage extends StatelessWidget {
  //FUNGSI UNTUK ME-LOAD DATA TERBARU
  Future<void> _refreshData(BuildContext context) async {
    //CALL FUNGSI fetchProduct() DARI PROVIDERS Products.dart
    await Provider.of<Products>(context, listen: false).fetchProduct();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('DW Data Barang'),
      ),
      drawer: SideBar(), //DRAWER BERFUNGSI UNTUK MENGATUR SIDEBAR MENU
      body: RefreshIndicator(
        onRefresh: () => _refreshData(context), //KETIKA DIPULL DOWN, MAKA FUNGSI _refreshData() DIJALANKAN
              child: FutureBuilder(
          //LOAD DATA MENGGUNAKAN FUTUREBUILDER
          future: Provider.of<Products>(context, listen: false).fetchProduct(),
          builder: (ctx, snapshop) {
            //KETIKA MASIH LOADING
            if (snapshop.connectionState == ConnectionState.waiting) {
              //MAKA RENDER WIDGET LOADING
              return Center(
                child: CircularProgressIndicator(),
              );
            } else {
              //JIKA TERDAPAT ERROR
              if (snapshop.error != null) {
                //MAKA TAMPILKAN TEXT ERROR
                return Center(
                  child: Text("Error Loading Data"),
                );
              } else {
                //KETIKA LOADING DATA SELESAI DAN TIDAK ADA ERROR
                //MAKA KITA AMBIL STATE PRODUCTS MENGGUNAKAN CONSUMER
                return Consumer<Products>(
                  builder: (ctx, product, child) => Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: ListView.builder(
                      //DIMANA STATE YANG DIAMBIL DARI PRODUCT YANG DIDALAMNYA TERDAPAT STATE ITEMS
                      itemCount: product.items.length,
                      //ADAPUN TAMPILANNYA AKAN DI-HANDLE OLEH CUSTOM COMPONENTS
                      //MAKA KITA TINGGAL MENGIRIMKAN DATA SETIAP ITEM KE CUSTOM COMPONENTS TERSEBUT
                      itemBuilder: (ctx, i) => ProductList(
                        product.items[i].id,
                        product.items[i].title,
                        product.items[i].description,
                        product.items[i].stock,
                        false,
                      ),
                    ),
                  ),
                );
              }
            }
          },
        ),
      ),
    );
  }
}

Component ProductList akan berisi code yang akan meng-handle tampilan setiap item dari data product akan terlihat seperti apa, buat file product_list.dart di dalam folder lib/components dan tambahkan code:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/products.dart';

class ProductList extends StatelessWidget {
  final String id;
  final String title;
  final String description;
  final int stock;
  final bool type;

  //BUAT CONSTRUCTOR UNTUK MEMINTA DATA DARI WIDGET YANG MENGGUNAKANNYA
  ProductList(this.id, this.title, this.description, this.stock, this.type);

  @override
  Widget build(BuildContext context) {
    //DISMISSIBLE, KETIKA ITEM PRODUKNYA DI GESER MAKA KITA AKAN MENJALANKAN ACTIONS
    return Dismissible(
      key: ValueKey(id),
      background: Container(
        color: Theme.of(context).errorColor,
        child: Icon(
          Icons.call_missed_outgoing,
          size: 40,
          color: Colors.white,
        ),
        alignment: Alignment.centerRight,
        padding: EdgeInsets.only(right: 20),
        margin: EdgeInsets.symmetric(
          horizontal: 15,
          vertical: 4,
        ),
      ),
      //ARAH GESERNYA LITA SET DARI KANAN KE KIRI
      direction: DismissDirection.endToStart,
      //KETIKA DI GESER
      confirmDismiss: (dismiss) {
        //MAKA AKAN MENAMPILKAN DIALOG
        showDialog(
          context: context,
          //DIMANA ISI DIALOGNYA ADALAH ALERTDIALOG
          builder: (ctx) => AlertDialog(
            title: Text("Kamu Yakin?"),
            content: Text("Kamu Akan Mengurangi Stok?"),
            actions: <Widget>[
              //KETIKA TOMBOL INI DITEKAN MAKA AKAN MENUTUP ALERT DENGAN MENGIRIMKAN VALUE FALSE
              FlatButton(
                child: Text(
                  "No",
                  style: TextStyle(
                    color: Colors.grey,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                onPressed: () {
                  Navigator.of(ctx).pop(false);
                },
              ),
              //DAN TOMBOL INI BERFUNGSI SAMA TAPI MENGIRIMKAN VALUE TRUE
              FlatButton(
                child: Text(
                  "Yes",
                  style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                onPressed: () {
                  Navigator.of(ctx).pop(true);
                },
              )
            ],
          ),
        ).then((result) {
          //KITA CEK, JIKA VALUENYA BERNILAI TRUE
          if (result) {
            //DAN STOKNYA ADA
            if (stock > 0) {
              //MAKA FUNGSI CHANGESTOCK DARI PROVIDERS AKAN DIJALANKAN
              Provider.of<Products>(context, listen: false).changeStock(id);
            } else {
              //SELAIN ITU AKAN MENAMPILKAN SNACKBAR UNTUK INFORMASI
              Scaffold.of(context).hideCurrentSnackBar();
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text("Stok Kosong"),
                duration: Duration(seconds: 2),
              ));
            }
          }
        });
      },
      child: Card(
        elevation: 4,
        //ADAPUN TAMPILANNYA KITA GUNAKAN LISTTILE, DIMANA TERDAPAT 3 BAGIAN: LEADING, TITLE DAN TRAILING
        child: ListTile(
          //LEADING AKAN DI-RENDER PADA POSISI KIRI
          //BAGIAN INI AKAN KITA GUNAKAN UNTUK MENAMPILKAN STOK
          leading: CircleAvatar(
            child: Text(stock.toString()),
          ),
          //TITLE AKAN DI-RENDER DITENGAH SETELAH LEADING, DAN BAGIAN INI AKAN MENAMPILKAN NAMA BARANG
          title: Text(title),
          //SUBTITLE AKAN DI-RENDER DIBAWAH TITLE. BAGIAN INI AKAN DIGUNAKAN UNTUK MENAMPILKAN DESKRIPS
          subtitle: Text(
            'Deskripsi: $description',
            style: TextStyle(color: Colors.grey, fontSize: 12),
          ),
          //TRAILING AKAN DI-RENDER DISEBELAH KANAN
          //LALU KITA CEK JIKA TYPENYA FALSE, KITA AKAN MENAMPILKAN STATUS STOK
          trailing: !type
              ? Text(
                  stock > 0 ? 'In Stock' : 'Sold Out',
                  style:
                      TextStyle(color: stock > 0 ? Colors.green : Colors.red),
                )
              //DAN JIKA TRUE, MAKA AKAN MENAMPILKAN TOMBOL EDIT DAN HAPUS
              : Container(
                  width: 100,
                  child: Row(
                    children: <Widget>[
                      IconButton(
                        icon: Icon(Icons.edit),
                        onPressed: () {
                          //EDIT BERISI FUNGSI UNTUK BERPINDAH PAGE DENGAN MENGIRIMKAN ARGUMENTS ID
                          Navigator.of(context)
                              .pushNamed('/add-product', arguments: id);
                        },
                      ),
                      IconButton(
                        icon: Icon(Icons.delete),
                        onPressed: () {
                          //SEDANGKAN DELETE AKAN MENAMPILKAN ALERT DIALOG LAGI UNTUK KONFIRMASI
                          showDialog(
                            context: context,
                            builder: (ctx) => AlertDialog(
                              title: Text("Kamu Yakin?"),
                              content: Text("Proses Ini Akan Menghapus Data"),
                              actions: <Widget>[
                                FlatButton(
                                  child: Text(
                                    "Batal",
                                    style: TextStyle(
                                      color: Colors.grey,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                  onPressed: () {
                                    Navigator.of(context).pop(false);
                                  },
                                ),
                                FlatButton(
                                  child: Text(
                                    "Lanjutkan",
                                    style: TextStyle(
                                      color: Colors.black,
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                  onPressed: () {
                                    //JIKA YES, MAKA FUNGSI removeProduct AKAN DIJALANKAN
                                    Provider.of<Products>(context,
                                            listen: false)
                                        .removeProduct(id)
                                        .then((_) {
                                      Navigator.of(context).pop(false);
                                    });
                                  },
                                )
                              ],
                            ),
                          );
                        },
                        color: Theme.of(context).errorColor,
                      ),
                    ],
                  ),
                ),
        ),
      ),
    );
  }
}

Jika diperhatikan, product_page.dart meng-import file side_bar.dart dimana pada file ini berisi code untuk menampilkan sidebar menu. Buat file tersebut di dalam folder lib/components dan tambahkan code berikut:

import 'package:flutter/material.dart';
class SideBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(children: <Widget>[
        //KITA BUAT HEADER SIDEBARNYA
        AppBar(title: Text('Daengweb.id'), automaticallyImplyLeading: false,),
        Divider(), //GARIS PEMISAH
        
        //UNTUK SETIAP MENUNYA KITA GUNAKAN LIST TILE DIMANA LEADINGNYA BERISI ICON DAN TITLENYA BERISI NAMA MENU. ADAPUN KETIKA DI TAP MAKA AKAN MENUJU KE PAGENYA MASING-MASING
        ListTile(leading: Icon(Icons.devices), title: Text('Inventory'), onTap: () {
          Navigator.of(context).pushReplacementNamed('/');
        },),
        Divider(),
        ListTile(leading: Icon(Icons.lens), title: Text('Manage Inventory'), onTap: () {
          Navigator.of(context).pushReplacementNamed('/manage-product');
        },),
      ],),
    );
  }
}

Manage Products Inventory

Home screen sudah selesai dimana fungsinya adalah menampilkan list barang beserta stoknya dan ketika digeser kekiri maka akan mengurangi stok barang tersebut. Bagian selanjutnya yang akan dibuat adalah page yang berfungsi untuk menampilkan list data barang serta memiliki fungsi untuk menambahkan, meng-edit dan menghapus data barang.

Pertama, buat file master_product_page.dart di dalam folder lib/pages dan tambahkan code:

import 'package:provider/provider.dart';
import 'package:flutter/material.dart';

import '../components/side_bar.dart';
import '../components/product_list.dart';
import '../providers/products.dart';

class MasterProductPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //FUNGSI UNTUK ME-LOAD DATA BARANG YANG TERBARU. 
    Future<void> _fetchData(BuildContext context) async {
      await Provider.of<Products>(context, listen: false).fetchProduct();
    }

    return Scaffold(
      appBar: AppBar(
        title: Text("Manage Inventory"),
        actions: <Widget>[
          //DI APPBAR, KITA BUAT TOMBOL UNTUK BERPINDAH KE PAGE ADD PRODUCT
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              Navigator.of(context).pushNamed('/add-product');
            },
          )
        ],
      ),
      drawer: SideBar(), //SIDEBARNYA KITA GUNAKAN CLASS YANG SAMA
      body: FutureBuilder(
        //LOAD DATA MENGGUNAKAN FUTURE BUILDER
        future: Provider.of<Products>(context, listen: false).fetchProduct(),
        //PENJELASAN BAGIAN INI SAMA DENGAN PENJELASAN YANG ADA DI PRODUCT_PAGE.DART
        builder: (ctx, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(
              child: CircularProgressIndicator(),
            );
          } else {
            if (snapshot.error != null) {
              return Center(
                child: Text("Error Loading Data"),
              );
            } else {
              return Padding(
                padding: EdgeInsets.all(10),
                child: RefreshIndicator(
                  onRefresh: () => _fetchData(context),
                  child: Consumer<Products>(
                    builder: (context, product, child) => ListView.builder(
                      itemCount: product.items.length,
                      //BAGIAN INI JUGA SAMA SEPERTI SEBELUMNYA. KITA GUNAKAN PRODUCTLIST UNTUK MENAMPILKAN SETIAP ITEM BARANG
                      itemBuilder: (ctx, i) => ProductList(
                        product.items[i].id,
                        product.items[i].title,
                        product.items[i].description,
                        product.items[i].stock,
                        true,
                      ),
                    ),
                  ),
                ),
              );
            }
          }
        },
      ),
    );
  }
}

Tugas selanjutnya adalah membuat page yang akan menampilkan form untuk menambahkan data barang, file ini akan kita buat reusable sehingga bisa digunakan untuk add data barang dan juga edit data barang. Buat file add_new_product.dart di dalam folder lib/pages dan tambahkan code:

import 'package:provider/provider.dart';

import '../providers/products.dart';
import 'package:flutter/material.dart';

class AddNewProduct extends StatefulWidget {
  @override
  _AddNewProductState createState() => _AddNewProductState();
}

class _AddNewProductState extends State<AddNewProduct> {
  final _titleDispose = FocusNode();
  final _priceDispose = FocusNode();
  final _descriptionDispose = FocusNode();

  final _titleController = TextEditingController();
  final _stockController = TextEditingController();
  final _descriptionController = TextEditingController();

  final _form = GlobalKey<FormState>();  //MEMBUAT GLOBAL KEY UNTUK FORM() WIDGET
  var _isLoading = false;
  var _initValue = true;
  String id;

  @override
  void didChangeDependencies() async {
    //KETIKA TERJADI PERUBAHANGAN MAKA FUNGSI INI DIJALANKAN, KITA CEK JIKA BERNILAI TRUE
    if (_initValue) {
      //MAKA _isLoading DIUBAH JADI TRUE
      setState(() {
        _isLoading = true;
      });
      //MENGAMBIL ID  YANG DIKIRIMKAN JIKA ADA (ID INI DIKIRIMKAN KETIKA TOMBOL EDIT DITEKAN)
      id = ModalRoute.of(context).settings.arguments as String;
      //DI CEK JIKA ID NYA TIDAK KOSONG, YANG BERARTI DARI EDIT AKAN BERISI ID. SEDANGKAN DARI ADD NEW ID NYA KOSONG 
      if (id != null) {
        //MAKA AKAN MENJALANKAN FUNGSI findById()
        final response = await Provider.of<Products>(context).findById(id);
        //KEMUDIAN DATA YANG DITERIMA AKAN DI ASSIGN
        _titleController.text = response.title;
        _stockController.text = response.stock.toString();
        _descriptionController.text = response.description;
      }
      //UBAH KEMBALI LOADING JADI FALSE
      setState(() {
        _isLoading = false;
      });
    }
    //INIT VALUE DI SET JADI FALSE AGAR TIDAK DIJALANKAN KEMBALI SEHINGGA FUNGSI DIATAS HANYA AKAN DIJALANKAN SEKALI KETIKA HALAMAN DILOAD
    _initValue = false;
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    _titleDispose.dispose();
    _priceDispose.dispose();
    _descriptionDispose.dispose();
    super.dispose();
  }

  //FUNGSI SUBMIT UNTUK MENYIMPAN DATA
  Future<void> _submit() async {
    final isValid = _form.currentState.validate(); //JALANKAN VALIDASI, DIMANA FUNGSI INI TELAH DISEDIAKAN OLEH FORM WIDGET()
    //JIKA TIDAK VALID
    if (!isValid) {
      return; //MAKA HENTIKAN PROSES
    }
    _form.currentState.save(); //JIKA VALID MAKA FUNGSI SAVE() DIJALANKAN, DIMANA FUNGSI INI JUGA DARI FORM WIDGET
    //SET LOADING JADI TRUE
    setState(() {
      _isLoading = true;
    });
    
    //KARENA FORM INI REUSABLE, MAKA CEK JIKA ID NULL
    if (id == null) {
      //MAKA YANG DIJALANKAN ADALAH FUNGSI ADD PRODUCT
      await Provider.of<Products>(context, listen: false)
          .addProduct(ProductItem(
        id: null,
        title: _titleController.text,
        stock: int.parse(_stockController.text),
        description: _descriptionController.text,
      ));
    //SELAIN ITU MAKA FUNGSI YANG DIJALANKAN ADALAH UPDATE PRODUCT
    } else {
      await Provider.of<Products>(context, listen: false)
          .updateProduct(ProductItem(
        id: id,
        title: _titleController.text,
        stock: int.parse(_stockController.text),
        description: _descriptionController.text,
      ));
    }
    //SET KEMBALI LOADING JADI FALSE
    setState(() {
      _isLoading = false;
    });
    //KEMBALI KE HALAMAN SEBELUMNYA
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(id == null ? 'Add New Product':'Edit Product'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.save),
            onPressed: _submit, //TOMBOL SIMPAN YANG KETIKA DITEKAN MAKA AKAN MENJALANKAN FUNGSI _submit YANG TELAH DIBUAT SEBELUMNYA
          )
        ],
      ),
      body: _isLoading
          ? Center(
              child: CircularProgressIndicator(), //KETIKA ISLOADING BERNILAI TRUE MAKA INDIKATOR LOADING AKAN DIRENDER
            )
          : Padding(
              padding: EdgeInsets.all(10),
              //SELAIN ITU AKAN MENAMPILKAN FORM INPUT
              child: Form(
                key: _form, //GLOBAL KEY YANG TELAH DIBUAT SEBELUMNYA DIGUNAKAN DISINI
                child: Column(
                  children: <Widget>[
                    //INPUTAN YANG MENGHANDLE NAMA BARANG
                    TextFormField(
                      decoration: InputDecoration(labelText: 'Nama Barang'),
                      textInputAction: TextInputAction.next,
                      onFieldSubmitted: (_) {
                        FocusScope.of(context).requestFocus(_priceDispose);
                      },
                      //VALIDASI INPUTAN BARANG
                      validator: (value) {
                        if (value.isEmpty) {
                          return 'Nama Barang Tidak Boleh Kosong';
                        }
                        return null;
                      },
                      controller: _titleController, //ADAPUN VALUENYA TELAH DIHANDLE OLEH CONTROLLER MASING-MASING
                    ),
                    TextFormField(
                      decoration: InputDecoration(labelText: 'Stok'),
                      textInputAction: TextInputAction.next,
                      keyboardType: TextInputType.number,
                      onFieldSubmitted: (_) {
                        FocusScope.of(context)
                            .requestFocus(_descriptionDispose);
                      },
                      validator: (value) {
                        if (value.isEmpty) {
                          return 'Stok Tidak Boleh Kosong';
                        }
                        if (int.tryParse(value) == null) {
                          return 'Value Harus Berisi Angkat';
                        }
                        if (int.parse(value) <= 0) {
                          return 'Stok Harus Lebih Besar Dari 0';
                        }
                        return null;
                      },
                      controller: _stockController,
                    ),
                    TextFormField(
                      maxLines: 3,
                      decoration: InputDecoration(labelText: 'Deskripsi'),
                      validator: (value) {
                        if (value.isEmpty) {
                          return 'Deskripsi Tidak Boleh Kosong';
                        }
                        return null;
                      },
                      controller: _descriptionController,
                    ),
                  ],
                ),
              ),
            ),
    );
  }
}

Reusable form input-an diatas sudah selesai, maka kita harus mendefinisikan routing dan juga membuat fungsi untuk simpan dan update data produk. Pertama kita definisikan routing-nya terlebih dahulu, buka file main.dart dan tambahkan code berikut pada bagian routes:

routes: {
  '/': (ctx) => ProductPage(),
  '/manage-product': (ctx) => MasterProductPage(), //TAMBAHKAN LINE INI
  '/add-product': (ctx) => AddNewProduct() //TAMBAHKAN LINE INI
},

Masih dengan file yang sama, tambahkan import statement:

import './pages/master_product_page.dart';
import './pages/add_new_product.dart';

Kemudian buka file lib/providers/products.dart dan tambahkan beberapa method berikut:

// [.. CODE SEBELUMNYA ..]

//METHOD YANG BERFUNGSI UNTUK MENGAMBIL DATA TUNGGAL BERDASARKAN ID
Future<ProductItem> findById(String id) async {
  final url = 'https://dw-inventory.firebaseio.com/products/$id.json';
  final response = await http.get(url);
  final convert = json.decode(response.body);
  //DATA TERSEBUT KITA RETURN MENGGUNAKAN FORM DARI CLASS PRODUCTITEM
  return ProductItem(id: id, title: convert['title'], description: convert['description'], stock: convert['stock']);
}

//METHOD YG BERFUNGSI UNTUK MENAMBAHKAN DATA BARU
Future<void> addProduct(ProductItem product) async {
  const url = 'https://dw-inventory.firebaseio.com/products.json';
  //METHOD YG DIGUNAKAN ADALAH POST DAN PADA PARAMETER KEDUA KITA KIRIMKAN DATANYA
  final response = await http.post(url,
      body: json.encode({
        'title': product.title,
        'stock': product.stock,
        'description': product.description
      }));
  //KEMUDIAN KITA ADD DATANYA KE LOCAL STATE AGAR TIDAK PERLU MELAKUKAN GET LG DARI SERVER
  _items.add(ProductItem(
    id: json.decode(response.body)['name'],
    title: product.title,
    stock: product.stock,
    description: product.description,
  ));
  notifyListeners(); //INFORMASIKAN UNTUK ME RE-RENDER WIDGET KARENA TERDAPAT DATA BARU
}

//METHOD YG BERFUNGSI UNTUK MENGURANGI STOK BERDASARKAN ID
Future<void> changeStock(String id) async {
  final url = 'https://dw-inventory.firebaseio.com/products/$id.json';
  final index = _items.indexWhere((prod) => prod.id == id);
  final stock = _items[index].stock - 1;

  await http.patch(url, body: json.encode({'stock': stock})); //UPDATE DATA DI SERVER

  //DAN UPDATE JUGA DI LOCAL STATE
  _items[index] = ProductItem(
    id: id,
    title: _items[index].title,
    description: _items[index].description,
    stock: stock,
  );
  notifyListeners();
}

//METHOD INI UNTUK MENGUPDATE SEMUA INFORMASI DATA YANG SEDANG DI EDIT
Future<void> updateProduct(ProductItem product) async {
  //BERDASARKAN ID PRODUCT
  final url = 'https://dw-inventory.firebaseio.com/products/${product.id}.json';
  //KEMUDIAN KITA KIRIMKAN PARAMETER DATA YANG INGIN DI PERBAHARUI
  await http.patch(url, body: json.encode({
    'title': product.title,
    'stock': product.stock,
    'description': product.description
  }));
  //KEMUDIAN KITA UPDATE JUGA LOCAL STATE
  final index = _items.indexWhere((prod) => prod.id == product.id);
  _items[index] = product;
  notifyListeners();
}

//HAPUS PRODUK BERDASARKAN ID
Future<void> removeProduct(String id) async {
  final url = 'https://dw-inventory.firebaseio.com/products/$id.json';
  await http.delete(url); //KIRIM PERMINTAAN KE SERVER
  _items.removeWhere((prod) => prod.id == id); //DAN HAPUS JUGA PADA LOCAL STATE
  notifyListeners();
}

Sampai pada tahap ini, semua part dari case sederhana aplikasi inventory telah dibuat. Jalankan code program diatas dengan menekan F5 pada VsCode atau ke menu Debug > Start Debugging.

Baca Juga: Aplikasi Laundry (Laravel 5.8 - Vue.js - SPA) #8: Manage Customers

Kesimpulan

Seria belajar State Management pada Flutter memiliki banyak cakupan yang telah kita pelajari, diantaranya bagaimana mengelola state secara global sehingga dapat digunakan dimana saja state tersebut dibutuhkan, pun juga berlaku hal yang sama dengan method yang berisi logic untuk berinteraksi dengan backend.

Selain itu, pembahasan mengenai HTTP request yang meliputi get, post, patch and delete juga sudah dibahas dengan tujuan untuk berinteraksi dengan backend dimana backend yang digunakan ada Firebase.

Adapun dokumentasi dari artikel ini dapat dilihat di Github.

Category:
Share:

Comments