<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: cabraule ketchanga</title>
    <description>The latest articles on DEV Community by cabraule ketchanga (@cabraule_ketchanga_80692b).</description>
    <link>https://dev.to/cabraule_ketchanga_80692b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947901%2F670d1dd5-39a8-44aa-9249-6ee688f8f91f.jpeg</url>
      <title>DEV Community: cabraule ketchanga</title>
      <link>https://dev.to/cabraule_ketchanga_80692b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cabraule_ketchanga_80692b"/>
    <language>en</language>
    <item>
      <title>Interruption recovery in Flutter: how I handle lost state after a phone call</title>
      <dc:creator>cabraule ketchanga</dc:creator>
      <pubDate>Mon, 08 Jun 2026 14:58:10 +0000</pubDate>
      <link>https://dev.to/cabraule_ketchanga_80692b/interruption-recovery-in-flutter-how-i-handle-lost-state-after-a-phone-call-9ko</link>
      <guid>https://dev.to/cabraule_ketchanga_80692b/interruption-recovery-in-flutter-how-i-handle-lost-state-after-a-phone-call-9ko</guid>
      <description>&lt;p&gt;&lt;strong&gt;The invisible problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've seen this happen dozens of times in my analytics. A user opens the app, starts a checkout, fills in their name, their address, picks a delivery method. They're on step 3 of 4. And then their phone rings. They pick up. A 5 minute call with their mom, a colleague, whatever. They hang up, go back to the app.&lt;/p&gt;

&lt;p&gt;And there it is, the home screen, state completely gone. Flutter did its job, it freed the memory when the process stayed backgrounded too long. Totally normal. Totally invisible to the user.&lt;/p&gt;

&lt;p&gt;What the user sees is that they have to start over. Name, address, delivery method. Three minutes of typing gone. No error message, no explanation, just nothing.&lt;/p&gt;

&lt;p&gt;The business cost is direct: checkout abandonment. Not a bug. Not a crash. Just the absence of memory between two states. And when you look at your data closely, you realize that window, the background then the return, is a black hole nobody really watches.&lt;/p&gt;

&lt;p&gt;The question I asked myself: is this really unavoidable, or did we just get used to doing nothing about it?&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why Flutter doesn't handle this&lt;/strong&gt;&lt;br&gt;
Flutter gives you the tools to observe the lifecycle, not to manage it for you. &lt;code&gt;WidgetsBindingObserver&lt;/code&gt; throws callbacks at you: paused, inactive, resumed, detached. You know when the app goes to background. You know when it comes back. What Flutter doesn't do is decide what to save, or how. That's intentional. The framework's philosophy is to give you full control over state. Good philosophy for a general purpose framework. Bad news when you have a 4 step checkout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_MyAppState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;WidgetsBindingObserver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;didChangeAppLifecycleState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppLifecycleState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AppLifecycleState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// OK but... where do we save what exactly?&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you go looking. &lt;code&gt;shared_preferences&lt;/code&gt;, too limited for complex state. Provider, Riverpod, Bloc, great for orchestrating state in memory, but they die with the process. Hive works, but you write the serialization by hand for every screen. The Flutter Restoration API looks promising on paper, fragile in practice across versions. I tried all these approaches. And every time I hit the same wall: saving state is the easy part. The hard part is deciding when to restore it, how to offer it to the user, and how long you keep that data. A silent restore and the user lands on a screen without understanding why. A badly worded confirmation and they tap "No" out of reflex. And if they come back two days later, do you rehydrate anyway? The real problem isn't technical. It's semantic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I tried (and what didn't work)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first implementation was clean on paper. I saved state to Hive on every input change, and on resume I reloaded it silently. No modal, no confirmation, just a transparent restore. I told myself: minimal UX, zero friction.&lt;/p&gt;

&lt;p&gt;A beta tester wrote to me three days later: "I thought your app had a bug, I was on a step I didn't remember reaching." That's the problem with silent restoration, it's worse than no restoration at all. The user is disoriented, they don't understand why they're there, and they associate it with the app behaving weird.&lt;/p&gt;

&lt;p&gt;Attempt 2. I add a modal on resume: "Want to resume your session?" Yes button, No button. Most users tapped No. Not because they didn't want to resume, but because the modal showed up on an empty screen, no context, no way to see what there was to resume. A user who doesn't see their cart, their total, the exact step they were on, recognizes nothing. They say No out of reflex.&lt;/p&gt;

&lt;p&gt;Attempt 3. I tell myself: OK, I'll keep the state for 7 days, that way even users who come back late can resume. Except one user came back 4 days later with a shipping address that had changed and an item out of stock. I restored stale data into a live checkout. Dangerous.&lt;/p&gt;

&lt;p&gt;Three tries, three fails. The lesson: saving is easy. Restoring is where it gets hard.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The approach that works: checkpoints + TTL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After those three failures, I changed the question. I stopped asking "how do I save the state" and started asking "what are the moments of coherence in this flow?" A 3 step checkout has exactly 3 moments where the state is clean, complete, valid. That's where you checkpoint. Not on every keystroke.&lt;/p&gt;

