Aplikasi Tracking Resi Ekspedisi - Flutter

Aplikasi Tracking Resi Ekspedisi - Flutter

Pendahuluan

Cara menarik dan efektif yang sebaiknya digunakan dalam belajar bahasa pemrograman adalah dengan membiasakan diri menyelesaikan sebuah case secara bertahap, mulai dari yang sederhana hingga aplikasi yang cukup kompleks. Maka pada artikel kali ini kita akan belajar membuat aplikasi Tracking resi dari salah satu ekspedisi yang sering digunakan di Indonesia.

Berinteraksi dengan API yang dimiliki oleh ekspedisi tersebut akan mengajarkan kita bagaimana cara meminta data dari server, menerima datanya dan melakukan formatting agar dapat digunakan sesuai dengan kebutuhan.

Adapun schema yang diinginkan adalah menampilkan 3 buah layer, pertama adalah layer form input beserta tombol cari, kedua adalah card untuk menampilkan informasi jika resi ditemukan dan yang terakhir adalah card untuk menampilkan informasi jika resi tidak ditemukan.

Baca Juga: Belajar Flutter Basic #8: State Management HTTP Request

Create Layout Tracking

Langkah awal yang akan dilakukan adalah membuat layout sebelum memasukkan perintah berdasarkan fungsinya masing-masing. Pertama install Flutter terlebih dahulu dengan command:

flutter create dw_tracking_resi

Tunggu hingga proses instalasi selesai, kemudian buka project-nya dan modifikasi file lib/main.dart menjadi:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Daengweb.id',
      theme: ThemeData(
        primarySwatch: Colors.pink,
      ),
      home: ResiSilincah(),
    );
  }
}

Note: Code diatas hanya berisi material app yang telah di-set untuk me-load class ResiSilincah().

Buat file silincah.dart di dalam folder lib dan tambahkan code:

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

class ResiSilincah extends StatefulWidget {
  @override
  _ResiSilincahState createState() => _ResiSilincahState();
}

class _ResiSilincahState extends State<ResiSilincah> {
  //DEFINISIKAN VARIABLE
  String noResi = '';
  String layanan = '';
  String name = '';
  String status = '';
  String tujuan = '';
  String date = '';
  String penerima = '';

  //VARIABLE DENGAN TIPE BOOLEAN
  bool loading = false;
  bool showDetail = false;
  bool isNotFound = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(leading: Icon(Icons.art_track), title: Text('Daengweb Resi')),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch, //WIDGET YANG ADA DI DALAM COLUMN AKAN DI SET WIDTHNYA SEJAUH YG BISA DIJANGKAU
            children: <Widget>[

              // FORM INPUT PENCARIAN RESI DISINI
            
              Divider(),
              
              //KETIKA VARIABLE LOADING BERNILAI TRUE
              loading
                  ? Center(
                      child: CircularProgressIndicator(), //MAKA AKAN MENAMPILKAN INDIKATOR LOADING
                    )
                  //JIKA BERNILAI FALSE
                  : showDetail //DI CEK LAGI, JIKA SHOWDETAIL BERNILAI TRUE
                      ? Detail(
                          noResi, layanan, name, status, tujuan, date, penerima) //MAKA COMPONENT DETAIL DI RENDER
                      //JIKA SHOW DETAIL BERNILAI FALSE
                      : isNotFound //DI CEK LAGI, JIKA IS NOT FOUND TRUE
                          ? Card( //MAKA KITA TAMPILKAN CARD YANG BERISI INFORMASI BAHWA RESI TIDAK DITEMUKAN
                              elevation: 4,
                              child: Center(
                                child: Padding(
                                  padding: const EdgeInsets.all(8.0),
                                  child: Text("Nomor Resi Tidak Ditemukan", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
                                ),
                              ),
                            )
                          : Center() //SELAIN ITU HANYA MERENDER WIDGET CENTER
            ],
          ),
        ),
      ),
    );
  }
}

Note: Comment FORM INPUT PENCARIAN RESI DISINI nantinya akan di-replace dengan widget textfield dan button.

