Flutter: ValueListenableBuilder
Cara menggunakan widget ValueListenableBuilder
Pengenalan
Di Flutter untuk mengubah state sebuah halaman kita bisa menggunakan setState
. Selain setState
, kita juga bisa menggunakan state management agar kode kita lebih rapi dan gampang dibaca. Walaupun penggunaan setState
bisa dikatakan cukup mudah namun, jika kita salah dalam menggunakannya bisa-bisa performance app kita jadi jelek loh. Misalnya nih, kita mau mengubah widget Time yang ada di halaman Home. Let say widget Time ini nantinya kita mau buat realtime dimana setiap detik gitu jamnya berubah dengan format HH:mm:ss. Niat kita hanya ingin mengubah widget Time saja tapi, karena kita tidak tahu cara menggunakan setState
yang baik. Alhasil kita langsung panggil saja tuh setState
di halaman Home yang mana dampaknya adalah semua widget yang ada di halaman Home akan ter-build ulang. Terus pertanyaannya gimana caranya agar kita hanya bisa build ulang widget Time-nya saja? Sebenarnya ada banyak tekniknya namun, di tulisan kali ini saya akan contohkan caranya menggunakan widget ValueListenableBuilder
.
Design
Biar pembahasan kali ini lebih menarik saya mau sekalian cari referensi design UI yang mau kita jadikan bahan di tulisan ini. Jadi, untuk referensi design-nya saya ada ambil dari dribbble ya.
Untuk design-nya nanti kita cuma akan pakai yang bagian ini aja ya.
Pembuatan Projek
Buat Projek
Untuk langkah pertamanya, silakan kita buat projek Flutter baru dengan nama flutter_mobile_attendance.
Persiapan Aset
Sebelum kita mulai membuatnya. Kita memerlukan aset foto profil. Untuk foto profilnya bebas mau pakai yang mana ya. Tapi, kali ini saya akan pakai salah satu foto dari website pexels.com. Setelah kita unduh fotonya kemudian, kita masukkan foto tersebut kedalam projek kedalam directory assets.
Selanjutnya, kita daftarkan directory assets kedalam file pubspec.yaml. Dan kita juga ada pakai plugin intl.
name: flutter_mobile_attendance
description: How to use ValueListenableBuilder
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.15.1 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
intl: ^0.17.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/
Selanjutnya, kita jalankan perintah flutter pub get
.
Buat Kerangka UI
Sekarang kita buka file main.dart dan ubah kode didalamnya menjadi seperti berikut.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: const MyHomePage(title: 'Flutter: ValueListenableBuilder'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var paddingTop = 0.0;
var paddingBottom = 0.0;
var widthScreen = 0.0;
var now = DateTime.now();
Timer? timer;
@override
Widget build(BuildContext context) {
final mediaQueryData = MediaQuery.of(context);
widthScreen = mediaQueryData.size.width;
paddingTop = mediaQueryData.padding.top;
paddingBottom = mediaQueryData.padding.bottom;
return Scaffold(
body: Container(
width: double.infinity,
color: Colors.white,
padding: EdgeInsets.fromLTRB(
16,
paddingTop + 16,
16,
paddingBottom > 0 ? paddingBottom : 16,
),
child: Column(
children: [
buildWidgetHeader(),
const SizedBox(height: 24),
buildWidgetDateTime(),
buildWidgetButtonPresence(),
buildWidgetHistoryPresence(),
const SizedBox(height: 24),
buildWidgetMenu(),
],
),
),
);
}
Widget buildWidgetHeader() {
// TODO: buat widget yang menampilkan info nama dan foto profil si user
return Container();
}
Widget buildWidgetDateTime() {
// TODO: buat widget yang menampilkan tanggal dan jam sekarang
return Container();
}
Widget buildWidgetButtonPresence() {
// TODO: buat widget yang menampilkan button presence
return Container();
}
Widget buildWidgetHistoryPresence() {
// TODO: buat widget yang menampilkan button ijin tidak hadir dan history presensi
return Container();
}
Widget buildWidgetMenu() {
// TODO: buat widget yang menampilkan menu-nya
return Container();
}
}
Nah, jadi kode diatas adalah kerangka dari UI yang akan kita buat.
Buat Widget Header
Sekarang kita akan coba membuat widget bagian header-nya dimana, dibagian tersebut kita akan menampilkan nama dan foto profil si pengguna.
Silakan kita ubah kode berikut.
Widget buildWidgetHeader() {
// TODO: buat widget yang menampilkan info nama dan foto profil si user
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetHeader() {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang',
style: Theme.of(context).textTheme.headline6?.copyWith(
fontWeight: FontWeight.normal,
),
),
Text(
'Putri',
style: Theme.of(context).textTheme.headline6,
),
],
),
),
ClipOval(
child: Image.asset(
'assets/photo_profile.jpeg',
width: 42,
height: 42,
fit: BoxFit.cover,
),
),
],
);
}
Jika dijalankan maka, outputnya akan menjadi seperti berikut.
Buat Widget Date Time
Langkah selanjutnya adalah kita akan membuat widget yang menampilkan jam dan tanggal saat ini. Untuk membuatnya, silakan ubah kode berikut.
Widget buildWidgetDateTime() {
// TODO: buat widget yang menampilkan tanggal dan jam sekarang
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetDateTime() {
final formattedTime = DateFormat('HH:mm', 'id').format(now);
final formattedTime2 = DateFormat(':ss', 'id').format(now);
final formattedDate = DateFormat('EEEE, dd MMM yyy', 'id').format(now);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: [
Text(
formattedTime,
style: Theme.of(context).textTheme.headline4?.copyWith(
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
),
Text(
formattedTime2,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
),
],
),
Text(
formattedDate,
style: Theme.of(context).textTheme.headline6?.copyWith(
color: Colors.grey,
),
),
],
);
}
Jadi, untuk sementara widget date time-nya belum kita buat realtime. Kita fokus buat UI-nya dulu ya.
Buat Widget Button Presence
Nah, di langkah berikutnya kita akan membuat widget button presence. Silakan kita ubah kode berikut.
Widget buildWidgetButtonPresence() {
// TODO: buat widget yang menampilkan button presence
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetButtonPresence() {
return Expanded(
child: Container(
width: widthScreen / 1.5,
height: widthScreen / 1.5,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.bottomLeft,
end: Alignment.topRight,
colors: [
Colors.orange[900]!,
Colors.orange,
Colors.orange[200]!,
],
),
boxShadow: [
BoxShadow(
color: Colors.orange.withOpacity(0.3),
offset: const Offset(0, 8),
blurRadius: 8,
spreadRadius: 8,
),
],
),
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.fingerprint,
color: Colors.white,
size: widthScreen / 3,
),
Text(
'Absen Masuk',
style: Theme.of(context).textTheme.subtitle2?.copyWith(
color: Colors.white,
),
),
],
),
),
);
}
Buat Widget History Presence
Langkah selanjutnya kita akan membuat widget history presence. Silakan ubah kode berikut.
Widget buildWidgetHistoryPresence() {
// TODO: buat widget yang menampilkan button ijin tidak hadir dan history presensi
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetHistoryPresence() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey),
),
alignment: Alignment.center,
padding: const EdgeInsets.all(12),
child: const Icon(
Icons.remove_circle,
color: Colors.red,
),
),
const SizedBox(height: 8),
const Text('Ijin Tidak Hadir'),
],
),
Column(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey),
),
alignment: Alignment.center,
padding: const EdgeInsets.all(12),
child: const Icon(
Icons.article_rounded,
color: Colors.orange,
),
),
const SizedBox(height: 8),
const Text("Lihat History"),
],
),
],
);
}
Buat Widget Menu
Nah, ini widget terakhir yang akan kita buat. Untuk membuatnya, silakan ubah kode berikut.
Widget buildWidgetMenu() {
// TODO: buat widget yang menampilkan menu-nya
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetMenu() {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
const Icon(
Icons.fingerprint,
color: Colors.orange,
),
const SizedBox(height: 4),
Text(
'Absensi',
style: Theme.of(context).textTheme.subtitle2?.copyWith(
fontSize: 12,
color: Colors.orange,
),
),
],
),
Column(
children: [
const Icon(
Icons.local_activity,
color: Colors.grey,
),
const SizedBox(height: 4),
Text(
'Kegiatan',
style: Theme.of(context).textTheme.subtitle2?.copyWith(
fontSize: 12,
color: Colors.grey,
),
),
],
),
Column(
children: [
const Icon(
Icons.help,
color: Colors.grey,
),
const SizedBox(height: 4),
Text(
'Problem',
style: Theme.of(context).textTheme.subtitle2?.copyWith(
fontSize: 12,
color: Colors.grey,
),
),
],
),
Column(
children: [
const Icon(
Icons.location_on,
color: Colors.grey,
),
const SizedBox(height: 4),
Text(
'Lokasi',
style: Theme.of(context).textTheme.subtitle2?.copyWith(
fontSize: 12,
color: Colors.grey,
),
),
],
),
],
),
);
}
Bisa kita lihat sekarang bahwa UI-nya sudah selesai kita buat. Namun, kalau diperhatikan di sini masih ada yang kurang ya. Yaitu, kita mau widget date time-nya itu jalan atau dibuat realtime.
Buat Widget Date Time menjadi Realtime
Oke, sekarang akan kita buat ya. Tapi, sebelum kita buat. Saya mau tunjukkan dulu bahwa untuk membuatnya saya akan tunjukkan dengan 2 cara yaitu, cara pertama dengan menggunakan setState
dan cara kedua menggunakan widget ValueListenableBuilder
. Sengaja saya tunjukkan biar kita semua tahu mana yang lebih praktis dan efisien dalam penggunaannya untuk kasus seperti ini.
Menggunakan setState
Nah, untuk cara pertama kita akan coba menggunakan setState
. Caranya, silakan kita update kode didalam file main.dart dan kita perlu override function initState
dan dispose
menjadi seperti berikut.
@override
void initState() {
timer = Timer.periodic(const Duration(seconds: 1), (_) {
now = now.add(const Duration(seconds: 1));
setState(() {});
});
super.initState();
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
Kemudian, untuk membuktikan bahwa method build
benar-benar ter-build ulang ketika timer-nya jalan kita perlu tambahkan kode berikut didalam method build
.
@override
Widget build(BuildContext context) {
print('Ini method build');
final mediaQueryData = MediaQuery.of(context);
widthScreen = mediaQueryData.size.width;
paddingTop = mediaQueryData.padding.top;
paddingBottom = mediaQueryData.padding.bottom;
return Scaffold(
...
Sekarang kalau kita jalankan programnya maka, kita sudah bisa lihat bahwa widget date time-nya sudah berjalan secara realtime.
Tapi, kalau kita perhatikan kodenya. Cara pertama kuranglah efisien. Mengapa berani saya katakan kurang efisien? Karena, ketika kita menggunakan setState
maka, tanpa kita sadari bahwa semua widget yang ada didalam method build
akan terbuat ulang semuanya. Dan ini kurang efektif mengingat kembali bahwa tujuan diawalnya kita hanya mau meng-update state dari widget Date Time saja. Widget yang lain tidak perlu diupdate. Memang di contoh kasus kali ini kita tidak melihat secara langsung efeknya tapi, kalau UI-nya sudah cukup kompleks maka, itu akan sangat kelihatan sekali bahwa jank atau freeze-nya terjadi. Jadi, untuk cara pertama ini kurang efektif ya.
Menggunakan Widget ValueListenableBuilder
Oke sekarang kita coba cara kedua ya. Untuk cara kedua kita akan menggunakan widget yang bernama ValueListenableBuilder
. Jadi, kalau kita baca-baca secara singkat dokumentasinya. Disebutkan bahwa widget ini berfungsi untuk build ulang widget yang ada didalamnya ketika nilai yang kita berikan berubah. Agak susah dimengerti ya? Oke, jadi bahasa gampangnya adalah widget ini hanya ter-build ulang kalau nilai didalamnya juga berubah.
Biar lebih gampang. Akan saya tunjukkan gimana cara buatnya. Pertama-tama kita hapus dulu tuh perintah setState
yang ada didalam method initState
. Sekaligus hapus variable yang bernama now
.
Timer? timer;
@override
void initState() {
timer = Timer.periodic(const Duration(seconds: 1), (_) {
// TODO: do something in here
});
super.initState();
}
Kemudian, kita perlu buat 1 variable global dengan nama valueNotifierNow
. Dan kita inisialisasikan nilainya didalam method initState
.
Timer? timer;
late ValueNotifier<DateTime> valueNotifierNow;
@override
void initState() {
valueNotifierNow = ValueNotifier<DateTime>(DateTime.now());
timer = Timer.periodic(const Duration(seconds: 1), (_) {
valueNotifierNow.value = DateTime.now();
});
super.initState();
}
Untuk saat ini kita akan melihat bahwa kode kita mengalami error tapi, it’s okay. Kita abaikan dulu ya. Sekarang kita ubah kode bagian ini.
child: Column(
children: [
buildWidgetHeader(),
const SizedBox(height: 24),
buildWidgetDateTime(),
buildWidgetButtonPresence(),
buildWidgetHistoryPresence(),
const SizedBox(height: 24),
buildWidgetMenu(),
],
),
Menjadi seperti ini.
child: Column(
children: [
buildWidgetHeader(),
const SizedBox(height: 24),
ValueListenableBuilder<DateTime>(
valueListenable: valueNotifierNow,
builder: (BuildContext context, DateTime now, Widget? child) {
return buildWidgetDateTime(now);
},
),
buildWidgetButtonPresence(),
buildWidgetHistoryPresence(),
const SizedBox(height: 24),
buildWidgetMenu(),
],
),
Lalu, kita tambahkan parameter didalam method buildWidgetDateTime
.
Widget buildWidgetDateTime(DateTime now) {
final formattedTime = DateFormat('HH:mm', 'id').format(now);
final formattedTime2 = DateFormat(':ss', 'id').format(now);
final formattedDate = DateFormat('EEEE, dd MMM yyy', 'id').format(now);
return Column(
...
Sekarang coba kita jalankan lagi app-nya maka, outputnya akan sama yaitu, waktu akan berjalan secara realtime. Mungkin kalau cuma dari UI-nya saja kita tidak percaya ya kalau yang ter-build ulang hanyalah method buildWidgetDateTime
. Untuk membuktikannya silakan kita tambahkan kode print
berikut didalam method buildWidgetDateTime
.
...Widget buildWidgetDateTime(DateTime now) {
print('ini method buildWidgetDateTime');
final formattedTime = DateFormat('HH:mm', 'id').format(now);
final formattedTime2 = DateFormat(':ss', 'id').format(now);
final formattedDate = DateFormat('EEEE, dd MMM yyy', 'id').format(now);
return Column(
...
Sekarang coba kita jalankan programnya dan buka DevTools untuk melihat output di console-nya apakah yang ter-build hanya method buildWidgetDateTime
saja atau method build
juga ikut ter-build.
Nah, lihat kan perbedaannya? Kalau pakai widget ValueListenableBuilder
maka, yang ter-build hanyalah method buildWidgetDateTime
-nya saja.
Kesimpulan
Jadi, di tulisan ini kita telah mempelajari mengenai penggunaan widget ValueListenableBuilder
. Jadi, widget ValueListenableBuilder
ini berfungsi untuk membuat widget yang kita inginkan hanya ter-build ulang jika nilai yang diberikan berubah. Jadi, lebih efektif daripada setState
jikalau kita hanya ingin mem-build ulang widget tertentu. Untuk source code lengkapnya bisa dicek di Github ya 😉.