DEV Community

Cover image for Scaling Shredzilla: Slaying Memory Leaks & Mastering State in Jetpack Compose
freerave
freerave

Posted on

Scaling Shredzilla: Slaying Memory Leaks & Mastering State in Jetpack Compose

Building a fitness app is easy. Building a production-ready, offline-capable, memory-efficient fitness app that survives Android's brutal lifecycle changes? That's a different beast entirely.

Over the past few weeks, I’ve been heavily refactoring my open-source workout tracker, Shredzilla, moving it from a "working prototype" to a rock-solid, production-ready architecture.

In this article, I want to share the critical architectural lessons learned during the v1.1.0 Refactoring Sprint, how we solved some nasty memory leaks, and what’s coming next in the v1.2.0 Performance Update.


What We Achieved in v1.1.0: The Great Refactoring

Version 1.1.0 was all about Stability, Security, and DRY Principles. Here are the biggest wins:

1. Slaying the Eternal Firestore Memory Leak πŸ§›β€β™‚οΈ

If you use Firebase Firestore's addSnapshotListener, you might be leaking memory without realizing it. Previously, Shredzilla's listeners stayed alive in the background even after a user logged out, consuming memory and unnecessary quota.

The Fix: We implemented a strict caching system for ListenerRegistration inside our UserDataManager.

// Inside UserDataManager.kt
private val listeners = mutableListOf<ListenerRegistration>()

fun setupFirestoreListeners(currentUser: FirebaseUser?) {
    val userDocRegistration = db.collection("users").document(userId)
        .addSnapshotListener { snapshot, e -> /* Update UI State */ }

    listeners.add(userDocRegistration) // Cache the listener
}

// Called upon Sign Out or Account Deletion
fun clearAllListeners() {
    listeners.forEach { it.remove() }
    listeners.clear()
}
Enter fullscreen mode Exit fullscreen mode

Takeaway: Never leave a listener hanging. Always cache and kill them when the user session ends.

2. State Hoisting & Surviving Process Death πŸ§Ÿβ€β™‚οΈ

Have you ever tried to update a username, rotated your phone while the loading spinner was spinning, and suddenly the app crashed or the state vanished? We fixed that.

We moved heavy, non-idempotent operations (like Account Deletion and Profile Updates) entirely into the MainViewModel's viewModelScope.

// ❌ BAD: UI handles the coroutine. Dies on rotation.
Button(onClick = { 
    rememberCoroutineScope().launch { deleteAccount() } 
})

// βœ… GOOD: ViewModel handles the launch. Survives UI recreation.
fun deleteUserAccount(onSuccess: () -> Unit, onError: (Exception) -> Unit) {
    viewModelScope.launch {
        isDeletingAccount = true
        try {
            // Delete Firestore Data -> Delete Auth User -> Clean Local Files
            // ...
            onSuccess()
        } finally { isDeletingAccount = false }
    }
}
Enter fullscreen mode Exit fullscreen mode

We also swapped standard remember for rememberSaveable in our text fields so users don't lose their typed input during configuration changes!

3. Bulletproof Navigation & Back-Stack Management

Onboarding flows can be tricky. You don't want a user who just finished logging in to press the "Back" button and end up back on the login screen.

In Shredzilla, we utilized Compose Navigation's popUpTo forcefully:

mainNavController.navigate(AppRoutes.FITNESS_MAIN) { 
    // Obliterate the Auth graph from the backstack!
    popUpTo(AppRoutes.AUTH) { inclusive = true } 
    launchSingleTop = true
}
Enter fullscreen mode Exit fullscreen mode

4. Data Integrity: The DRY Settings Helper

Instead of repeating SetOptions.merge() everywhere in the UI when a user changes their Unit System (Kg/Lbs) or Theme, we abstracted it into a single, type-safe generic function in the data layer:

fun updateUserSetting(userId: String?, key: String, value: Any?) {
    if (userId.isNullOrEmpty()) return
    db.collection("users").document(userId)
      .set(hashMapOf(key to value), SetOptions.merge())
}
Enter fullscreen mode Exit fullscreen mode

What's Next: The v1.2.0 Roadmap

With the foundation secured, the upcoming v1.2.0 Sprint is focused purely on Speed, Analytics, and Gym-floor UX.

Here is what we are building next:

1. The Active Workflow Engine (Today Screen)

  • Offline-First Sync: Gyms have notoriously bad Wi-Fi. We are building sophisticated offline caching so weightlifters can log intense sets seamlessly in dead-zones, with implicit background syncing upon reconnection.
  • Zero-Latency Timers: Decoupling the rest timer mechanics from Compose UI recomposition cycles to guarantee frame-perfect countdowns even when the app is minimized.

2. Exercise Database & 60-FPS Filtering

  • Debounced Search Flows: Users shouldn't experience UI lag while typing "Bench Press". We are migrating the search logic to a debounced Kotlin StateFlow emitted directly from the ViewModel to prevent ANRs.
  • LazyColumn Optimization: Applying standard pagination blocks and keying for buttery smooth 60-FPS scrolling through massive exercise arrays.
  • Shared Element Transitions: Building fluid, polished animations that morph from a standard list item directly into the ExerciseDetailScreen.

3. Analytics Processing & Smart Charting

  • Thread Shifting: Heavyweight calculations (like total volume summarization over months of temporal data) will be firmly shifted off the Main Thread and onto Dispatchers.Default.
  • Dynamic Y-Axis Clamping: Designing an algorithmic bounds calculator for the SetGraphScreen to intelligently clamp ranges. This will prevent extreme one-off graphical spikes from masking micro-progressions in strength.
  • SavedStateHandle for Analytics: Caching mathematical aggregations so that complex graphs render instantly during spontaneous orientation pivots (Portrait -> Landscape).

Let's Build Together!

Shredzilla is an open-source project built with ❀️ for the Android community. If you are learning Jetpack Compose, Firebase, or Clean Architecture, diving into the codebase is a great way to see these concepts in action.

Check out the repository here: github.com/kareem2099/Shredzilla

  • Drop a star if you find the architecture helpful!
  • Found a bug or have an idea? Open an issue!
  • Want to tackle one of the v1.2.0 roadmap items? PRs are always welcome.

*What is your favorite way to handle State Hoisting in Jetpack Compose? Let's discuss in the comments below! *

Top comments (0)