&lt;p&gt;That gave me three concrete rules.&lt;/p&gt;

&lt;p&gt;Snapshot per completed step, not per change. When the user validates step 1 (address), we persist. When they validate step 2 (delivery), we persist. Never in between. The saved state is always coherent because we capture it at a moment when the user themselves said "this is good."&lt;/p&gt;

&lt;p&gt;Short and contextual TTL. 15 minutes for a checkout, not 7 days. After 15 minutes an e-commerce cart can have changed (stock, price, expired promo, etc.). We refuse to restore potentially stale data. The TTL isn't a technical limit, it's a business decision.&lt;/p&gt;

&lt;p&gt;Confirmation with visible context. Never a blind modal. If we offer to "resume," we show the cart, the total, the exact step. The user recognizes what they were doing, they decide with full knowledge.&lt;/p&gt;

&lt;p&gt;The concrete pattern I use, the one I ended up wrapping into Morph, the SDK I'm building, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On every completed checkout step&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;morphSetCheckoutMultiStepContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;workflowId:&lt;/span&gt; &lt;span class="n"&gt;_workflowId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;step:&lt;/span&gt; &lt;span class="n"&gt;_currentStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;totalSteps:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;cartData:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'total'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'itemCount'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;savedData:&lt;/span&gt; &lt;span class="n"&gt;_savedStepData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;strategy:&lt;/span&gt; &lt;span class="n"&gt;RecoveryStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;ttl:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;minutes:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;workflowId&lt;/code&gt; identifies the checkout session. &lt;code&gt;step&lt;/code&gt; + &lt;code&gt;totalSteps&lt;/code&gt; automatically generate the message "You were on step 2 of 3." &lt;code&gt;cartData&lt;/code&gt; is what we show in the confirmation card.&lt;code&gt;strategy: confirm&lt;/code&gt; makes sure we never impose anything silently. And the 15 minute ttl kills the snapshot before it becomes dangerous. Why does this work where the other approaches failed? Because the user lands on a card that looks like theirs, their cart, their step, their context. They don't go through a mysterious restore. They recognize, they decide. And if they say no, that's their choice, the SDK learns from that too, and stops offering recovery if they refuse systematically. Now let's see what this looks like in practice on a real 3 step checkout.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Real case: a 3 step checkout&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's take Alice. She opens the app, starts her checkout.&lt;/p&gt;

&lt;p&gt;Step 1: she enters her address, validates. The snapshot is taken silently.&lt;/p&gt;

&lt;p&gt;Step 2: she enters her card number, halfway through typing. Her phone rings. A 5 minute call. She hangs up, goes back to the app. The OS killed the process. What happens on the return is exactly what we designed. The app detects that a snapshot exists, that it's less than 15 minutes old, and that it belongs to a multi step workflow. A card slides up from the bottom of the screen, not a blocking modal, not an aggressive popup. Just a discreet surface with what's needed: "Resume your order?" 2 items, $47.50, step 2/3 (Payment), [Continue] or [Start over]. Alice immediately sees what she was doing. She recognizes her cart, her step, her total. She's not lost. She doesn't wonder why she's there. She taps "Continue." The SDK loads the step 1 snapshot from the chain, the address is already there. The Payment screen opens. She picks up her typing right where she left off. 30 seconds later, her order is placed. Without Morph, she would have started from scratch, or she would have abandoned. That's the delta that matters. Not the technical feat, the completed checkout instead of the abandoned one.&lt;/p&gt;

&lt;p&gt;I rebuilt this whole flow in an open source Flutter e-commerce demo (MIT) that you can clone and modify freely: &lt;a href="https://github.com/polarismorph-code/morphui-flutter-ecommerce" rel="noopener noreferrer"&gt;https://github.com/polarismorph-code/morphui-flutter-ecommerce&lt;/a&gt;. The interruption recovery code is in &lt;code&gt;lib/features/checkout/checkout_screen.dart&lt;/code&gt;. If you want to dig in, open an issue, or adapt the pattern to your own flow, that's what it's there for.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
What I learned in 8 months on this topic: the best UX optimizations are invisible. The user doesn't know they were about to abandon their checkout. They finish it, that's all. There's no success notification, no "wow" moment. Just an order completed where there would have been a silent abandonment.&lt;/p&gt;

&lt;p&gt;Interruption recovery isn't a detail you add after v1. It's an architecture decision to make early, because retrofitting it onto an existing flow is painful.&lt;/p&gt;

&lt;p&gt;If you work on multi step flows in Flutter, checkout, KYC, onboarding, I'm curious how you handle this on your side. Same pattern? Different approach? Comments are open. And for the curious: Morph also covers React. Everything's on morphui.dev.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>webdev</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
