✦ Selamat Idul Fitri 1447 H πŸŒ™ Taqabbalallahu minna wa minkum. Mohon maaf lahir dan batin. ✦
ALFAkwt

Membuat CustomPainter yang Efisien di Flutter: Pelajaran dari Proyek Nyata

Β· 0 komentar Β· Β± 6 menit baca Β· πŸ‘ 405 dilihat
Contoh hasil CustomPainter untuk ornamen dekoratif aplikasi Flutter

Pertama kali saya mencoba CustomPainter, hasilnya adalah widget yang membekukan layar setiap kali ada interaksi. Scroll terasa berat, animasi patah-patah, dan profiler menunjukkan frame time yang memalukan.

Masalahnya bukan di Flutter. Masalahnya ada di cara saya menggunakannya.

CustomPainter adalah salah satu fitur Flutter yang paling powerful sekaligus paling mudah disalahgunakan. Dengan pemahaman yang tepat tentang bagaimana Flutter rendering pipeline bekerja, Anda bisa menghasilkan grafis kustom yang kompleks tanpa mengorbankan performa. Artikel ini adalah panduan praktis untuk itu.


Bagaimana Flutter Memutuskan Kapan Harus Repaint

Ini adalah hal yang paling penting untuk dipahami sebelum menulis satu baris pun di CustomPainter.

Setiap frame, Flutter memanggil method shouldRepaint() pada painter Anda. Kalau method ini mengembalikan true, Flutter akan memanggil paint() β€” yang berarti semua operasi menggambar dieksekusi ulang dari awal. Kalau false, Flutter menggunakan cache dari frame sebelumnya.

Kesalahan paling umum yang saya lihat:

// SALAH β€” akan repaint setiap frame tanpa kecuali
@override
bool shouldRepaint(MyPainter old) => true;

Ini adalah cara paling cepat untuk membunuh performa aplikasi Anda. Flutter akan menggambar ulang painter ini setiap 16ms (untuk 60fps), bahkan ketika tidak ada yang berubah sama sekali.

Yang benar:

// BENAR β€” repaint hanya ketika data yang relevan berubah
@override
bool shouldRepaint(MyPainter old) {
return old.warnaPrimer != warnaPrimer ||
old.nilai != nilai ||
old.progress != progress;
}

Bandingkan hanya properti yang benar-benar mempengaruhi output visual. Tidak lebih, tidak kurang.


Paint Object: Jangan Buat di Dalam paint()

Ini adalah kesalahan kedua yang sangat umum. Object Paint relatif mahal untuk dibuat. Kalau Anda membuatnya di dalam method paint(), ia akan dibuat ulang setiap kali painter dijalankan.

// KURANG EFISIEN
@override
void paint(Canvas canvas, Size size) {
final paint = Paint() // dibuat ulang setiap frame
..color = Colors.blue
..strokeWidth = 2.0;
canvas.drawCircle(center, radius, paint);
}
// LEBIH EFISIEN β€” definisikan Paint sebagai field
class MyPainter extends CustomPainter {
final _paint = Paint()
..color = Colors.blue
..strokeWidth = 2.0;
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(center, radius, _paint);
}
}

Untuk painter yang sederhana, perbedaannya mungkin tidak terukur. Tapi untuk painter yang kompleks dengan banyak objek Paint dan dipanggil 60 kali per detik, ini membuat perbedaan nyata.


RepaintBoundary: Isolasi Area yang Sering Berubah

Ini adalah senjata yang sering dilupakan tapi sangat efektif. RepaintBoundary memberitahu Flutter untuk membuat layer rendering terpisah untuk widget di dalamnya. Ketika konten di dalam boundary perlu direpaint, Flutter hanya mengulang bagian itu β€” tidak seluruh pohon widget.

RepaintBoundary(
child: CustomPaint(
painter: GrafikAnimasiPainter(progress: _animationValue),
),
)

Kapan menggunakan RepaintBoundary?

  • Widget yang sering diupdate (animasi, timer, data real-time)
  • CustomPainter yang kompleks tapi isolated β€” tidak perlu tau kondisi widget di sekitarnya
  • Scrollable content yang berisi grafis kustom

Tapi jangan berlebihan. Setiap RepaintBoundary membutuhkan memori tambahan untuk layer-nya. Kalau widget jarang berubah, RepaintBoundary justru menambah overhead tanpa manfaat.


Menggambar Path yang Kompleks: Precompute, Jangan Inline

Misalkan Anda ingin menggambar ornamen bintang 8 sudut. Cara naif:

