A production story from real apps. For a long time, I thought process death was an edge case. Something you read about in docs. Something that might happen, but not in real life. After all, my apps worked fine. QA was happy. Users were using them.
Then production taught me otherwise.
It Started With Autonemo
While working on Autonemo, a vehicle tracking app, everything looked solid:
- Live vehicle tracking
- Trip status
- Ongoing routes
- Driver activity
You open the app → tracking works. You background it → come back → still works.
Until users started reporting something strange:
"Sometimes when I come back to the app, tracking is gone." "The trip looks like it never started."
No crashes. No ANRs. No error logs.
Just… wrong state.
The Bug I Couldn't Reproduce
On my phone:
- High RAM
- Latest Android
- No aggressive background killing
I could switch apps, come back, and everything was fine.
But real users?
- Budget devices
- Old OS versions
- Heavy multitasking
That's when I realized:
The OS was killing the app in the background.
Not pausing it. Not stopping it nicely. Killing the process.
The Mistake (Flutter)
At that time, critical tracking state lived only in memory:
class TrackingState {
bool isTripActive = false;
String? vehicleId;
}As long as the app was alive:
- UI was correct
- Tracking looked accurate
But once the OS killed the process:
- Memory was wiped
- State reset
- App restarted as if nothing was happening
No crash = no alert. Just broken logic.
"Okay, Fixed" — Then VivaUtilities Happened
Later, while working on VivaUtilities, a Flutter app for meal and leave management, I saw the same issue — but this time in a different form.
Users would:
- Start applying for leave
- Fill half the form
- Switch apps (email, calendar, messages)
- Come back…
And the form was empty.
Again:
- No crash
- No error
- Just lost progress
And again, QA couldn't reproduce it consistently.
The Common Pattern I Missed
I finally stopped looking at features and started looking at lifecycle.
Both apps had the same assumption:
"If the app goes to background, my state is still there."
That assumption is wrong.
On Flutter:
- Widgets rebuild
- Controllers recreate
- Providers restart
If the process is dead, everything in memory is gone.
The Flutter Fix: Persist What Matters
I changed how I decided what to persist.
Not everything. Only what would hurt to lose.
await prefs.setBool('is_trip_active', true);
await prefs.setString('vehicle_id', vehicleId);Then restored it on app start, not later.
For UI-related state, I also started using state restoration (which I had ignored before):
class LeaveFormScreen extends StatefulWidget {
const LeaveFormScreen({super.key});
@override
State<LeaveFormScreen> createState() => _LeaveFormScreenState();
}
class _LeaveFormScreenState extends State<LeaveFormScreen>
with RestorationMixin {
final RestorableString reason = RestorableString('');
@override
String get restorationId => 'leave_form';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(reason, 'leave_reason');
}
}This alone prevented a whole class of "random reset" complaints.
Then Native Android Taught Me the Same Lesson (VivaCash)
I thought this was a Flutter-specific problem.
Then VivaCash proved me wrong.
VivaCash is a native Android app written in Java, handling sensitive MFS flows:
- Amount input
- Transaction steps
- OTP flows
- App switching (SMS, banking apps)
Users reported:
"I went to check OTP and came back — transaction restarted."
Again:
- No crash
- No exception
- Just lost state
The Java Trap: Trusting Memory Too Much
The code looked innocent:
public class PaymentViewModel extends ViewModel {
public double amount = 0.0;
}I assumed:
"ViewModel survives rotation, so I'm safe."
But ViewModel does not survive process death.
When the OS killed the app:
- ViewModel was recreated
- Amount reset
- Flow broken
The Real Fix on Android: SavedStateHandle
Once we moved critical state into SavedStateHandle, the issue disappeared:
public class PaymentViewModel extends ViewModel {
private SavedStateHandle handle;
public PaymentViewModel(SavedStateHandle handle) {
this.handle = handle;
}
public void setAmount(double amount) {
handle.set("amount", amount);
}
public double getAmount() {
Double value = handle.get("amount");
return value != null ? value : 0.0;
}
}Now:
- Rotation → safe
- Background kill → safe
- Process recreation → safe
The Rule I Ship Apps With Now
After Autonemo. After VivaUtilities. After VivaCash.
This is the rule I never break:
If losing it would confuse or frustrate the user, it does not belong only in memory.
Memory is temporary. The OS is ruthless. Users don't care why it happened.
Why This Bug Is So Dangerous
- It passes QA
- It doesn't crash
- It only happens on real devices
- It slowly destroys trust
And users don't complain loudly. They just stop using your app.
Final Thought
Your app doesn't fail when it crashes.
It fails when the OS kills it — and your app pretends nothing important was happening.
That's a silent failure. And those are the worst ones.