Memory Management di Flutter? Ini yang Saya Lakukan
Kamu udah nulis Flutter app cukup lama. Sering pakai WidgetsBindingObserver. Mungkin untuk didChangeAppLifecycleState — biar video berhenti waktu app masuk background, atau matiin timer, atau disconnect WebSocket.
Ituu, mostly dev flutter pernah pakai.
Tapi kalau saya tanya: “Apa itu *didHaveMemoryPressure*, dan kapan terakhir kali kamu pakainya?" — saya yakin jawabannya entah diam, atau buka docs di tab baru sambil pura-pura udah tahu.
That’s okay. Saya juga pernah ada di posisi itu. Now it’s time to cut that stupid shit, kalo kata Pandji.
Dulu saya kira itu urusan OS, bukan urusan saya
Logikanya waktu itu: “Kalau memory habis, ya OS yang urus. Bukan tugas saya. Ya ndak tahuu, tanya kok tanya sayaaa….”
Ternyataaa, itu salah besar.
Iyaa sih, OS memang yang urus — tapi caranya dengan membunuh app secara paksa. Paksa close. Tanpa permisi. User lagi di tengah-tengah isi form, atau lagi streaming video, atau lagi checkout. Tiba-tiba app force-closed.
Dan kamu tidak akan tahu kenapa, karena tidak ada crash report yang jelas. Hanya SIGKILL yang datang diam-diam.
didHaveMemoryPressure adalah OS yang mengetuk pintu terlebih dahulu sebelum memaksa masuk. Dia bilang: "Assalamulaikum, saya mulai kehabisan RAM. Tolong beresin dongg, bro!."
Kalau kamu tidak buka pintunya — tidak override callback-nya — OS anggap kamu tidak kooperatif. Dan dia akan bertindak sendiri dengan menghilangkan aktivis secara paksa, eh dengan mematikan aplikasi mu secara paksa.
WidgetsBindingObserver — lebih dari sekadar lifecycle
Ini mixin yang bikin class kamu bisa dengerin event dari sistem. Register sekali, dapat notifikasi untuk banyak hal:
class MyController extends ChangeNotifier with WidgetsBindingObserver {
MyController() {
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Sebagian besar Flutter developer hanya pakai dua atau tiga callback-nya. Padahal ada dua belas. Yang sering dilupakan — dan justru paling berguna:
https://gist.github.com/AkhmadRamadani/06c84f002e6bf3dd8a02d890020f1181
Hari ini kita bahas yang pertama. Karena itu yang paling sering diabaikan — dan paling sering jadi penyebab app mati mendadak tanpa sebab yang jelas.
Pendekatan naif dan kenapa itu backfire
Instinct pertama yang paling umum waktu akhirnya ingat untuk override callback ini:
@override
void didHaveMemoryPressure() {
imageCache.clear();
imageCache.clearLiveImages();
}
Oke, lumayan. Tapi sekarang tim product kamu lapor: “Foto profil user sering hilang tiba-tiba.” Analytics service-mu kehilangan batch event yang belum sempat dikirim. WebSocket-mu reconnect tanpa alasan yang jelas.
Kamu bersihkan semuanya — termasuk yang lagi aktif dipakai.
Pemodelan yang menurutku lebih tepat: tidak semua service itu setara. Ada yang tidak boleh mati dalam kondisi apapun (auth state, core network, navigation). Ada yang boleh mati hanya kalau benar-benar darurat (controller yang lagi aktif ditampilkan). Ada yang harusnya mati dengan senang hati begitu memory mulai ketat, yang ga penting-penting amat (prefetch cache, analytics batcher, background sync).
Kamu perlu cara untuk menyatakan perbedaan itu. Dan bertindak berdasarkan itu secara otomatis.
Membangun sistem manajemen memory yang proper
Ada empat bagian. Mari kita bahas satu per satu.
Bagian 1: Definisikan prioritas
enum ServicePriority {
/// Auto-kill saat idle. Yang pertama dikorbankan.
/// Untuk: prefetch cache, analytics batcher, background sync.
low(0),
/// Mati di bawah tekanan memory moderat.
/// Untuk: controller fitur yang tidak sedang ditampilkan, image cache.
normal(1),
/// Hanya mati kalau device benar-benar kehabisan memory.
/// Untuk: controller yang aktif ditampilkan, koneksi real-time.
high(2),
/// Tidak pernah dibunuh oleh memory manager.
/// Untuk: auth state, navigasi, core networking, storage.
critical(3);
final int weight;
const ServicePriority(this.weight);
bool canKillAt(MemoryPressure pressure) {
switch (pressure) {
case MemoryPressure.low: return this == ServicePriority.low;
case MemoryPressure.moderate: return weight <= ServicePriority.normal.weight;
case MemoryPressure.high: return weight <= ServicePriority.high.weight;
case MemoryPressure.critical: return this != ServicePriority.critical;
}
}
}
Empat level. Masing-masing tahu apakah dia boleh dikill pada tekanan tertentu. critical satu-satunya yang tidak bisa disentuh — seberapapun parahnya kondisinya.
Bagian 2: Definisikan kontrak untuk tiap service
abstract class ManagedService {
String get serviceId;
ServicePriority get priority;
bool get isActive; // Apakah service ini sedang dipakai?
// Kurangi penggunaan memory - tapi jangan mati dulu
Future<void> onMemoryWarning();
// Lepaskan semua resource - siap-siap untuk di-revive nanti
Future<void> onKill();
// Tekanan memory turun - restart resourcemu
Future<void> onRevive();
}
Tiga lifecycle hook. onMemoryWarning adalah sinyal "tolong kurangi dulu". onKill adalah "kita butuh servismu sekarang". onRevive adalah "boleh balik lagi, situasi sudah aman".
Image cache service implementasi ketiganya. Begitu juga analytics batcher-mu. Begitu juga prefetch service-mu. Masing-masing tahu cara bersihin diri sendiri dan cara restart.
Bagian 3: Lacak level tekanan
enum MemoryPressure { low, moderate, high, critical }
Satu system callback bisa memicu beberapa level tekanan seiring waktu. Kalau sistem memanggil didHaveMemoryPressure() dan kamu sudah di level moderate, eskalasi ke high. Setiap eskalasi membunuh lebih banyak service.
Waktu app kembali ke foreground dan tidak ada sinyal tekanan baru, turunkan level — dan hidupkan kembali service yang seharusnya tidak mati di level yang lebih rendah.
Bagian 4: MemoryManager — tempat semuanya nyambung
class MemoryManager with WidgetsBindingObserver {
final EventBus _eventBus;
final Map<String, ServiceState> _services = {};
MemoryPressure _currentPressure = MemoryPressure.low;
MemoryManager({required EventBus eventBus, ...}) {
WidgetsBinding.instance.addObserver(this);
_startIdleCheck(idleCheckInterval);
}
@override
void didHaveMemoryPressure() {
_escalatePressure(); // ← Ini intinya
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.detached:
_handleBackgrounded(); // Kill service low-priority yang idle sekarang
case AppLifecycleState.resumed:
_handleResumed(); // De-eskalasi, hidupkan kembali yang perlu
default:
break;
}
}
}
MemoryManager register dirinya sendiri sebagai WidgetsBindingObserver. Dia terima callback dari sistem, eskalasi level tekanan, dan trigger urutan pembunuhan secara otomatis.
Urutan kill itu penting. Waktu menerapkan tekanan, service diurutkan berdasarkan:
- Prioritas (terendah duluan — low mati sebelum normal)
- Status aktif (yang tidak aktif mati sebelum yang aktif)
- Durasi idle (yang paling lama idle, mati duluan)
final killable = aliveServices
.where((state) => state.service.priority.canKillAt(pressure))
.toList()
..sort((a, b) {
final p = a.service.priority.weight.compareTo(b.service.priority.weight);
if (p != 0) return p;
if (a.service.isActive && !b.service.isActive) return 1;
if (!a.service.isActive && b.service.isActive) return -1;
return b.idleDuration.compareTo(a.idleDuration);
});
Artinya: service normal -priority yang idle 20 menit mati jauh lebih dulu daripada service normal -priority yang lagi aktif ngerender ke user.
Bonus: Auto-kill service yang nganggur
Ada idle check yang jalan di timer terpisah, independen dari pressure callback:
Future<void> _checkIdleServices() async {
final idleServices = _services.values.where((state) {
return !state.isKilled &&
state.service.priority == ServicePriority.low &&
!state.service.isActive &&
state.idleDuration > _idleThreshold;
}).toList();
for (final state in idleServices) {
await _killService(state);
}
}
Kamu tidak perlu nunggu OS teriak. Service low-priority yang idle 5 menit — bisa configured — dibersihkan secara proaktif. Ini cara mencegah pressure warning terjadi dari awal — bukan bereaksi setelah sudah terlambat.
Cara pakainya di real case
// 1. Implementasikan ManagedService di service kamu
class ImageCacheService implements ManagedService {
@override
String get serviceId => 'image_cache';
@override
ServicePriority get priority => ServicePriority.normal;
@override
bool get isActive => _activeRequests > 0;
@override
Future<void> onMemoryWarning() async {
// Trim ke setengah kapasitas dulu
imageCache.maximumSize = imageCache.maximumSize ~/ 2;
imageCache.maximumSizeBytes = imageCache.maximumSizeBytes ~/ 2;
}
@override
Future<void> onKill() async {
imageCache.clear();
imageCache.clearLiveImages();
}
@override
Future<void> onRevive() async {
// Kembalikan limit default
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 100 << 20; // 100 MB
}
}
// 2. Register ke MemoryManager
final memoryManager = MemoryManager(eventBus: eventBus);
memoryManager.register(ImageCacheService());
memoryManager.register(AnalyticsBatcherService());
memoryManager.register(PrefetchService());
// 3. Mark active saat digunakan
memoryManager.markActive('image_cache'); // Dipanggil saat loading gambar
Untuk testing dan debugging, bisa set pressure secara manual:
// Simulasikan apa yang terjadi waktu OS memanggil didHaveMemoryPressure
await memoryManager.setPressure(MemoryPressure.high);
// Cek siapa yang masih hidup
final snapshot = memoryManager.getSnapshot();
for (final s in snapshot) {
print(s); // [KILLED] image_cache (normal) idle: 312s
}
Dan kamu dapat event di bus untuk semua yang perlu bereaksi:
eventBus.on<ServiceKilledEvent>().listen((event) {
// Log, update UI, atau apapun yang perlu dilakukan
});
eventBus.on<MemoryPressureChangedEvent>().listen((event) {
print('Tekanan: ${event.previous.name} → ${event.current.name}');
});
Pelajaran yang bisa diambil
1. Override **didHaveMemoryPressure()**. Sekarang. Callback-nya ada. Dipanggil secara reliable. Tapi mayoritas app Flutter membiarkannya kosong — dan berakhir di-kill OS tanpa sempat berbenah.
2. Tidak semua service boleh mati dengan cara yang sama. Auth state dan navigasi itu nyawa app. Analytics batcher itu bukan. Ekspresikan perbedaan itu secara eksplisit di kode, bukan cuma di kepala.
3. Jangan nunggu OS teriak — bersihkan yang idle secara proaktif. Background sync yang tidak melakukan apa-apa dalam 10 menit tidak punya alasan untuk tetap makan memory. Bebaskan sebelum diminta.
didHaveMemoryPressure bukan API niche untuk kasus spesial. Ini OS yang meminta bantuan. Sebagian besar app tidak menjawab. Yang menjawab adalah yang lifecycle lebih lama, crash lebih jarang, dan terasa lebih cepat — terutama di perangkat kelas menengah ke bawah, tempat sebagian besar pengguna nyata kita berada.
Callback-nya satu baris. Desain di baliknya yang butuh pemikiran.
Artikel ini bagian dari seri tentang arsitektur Flutter. *MemoryManager* yang ditunjukkan di sini dibangun di atas Karna MVC — Flutter starter template dengan typed error handling, data strategy, dan event bus.
Oke, sekian — terima kasih sudah baca sampai sini! 🙏