Bagian terakhir dari layouting adalah membuat custom Widget yang berisi card untuk menampilkan detail informasi apabila resi ditemukan. Didalam folder yang sama, buat file detail.dart dan tambahkan code:

import 'package:flutter/material.dart';

class Detail extends StatelessWidget {
  //DEFINISIKAN VARIABLE YANG SAMA
  String noResi = '';
  String layanan = '';
  String name = '';
  String status = '';
  String tujuan = '';
  String date = '';
  String penerima = '';

  //CONSTRUCTOR YANG BERARTI KETIKA ME-LOAD FILE INI, DIA AKAN MEMINTA DATA
  Detail(this.noResi, this.layanan, this.name, this.status, this.tujuan, this.date, this.penerima);
  
  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListTile( //DETAIL INFORMASINYA KITA GUNAKAN LISTTILE
          title: Text( //PADA BAGIAN TITLE DIGUNAKAN UNTUK MENAMPILKAN RESI DAN JENIS LAYANANNYA
            '${noResi} - $layanan',
            style: TextStyle(
                fontWeight: FontWeight.bold, fontSize: 18),
          ),
          subtitle: Padding(
            //SEDANGKAN PADA BAGIAN SUBTITLE (TEPAT DIBAWAH TITLE), KITA TAMPILKAN INFORMASI LAINNYA
            padding: const EdgeInsets.only(top: 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Divider(),
                //BAGIAN PERTAMA ADALAH NAMA PENERIMA
                Text(
                  'Nama: $name',
                  style: TextStyle(
                      color: Colors.black87, fontSize: 13),
                ),
                //KEMUDIAN STATUS PENGIRIMAN
                Text(
                  'Status: $status',
                  style: TextStyle(
                      color: Colors.black87, fontSize: 13),
                ),
                //TUJUAN PENGIRIMAN
                Text(
                  'Tujuan: $tujuan',
                  style: TextStyle(
                      color: Colors.black87, fontSize: 13),
                ),
                //TANGGAL BARANG DITERIMA
                Text(
                  'Tgl Terima: $date',
                  style: TextStyle(
                      color: Colors.black87, fontSize: 13),
                ),
                //DAN SIAPA NAMA PENERIMANYA
                Text(
                  '$penerima',
                  style: TextStyle(
                      color: Colors.black87, fontSize: 13),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Form Input & HTTP Request

Setelah menyelesaikan layout yang akan menampilkan informasi baik resi ditemukan atau tidak ditemukan, maka tugas selanjutnya adalah membuat form input yang berfungsi untuk memasukkan nomor resi yang akan dicari. Buka file silincah.dart dan tambahkan code berikut tepat pada komentar form input

Container(
  padding: const EdgeInsets.all(8),
  child: TextField(
    focusNode: _focusNode,
    controller: _resiController, //UNTUK MENG-HANDLE VALUENYA KITA GUNAKAN CONTROLLER
    decoration: InputDecoration(
        border: OutlineInputBorder(),
        hintText: 'Masukkan Resi',
        labelText: 'Nomor Resi'),
  ),
),
Padding(
  padding: const EdgeInsets.all(8.0),
  child: MaterialButton(
    child: Text("Cari"),
    highlightElevation: 2,
    color: Colors.redAccent,
    textColor: Colors.white,
    onPressed: cekResi, //KETIKA TOMBOLNYA DITEKAN, MAKA AKAN MENJALANKAN FUNGSI cekResi
  ),
),

Masih dengan file yang sama, kita buat initState serta controller dan focus node, kemudian letakkan diluar dari Widget build()

final TextEditingController _resiController = TextEditingController();
FocusNode _focusNode;

@override
void initState() {
  _focusNode = FocusNode();
  _focusNode.addListener(() {
    if (_focusNode.hasFocus) _resiController.clear();
  });
  super.initState();
}

Tugas selanjutnya adalah membuat method cekResi() untuk meng-handle proses http request ke API ekspedisi terkait. Tentu saja dengan file yang sama tambahkan code:

void cekResi() async {
  setState(() {
    //RESET SHOW DETAIL DAN ISNOTFOUND MENJADI FALSE
    showDetail = false;
    isNotFound = false;
    loading = true; //SET LOADING JADI TRUE AGAR LOADING INDICATOR DI RENDER
  });

  //KEMUDIAN BUAT HEADERS DIMANA TIPENYA ADALAH MAP DENGAN KEY STRING DAN VALUE STRING
  //HEADER INI DIGUNAKAN KETIKA MENGIRIM REQUEST KE API UNTUK MENGHINDARI BLOCKING DARI API TERKAIT
  Map<String, String> requestHeaders = {
    'Accept': 'application/json, text/javascript, */*; q=0.01',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'en-US,en;q=0.9',
    'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Host': 'sicepat.com',
    'Origin': 'http://sicepat.com',
    'Referer': 'http://sicepat.com',
    'User-Agent':
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
    'Cookie':
        '__cfduid=d4a82874e43363ef776bd20169c2e37ca1568383926; ci_session=nphsvktt6jl68lu74er1osttbfc4ct9d',
    'X-Requested-With': 'XMLHttpRequest'
  };

  //BUAT JUGA VARIABLE LAINNYA DENGAN TIPE MAP DAN KEY STRING, VALUE STRING
  //DATA INI DIGUNAKAN SEBAGAI BODY KETIKA MENGIRIMKAN REQUEST KE API
  Map<String, String> resi = {'awb[]': _resiController.text};

  //LAKUKAN REQUEST KE API DENGAN MENYERTAKAN BODY = RESI DAN HEADERS = REQUESTHEADERS
  final response = await http.post('http://sicepat.com/checkAwb/doSearch',
      body: resi, headers: requestHeaders);
  //DECODE HASILNYA DAN HANYA MENGAMBIL VALUE DARI KEY HTML
  final content = json.decode(response.body)['html'];
  
  //KEMUDIAN VALUE DARI CONTENT KITA REMOVE BEBERAPA BAGIAN YANG TIDAK DIBUTUHKAN DENGAN BANTUAN METHOD BARU YANG BERNAMA removeHeading() DIMANA METHOD INI AKAN KITA BUAT KEMUDIAN
  final withoutHtml = removeHeading(content);

  setState(() {
    loading = false; //SET LOADING JADI FALSE KARENA PROSES REQUESTNYA SDH BERHASIL
    if (withoutHtml.length > 1) { //JIKA RESINYA ADA
      //MAKA DATA DARI API TERSEBUT KITA ASSIGN KE DALAM VARIABLE YANG TELAH DIBUAT SEBELUMNYA
      //KARENA DATANYA MENGGUNAKAN FORMAT HTML, MAKA KITA PERLU MEMECAH DATANYA DAN MENGHILANGKAN HTML CODENYA
      //KITA JUGA AKAN MEMBUAT METHOD BARU BERNAMA withoutHtml YANG BERFUNGSI UNTUK MEMECAH DATA
      noResi = explodeItem(withoutHtml[1], '<div class="visible-xs">', 0);
      layanan = explodeItem(withoutHtml[1], '<div class="visible-xs">', 1);
      name = explodeItem(withoutHtml[4], '<td class="hidden-xs">', 1);
      status = explodeItem(withoutHtml[7], '<td>', 1);
      tujuan = explodeItem(withoutHtml[3], '<div class="visible-xs">', 0);
      date = explodeItem(withoutHtml[5], '<div class="visible-xs">', 0);
      penerima = explodeItem(withoutHtml[5], '<div class="visible-xs">', 1);
      showDetail = true; //SET SHOW DETAIL JADI TRUE AGAR CARD INFORMASI RESI DIRENDER
    } else {
      isNotFound = true; //IJKA RESI TIDAK DITEMUKAN, SET IS NOT FOUND JADI TRUE
    }

    FocusScope.of(context).nextFocus(); //HILANGKAN FOCUS DARI TEXT FIELD
    _resiController.clear(); //DAN BERSIHKAN VALUENYA
  });
  return;
}

Ada dua method bantuan yang akan meng-handle data dari API karena data tersebut tidak menggunakan format json. Tambahkan kedua method tersebut setelah method cekResi()

List removeHeading(String htmlText) { //METHOD PERTAMA NILAI BALIKNYA DENGAN FORMAT LIST (ARRAY)
  
  //METHOD INI MENERIMA VALUE DARI RESPONSE API, KEMUDIAN CODE HTML DIBAWAH DIHAPUS MENGGUNAKAN REPLACE ALL
  final removeHeading = htmlText.replaceAll(
      '<div class=\"awb-detail-title text-center\">       <div class=\"container\">           Status Pengiriman Anda       </div>   </div>   <div class=\"awb-detail-sub-title text-center\">       <div class=\"container\">           Terimakasih telah menggunakan pengiriman SiCepat. Silahkan cek daftar pengiriman Anda       </div>   </div><span class=\"awb-click-info\">Silahkan klik salah satu baris untuk melihat detail pengiriman.</span>   <div class=\"table-responsive\"><table id=\"awb-list\" class=\"table table-striped  nowrap ws-table\"\n                 cellspacing=0 width=\"100%\">  <thead><tr> <th  id=\"seq\" class=\"hidden-xs\">No</th> <th  id=\"awb_number\" class=\"\">No RESI</th> <th  id=\"service\" class=\"hidden-xs\">Layanan</th> <th  id=\"destination\" class=\"\">Tujuan</th> <th  id=\"receipt_name\" class=\"hidden-xs\">Penerima</th> <th  id=\"receipt_date\" class=\"\">Tgl Diterima</th> <th  id=\"receipt_paket\" class=\"hidden-xs\">Penerima Paket</th> <th  id=\"status\" class=\"\">STATUS</th></tr>  </thead><tbody><tr class=\"res-item\"> ',
      '');
  
  //SETELAH BAGIAN YANG TIDAK DIPERLUKAN DIHAPUS, KITA PECAH DATANYA MENGGUNAKAN SPLIT DENGAN </TD> SEBAGAI PARAMETERNYA
  final splitTd = removeHeading.split('</td>');
  return splitTd.toList(); //KEMUDIAN KEMBALIKAN VALUE YANG BARU DALAM BENTUK LIST
}

String explodeItem(String str, String remove, int index) { //METHOD INI AKAN MENGHASILKAN STRING SEBAGAI NILAI BALIK
  //PARAMETER PERTAMA ADALAH STRING YANG AKAN DIPECAH
  //PARAMETER KEDUA KEDUA ADALAH PEMECAHNYA. CONTOH: PADA METHOD SEBELUMNYA KITA GUNAKAN </TD>, KARENA METHOD INI REUSABLE JADI KITA BUAT DALAM BENTUK VARIABLE JADI TERGANTUNG REQUEST
  //PARAMETER KETIGA INDEX DATA YANG MAU DIAMBIL
  
  //KITA PECAH STRINGNYA MENGGUNAKAN PARAMETER REMOVE DAN HASILNYA DIUBAH JADI LIST
  final result = str.split(remove).toList();

  RegExp exp = RegExp(r"<[^>]*>", multiLine: true, caseSensitive: true); //BUAT REGEX UNTUK MENGIDENTIFIKASI HTML

  return result[index].replaceAll(exp, ''); //REPLACE SEMUA HTML CODE 
}

Bagian terakhir adalah meng-install package http. Buka file pubspec.yaml dan tambahkan code dibawah di dalam part dependecies

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.0+2 #TAMBAHKAN CODE INI

Kemudian pada console, jalankan command:

flutter pub get

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

Kesimpulan

Case membuat Aplikasi tracking resi ekspedisi telah memberikan kita beberapa pelajaran menarik dalam memahami Flutter. Dalam case ini saja kita telah belajar bagaimana meminta data ke-server, mengelola response yang diberikan, menampilkan progress loading, me-render informasinya menggunakan Card dan serta cara install external package.

Adapun dokumentasi code dari artikel ini bisa kamu lihat di Github.

Category:
Share:

Comments