Skip to main content
Adzbyte
Mobile DevelopmentTesting

Push Notifications Only Became Real After I Tested Three App States

Adrian Saycon
Adrian Saycon
October 13, 20264 min read
Push Notifications Only Became Real After I Tested Three App States

When I added push notifications to a Flutter app, the implementation passed unit tests long before I trusted it. A notification is not one behavior. It can arrive while the app is open, while it is backgrounded, or after the process has been terminated. The operating system presents each state differently, and tapping the same payload may enter the router through a different path. I only considered the feature real after walking those states on an emulator, capturing evidence, forcing duplicates, and checking that a notification opened the intended screen without replaying an in-app event.

Foreground delivery is an application decision

When the app is visible, the system may not display a normal tray notification. The application must decide whether to show an in-app banner, update state silently, or defer the message. That choice needs to respect whatever modal, celebration, or navigation is already active.

I tested arrival during ordinary home state and during an overlay. A notification that technically arrives but obscures a more important flow is not correct.

Background taps test routing ownership

From the background, the operating system owns presentation and returns control after the user taps. The app must parse a bounded route, restore session state, and navigate only after the router is ready.

I avoided treating a server-provided string as an arbitrary navigation command. The payload maps to known destinations and validates the identifiers required by those screens.

Terminated launch is a cold-start problem

When the process is gone, notification intent competes with authentication restoration, local storage initialization, and the initial route. If navigation happens too early, the app can lose the target or briefly show the wrong screen.

My emulator walk included swiping the app away, sending the notification, tapping from the tray, and waiting through a clean launch. Screenshots and logs made timing bugs visible in a way a mocked callback could not.

Deduplication belongs at the dispatch boundary

Retries are normal in notification systems. I added a durable dispatch record keyed by user, notification kind, and date so a repeated job would not send the same reminder twice. Then I deliberately invoked the dispatch again and confirmed only one tray item arrived.

Client-side suppression can improve presentation, but it should not be the only defense. The service that decides to send needs an idempotent record of that decision.

Time zones changed the product rule. Streak and comeback notifications depend on calendar days, not simply elapsed hours. I found that eligibility based on local time could behave differently across devices and server jobs. The durable rule moved to a documented UTC calendar boundary, while user-facing copy remained understandable.

This is a product decision disguised as date arithmetic. Teams should define whose day matters before writing a cron expression.

The state matrix I now require

  • Foreground arrival with and without another overlay.
  • Background tray display and tap routing.
  • Terminated tray display and cold-start routing.
  • Logged-out or deleted-account behavior.
  • Invalid and stale destination identifiers.
  • Repeated server dispatch and repeated client delivery.
  • Permission accepted, declined, and later changed in settings.

Each case needs an expected visible result and an expected analytics or log trail.

I tested failure as a user experience. Valid delivery is only half the feature. I checked what happens when the destination record no longer exists, the user is signed out, permission is denied, or the notification arrives after the underlying state changed. The app should fall back to a safe screen and avoid exposing a raw exception or looping through login.

I also verified that deleting an account prevents later dispatch and that stale tokens can be retired without blocking other users. Server logs use privacy-safe identifiers and structured event names rather than notification bodies. These cases matter because reminders often arrive hours after the state that created them. By then, a goal may be abandoned, a quest completed elsewhere, or an account removed. Notifications should be treated as hints to re-fetch authoritative state, not as authoritative state themselves.

Analytics events were designed around decisions rather than message content: dispatch attempted, provider accepted, foreground received, notification opened, route resolved, route rejected, and preference changed. That makes delivery gaps diagnosable without copying private notification text into the analytics system. Server acceptance is not the same as device receipt, and receipt is not the same as a useful open.

I repeated the highest-risk walks after a clean app restart and after rebuilding the debug package. That guarded against success caused by leftover local state or a hot-reload session. Reproducible setup is part of mobile evidence.

The emulator walk is part of the feature

For the next notification feature, I would write the state matrix and route allowlist before wiring the provider SDK. Then I would reserve time for a real emulator walk with captured tray, tap, and cold-start evidence. Push is a distributed user journey; a green callback test proves only one small part of it.

Photo by Szabó Viktor on Pexels.

Adrian Saycon

Written by

Adrian Saycon

A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.

Discussion (0)

Your email is used only for comment moderation and is never published.

No comments yet. Be the first to share your thoughts.