- Laptop
- Internet
- Sudah terinstall
- Flutter SDK
- Android SDK
- iOS SDK (opsional - untuk perangkat iPhone/iPad)
- Dasar logika yang kuat
- Tekad belajar yang kuat
TODO 00
Buat project Flutter baru.
TODO 01
Tambahkan library (dependencies) http, yang akan berfungsi untuk memanggil REST API atau mendapatkan data yang dibutuhkan dari internet.
Tambahkan library (dependencies) charts_flutter, yang akan berfungsi untuk menampilkan data yang ada menjadi sebuah grafik dengan berbentuk pie chart, bar chart, line, dan lainnya.
TODO 02
(Opsional) Hapus semua kode yang ada pada file widget_test.dart
pada folder test
, dan ganti dengan fungsi main
saja seperti berikut,
void main() {}
TODO 03
Buat folder atau package baru di dalam folder lib
dengan nama config
, folder ini nantinya akan digunakan untuk menyimpan file konfigurasi seperti url utama untuk REST API, nama rute halaman, dan lain sebagainya.
Buat file dart baru pada folder config
dengan nama env.dart
, isi dengan kelas Env
dan beberapa nilai konstan seperti, baseUrl
adalah berisi url utama untuk pemanggilan API nantinya, homeRoute
adalah nama rute untuk halaman home, dan detailRoute
adalah nama rute untuk halaman detail. Sedangkan ChartsType
adalah sebuah enum
yang akan kita gunakan untuk mengganti tampilan dari bar charts ke pie charts.
class Env {
static final String baseUrl = "https://indonesia-covid-19.mathdro.id/api";
static final String homeRoute = "/home";
static final String detailRoute = "/detail";
}
enum ChartsType { BAR, PIE }
TODO 04
Buat folder atau package model
pada folder lib
, folder ini nantinya digunakan untuk menyimpan data model yang akan digunakan untuk menyimpan nilai dari data REST API yang didapatkan.
Buka url REST API berikut ini pada browser dan copy semua kode yang muncul, buka pada tab baru aplikasi Quicktype dan paste kode yang tadi pada bagian kiri, jangan lupa untuk mengganti nama kelasnya dan bahasanya menjadi Dart
.
(Opsional) Kamu juga bisa mencentang beberapa opsi yang ada pada Quicktype tadi sesuai dengan kebutuhan.
Copy kode yang ada pada Quicktype bagian kanan, lalu buat file dart baru pada folder model
dengan nama corona_province.dart
dan paste kode tadi ke dalam file tersebut.
TODO 05
Buat juga file dart dengan nama corona_cases.dart
pada folder model
dan isi dengan sebuah kelas model CoronaCases
untuk menyimpan title
dan count
, yang akan digunakan untuk model data pada grafik (charts) nantinya
class CoronaCases {
String title;
int count;
CoronaCases({
this.title,
this.count,
});
}
TODO 06
Buat folder atau package baru dengan nama network
pada folder lib
, folder ini nantinya digunakan untuk menyimpan file dart untuk koneksi ke internet.
Buat file dart baru pada folder network
dengan nama api_client.dart
, dan isi dengan fungsi untuk mendapatkan data dari internet (REST API). Buat fungsi tersebut menjadi static
agar fungsi tersebut dapat diakses langsung dari kelas (ApiClient.getCoronaProvince
), bukan ketika instance kelas (ApiClient().getCoronaProvince
), untuk lebih detailnya bisa coba baca disini. Lalu gunakan tipe data Future
dengan nilai T
nya adalah kelas data model yang sesuai dengan JSON yang nanti didapatkan, tipe Future
ini adalah tipe yang bersifat asynchronous atau nilai akan terisi setelah semua perintah di dalam fungsi dijalankan (tidak berurutan sesuai baris). Kemudian Response
adalah berisi nilai hasil dari pemanggilan url https://indonesia-covid-19.mathdro.id/api/provinsi dengan method GET
, lalu lakukan pengecekan jika status code dari response tersebut bernilai 200 atau artinya data berhasil didapatkan maka berikan nilai balik dari fungsi tersebut CoronaProvince.fromJson
yang diambil dari kelas model dari nilai T
fungsi tersebut, dan jika bukan 200 atau gagal maka lempar Exception
error code tersebut.
class ApiClient {
static Future<CoronaProvince> getCoronaProvince() async {
Response _response = await get("${Env.baseUrl}/provinsi");
if (_response.statusCode == 200) {
return CoronaProvince.fromJson(_response.body);
} else {
throw Exception("error code : ${_response.statusCode}");
}
}
}
TODO 07
Buat folder dengan nama view
di dalam folder lib
dan dua folder lagi di dalam folder view
yaitu detail
dan home
, folder ini nantinya digunakan untuk menyimpan file dart untuk UI atau tampilan pada aplikasi.
Tambahkan di masing-masing folder tersebut dua file dart, home_page.dart
dan home_content.dart
pada folder home
, sedangkan detail_page.dart
dan detail_content.dart
pada folder detail
.
Pada file home_page.dart
buat kelas HomePage
dengan StatelessWidget
, dan isi fungsi build
dengan Scaffold
, seperti berikut,
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
),
);
}
}
TODO 08
Pada file home_content.dart
, buat kelas HomeContent
dengan StatefulWidget
, tambahkan juga parameter CoronaProvince
atau kelas model yang sebelumnya dibuat pada kelas tersebut.
class HomeContent extends StatefulWidget {
final CoronaProvince coronaProvinceId;
const HomeContent({
Key key,
this.coronaProvinceId,
}) : super(key: key);
@override
_HomeContentState createState() => _HomeContentState();
}
class _HomeContentState extends State<HomeContent> {
@override
Widget build(BuildContext context) {
return Container();
}
}
Deklarasikan dua List
yang akan digunakan untuk menampilkan data nantinya, dan inisialisasikan datanya pada fungsi initState
. Dimana data pertama (_listCoronaCases
) adalah list untuk data keseluruhan (total) positif, sembuh, meninggal, dan perawatan, sedangkan data kedua (_listData
) adalah list data asli yang didapatkan dari REST API yang masih berupa data per provinsi. Dan untuk mendapatkan data keseluruhan (total) untuk positif, sembuh, dan meninggal, gunakan looping for in dari data per provinsi kemudian tambahkan nilainya ke dalam data keseluruhan, sedangkan untuk data perawatan didapatkan dari total positif dikurangi total sembuh dan meninggal.
[...]
List<CoronaCases> _listCoronaCases;
List<Datum> _listData;
@override
void initState() {
_listCoronaCases = <CoronaCases>[
CoronaCases(
title: "Positif",
count: 0,
),
CoronaCases(
title: "Sembuh",
count: 0,
),
CoronaCases(
title: "Meninggal",
count: 0,
),
CoronaCases(
title: "Perawatan",
count: 0,
),
];
_listData = widget.coronaProvinceId.listData;
for (Datum data in _listData) {
_listCoronaCases[0].count += data.positiveCases;
_listCoronaCases[1].count += data.curedCases;
_listCoronaCases[2].count += data.deathCases;
}
_listCoronaCases[3].count = _listCoronaCases[0].count;
_listCoronaCases[3].count -= _listCoronaCases[1].count;
_listCoronaCases[3].count -= _listCoronaCases[2].count;
super.initState();
}
[...]
TODO 09
Buat sebuah fungsi widget di dalam kelas _HomeContentState
, yang nantinya digunakan untuk item yang menampilkan data keseluruhan (total), berikan pula parameter untuk mendapatkan index dari item tersebut. Gunakan widget ListView
untuk menghindari error overflowed ketika ditampilkan pada layar yang terlalu kecil atau teks yang berlebih, berikan primary
dengan false
agar tidak dapat di scroll ketika ukuran mencukupi untuk ditampilkan, dan shrinkWrap
dengan true
agar ukuran mengikuti ukuran item di dalamnya.
[...]
Widget _itemGrid(int index) {
return Card(
elevation: 8.0,
margin: const EdgeInsets.all(16.0),
color: Colors.primaries[index],
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Center(
child: ListView(
primary: false,
shrinkWrap: true,
children: <Widget>[
Text(
"${_listCoronaCases[index].title}",
style: TextStyle(
fontSize: 32.0,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Text(
"${_listCoronaCases[index].count}",
style: TextStyle(
fontSize: 64.0,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
);
}
[...]
TODO 10
Pada fungsi build
kelas _HomeContentState
buat agar dapat memunculkan GridView
dan ListView
bersamaan di dalam Column
dan bungkus kembali dengan SingleChildScrollView
agar dapat digulirkan. Jangan lupa untuk atur primary
menjadi false
dan shrinkWrap
menjadi true
pada GridView
dan ListView
agar keduanya tidak dapat digulirkan dan mengikuti ukuran item masing-masing, karena sudah menggunakan SingleChildScrollView
untuk dapat digulirkan.
[...]
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
GridView.builder(
primary: false,
shrinkWrap: true,
padding: const EdgeInsets.all(16.0),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 512.0,
childAspectRatio: 1.5,
),
itemBuilder: (BuildContext context, int index) {
return _itemGrid(index);
},
itemCount: _listCoronaCases.length,
),
ListView.builder(
primary: false,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
title: Text(_listData[index].province),
trailing: Icon(Icons.chevron_right),
),
);
},
itemCount: _listData.length,
),
],
),
);
}
[...]
TODO 11
Pada file detail_page.dart
buat kelas DetailPage
dengan StatefulWidget
untuk halaman detail.
class DetailPage extends StatefulWidget {
@override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Container();
}
}
Pada kelas _DetailPageState
inisialisasikan _chartsType
dengan nilai ChartsType.BAR
untuk membuat tampilan default adalah berbentuk charts bar. Pada fungsi build
tangkap argumen yang dilempar dari halaman sebelumnya dan masukan ke dalam _data
. Buat juga IconButton
pada actions
di AppBar
, yang dimana ketika di klik ubah nilai _chartsType
, jika _chartsType
sebelumnya adalah BAR
maka isi dengan PIE
atau jika bukan isi dengan BAR
, jangan lupa untuk gunakan setState
setelahnya untuk trigger perubahan tampilan.
[...]
ChartsType _chartsType;
@override
void initState() {
_chartsType = ChartsType.BAR;
super.initState();
}
@override
Widget build(BuildContext context) {
Datum _data = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text(_data?.province ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(Icons.cached),
onPressed: () {
_chartsType = _chartsType == ChartsType.BAR
? ChartsType.PIE
: ChartsType.BAR;
setState(() {});
},
),
],
),
body: Card(
margin: const EdgeInsets.all(16.0),
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16.0),
),
),
);
}
[...]
TODO 12
Pada file detail_content.dart
buat kelas baru dengan nama DetailContent
. Berikan parameter data
yaitu data yang akan dimunculkan pada chats dan chartsType
yaitu tipe charts yang dimunculkan.
class DetailContent extends StatefulWidget {
final Datum data;
final ChartsType chartsType;
const DetailContent({
Key key,
this.data,
this.chartsType,
}) : super(key: key);
@override
_DetailContentState createState() => _DetailContentState();
}
class _DetailContentState extends State<DetailContent> {
@override
Widget build(BuildContext context) {
return Container();
}
}
TODO 13
Pada kelas _DetailContentState
inisialisasikan _listCases
dan _listSeries
, dimana _listCases
adalah data yang akan kita tampilkan pada charts
untuk satuannya, disini kita coba untuk munculkan data positif, sembuh, dan meninggal, sedangkan _listSeries
adalah kelompok data yang dibutuhkan untuk digunakan pada charts baik pada pie maupun bar, disini kita cukup gunakan satu kelopok atau satu Series
, dengan id
-nya adalah nama provinsi, lalu data yang gunakan adalah _listCases
, domainFn
adalah untuk judul setiap datanya (berbentuk String), measureFn
adalah data atau angka yang akan diukur (berbentuk int), dan labelAccessorFn
adalah untuk label setiap data (berbentuk String).
[...]
List<CoronaCases> _listCases;
List<Series<CoronaCases, String>> _listSeries;
@override
void initState() {
_listCases = <CoronaCases>[
CoronaCases(
title: "Positif",
count: widget.data.positiveCases,
),
CoronaCases(
title: "Sembuh",
count: widget.data.curedCases,
),
CoronaCases(
title: "Meninggal",
count: widget.data.deathCases,
),
];
_listSeries = <Series<CoronaCases, String>>[
Series(
id: widget.data.province,
data: _listCases,
domainFn: (CoronaCases cases, int index) {
return cases.title;
},
measureFn: (CoronaCases cases, int index) {
return cases.count;
},
labelAccessorFn: (CoronaCases cases, int index) {
return "${cases.count}";
},
),
];
super.initState();
}
[...]
TODO 14
Pada fungsi build
di _DetailContentState
buat jika chartsType
adalah BAR
maka tampikan BarCharts
dan jika bukan maka tampilkan PieCharts
. Pada BarCharts
, isi barRendererDecorator
untuk menampikan label di setiap data, dan atur posisi label selalu berada pada luar bar. Pada PieCharts
, isi defaultRenderer
untuk menampilkan label, dan atur posisi label selalu berada pada luar pie, tambahkan juga DatumLegend
pada behaviors
untuk memunculkan legenda pada charts tersebut.
[...]
@override
Widget build(BuildContext context) {
return widget.chartsType == ChartsType.BAR
? BarChart(
_listSeries,
barRendererDecorator: BarLabelDecorator<String>(
labelPosition: BarLabelPosition.outside,
),
)
: PieChart(
_listSeries,
defaultRenderer: ArcRendererConfig(
arcRendererDecorators: [
ArcLabelDecorator(
labelPosition: ArcLabelPosition.outside,
),
],
),
behaviors: [
DatumLegend(
showMeasures: true,
legendDefaultMeasure: LegendDefaultMeasure.sum,
),
],
);
}
[...]
TODO 15
Balik lagi ke DetailPage
, pada Container
yang ada di body
isi child
nya dengan DetailContent
dengan data parameter yang ada.
[...]
body: Card(
[...]
child: Container(
[...]
child: DetailContent(
data: _data,
chartsType: _chartsType,
),
),
),
[...]
TODO 16
Balik ke HomeContent
, tambahkan onTap
pada ListTile
yang ada pada ListView
. Dimana ketika di klik maka akan berpindah ke halaman DetailPage
dengan melempar argumen yaitu sebuah data.
[...]
ListView.builder(
[...]
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
[...]
onTap: () {
Navigator.pushNamed(
context,
Env.detailRoute,
arguments: _listData[index],
);
},
),
);
},
[...]
),
[...]
TODO 17
Pada HomePage
gunakan FutureBuilder
pada body
untuk mendapatkan dan menampilkan data dari REST API. Pada parameter future
isi dengan fungsi yang mendapatkan data dari internet, dengan nilai T
dari FutureBuilder
adalah nilai balik dari fungsi yang ada pada future
tersebut, begitu juga nilai T
pada AsyncSnapshot
. Pada builder
lakukan pengecekan jika snapshot
sudah memiliki data, maka tampilkan HomeContent
dengan parameter data
dari snapshot
tersebut, namun jika snapshot
masih kosong tampilkan CircularProgressIndicator
.
[...]
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Eudeka! Flutter x Corona"),
),
body: Container(
alignment: Alignment.center,
child: FutureBuilder<CoronaProvince>(
future: ApiClient.getCoronaProvinceId(),
builder: (
BuildContext context,
AsyncSnapshot<CoronaProvince> snapshot,
) {
if (snapshot.hasData) {
return HomeContent(
coronaProvinceId: snapshot.data,
);
} else {
return CircularProgressIndicator();
}
},
),
),
);
}
[...]
TODO 18
Pada file main.dart
ganti semua kode nya dengan kelas MainApp
, dimana pada kelas tersebut buat sebuah _routes
atau halaman-halaman yang ada pada aplikasi tersebut, karena kelas tersebut menggunakan StatelessWidget
maka variabel yang ada kita buat menjadi final
, dan isi initialRoute
dengan halaman HomePage
.
void main() {
runApp(
MainApp(),
);
}
class MainApp extends StatelessWidget {
final Map<String, WidgetBuilder> _routes = <String, WidgetBuilder>{
Env.homeRoute: (BuildContext context) {
return HomePage();
},
Env.detailRoute: (BuildContext context) {
return DetailPage();
},
};
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: _routes,
initialRoute: Env.homeRoute,
);
}
}
Notes:
Jangan lupa untuk menambahkan izin akses internet pada AndroidManifest.xml
ketika ingin menjalankan atau membuat aplikasi dalam mode release
.