Nusanet Developers
Published in

Nusanet Developers

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.

Referensi design

Pembuatan Projek

Untuk langkah pertamanya, silakan kita buat projek Flutter baru dengan nama flutter_mobile_attendance.

Buat Projek

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.

Persiapan Aset

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.

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.

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 Header

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,
),
),
],
);
}
Buat Widget Date Time

Jadi, untuk sementara widget date time-nya belum kita buat realtime. Kita fokus buat UI-nya dulu ya.

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 Button 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 History Presence

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,
),
),
],
),
],
),
);
}
Buat Widget Menu

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.

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.

Membuat Widget Date Time Realtime Menggunakan setState

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.

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.

Membuat Widget Date Time Realtime Menggunakan widget ValueListenableBuilder

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 😉.

--

--

Stories and insights from the developers in Nusanet

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store