@override
void paint(Canvas canvas, Size size) {
final path = Path();
// Hitung semua titik di sini, setiap frame
for (int i = 0; i < 16; i++) {
final angle = i * pi / 8;
final radius = i.isEven ? outerRadius : innerRadius;
final x = center.dx + radius * cos(angle);
final y = center.dy + radius * sin(angle);
if (i == 0) path.moveTo(x, y);
else path.lineTo(x, y);
}
path.close();
canvas.drawPath(path, _paint);
}

Ini menghitung semua titik setiap 16ms. Kalau ukuran dan posisi bintang tidak berubah, ini pemborosan murni.

Pendekatan yang lebih baik: precompute path saat inisialisasi, simpan sebagai field:

class BintangPainter extends CustomPainter {
final double outerRadius;
final double innerRadius;
final Offset center;
final Color warna;
late final Path _cachedPath;
late final Paint _paint;
BintangPainter({
required this.outerRadius,
required this.innerRadius,
required this.center,
required this.warna,
}) {
_paint = Paint()..color = warna;
_cachedPath = _buildPath();
}
Path _buildPath() {
final path = Path();
for (int i = 0; i < 16; i++) {
final angle = i * pi / 8 - pi / 2;
final r = i.isEven ? outerRadius : innerRadius;
final x = center.dx + r * cos(angle);
final y = center.dy + r * sin(angle);
if (i == 0) path.moveTo(x, y);
else path.lineTo(x, y);
}
return path..close();
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(_cachedPath, _paint);
}
@override
bool shouldRepaint(BintangPainter old) =>
old.outerRadius != outerRadius ||
old.innerRadius != innerRadius ||
old.center != center ||
old.warna != warna;
}

Path hanya dihitung sekali saat painter dibuat. Method paint() sendiri tinggal satu baris.


Animasi dengan CustomPainter: Gunakan AnimationController dengan Benar

Untuk painter yang perlu dianimasikan, polanya adalah menghubungkan AnimationController ke painter melalui Listenable:

class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: LoadingRingPainter(repaint: _controller),
size: const Size(100, 100),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

Parameter repaint pada CustomPaint menerima Listenable β€” ketika Listenable ini notify, painter akan direpaint. AnimationController adalah Listenable, jadi ini bekerja langsung.

Keuntungan pendekatan ini: hanya area CustomPaint yang direpaint ketika animasi bergerak, bukan seluruh widget tree di atasnya.


Menggunakan saveLayer dengan Sangat Hati-hati

canvas.saveLayer() memungkinkan efek seperti blur dan blending yang tidak bisa dilakukan dengan cara lain. Tapi ini adalah operasi yang sangat mahal β€” ia membuat intermediate framebuffer baru di GPU.

Rule of thumb saya: gunakan saveLayer() hanya untuk efek yang benar-benar tidak bisa dicapai tanpanya, dan pastikan area yang dibungkus sesempit mungkin.

Jika Anda perlu efek bayangan, pertimbangkan MaskFilter.blur pada object Paint alih-alih saveLayer() dengan BlendMode. Untuk efek transparansi sederhana, atur Color dengan alpha yang tepat daripada menggunakan layer terpisah.


Profiling: Cara Tahu Apakah Painter Anda Bermasalah

Flutter DevTools memiliki tab Performance yang sangat berguna untuk ini. Cara saya menggunakannya:

Jalankan aplikasi dalam profile mode (bukan debug, bukan release):

flutter run --profile

Buka DevTools, tab Performance, dan rekam beberapa detik interaksi yang terasa lambat. Cari frame yang melebihi 16ms (untuk 60fps) atau 8ms (untuk 120fps). Drill down ke flame chart untuk melihat method mana yang memakan waktu terbanyak.

Kalau Anda melihat banyak waktu dihabiskan di paint() untuk painter yang seharusnya statis, itu sinyal jelas bahwa shouldRepaint() terlalu sering mengembalikan true.


Penutup

CustomPainter adalah salah satu hal yang membuat Flutter berbeda dari framework mobile lain β€” kemampuan menggambar grafis kustom dengan performa mendekati native, menggunakan bahasa yang sama dengan kode UI lainnya.

Tapi seperti semua alat yang powerful, ia membutuhkan pemahaman yang proporsional. Pahami kapan Flutter memutuskan untuk repaint. Minimalkan kerja yang dilakukan di dalam paint(). Gunakan RepaintBoundary untuk mengisolasi area yang sering berubah.

Setelah prinsip-prinsip itu tertanam, CustomPainter menjadi alat yang menyenangkan untuk digunakan β€” bukan sumber frustrasi.


Komentar

Belum ada komentar. Jadilah yang pertama menulis.

Tulis Komentar

↑