# Designing Navigation Pages That Actually Survive Refresh

## Problem statement: it works… until someone refreshes

I’ve seen this pattern again and again, especially with newer Flutter developers.

You build navigation using `go_router`.  
You pass objects from one page to another.  
Everything works perfectly during normal app usage.

Then someone refreshes the page.

Or opens a link directly.

Or lands on a URL from Google.

And suddenly:

* the page crashes
    
* or shows a blank screen
    
* or awkwardly redirects back to a list
    

At this point, most people start adding workarounds:  
“if data is null, go back”  
“if object is missing, fetch again”  
“if state is gone, redirect home”

The router isn’t the problem here.  
The page design is.

---

## What’s actually going wrong

The root issue is **tight coupling between pages**.

A lot of pages are built with the assumption that:

> “This page will always be opened from its parent.”

That assumption is no longer safe.

With **Navigator 2.0**, deep linking is not optional anymore.  
On web, every page is already a deep link — whether you planned for it or not.

If a page depends on parent state to exist, that page is fragile by design.

---

## One simple question that changes everything

Whenever I add a new route, I ask myself one question:

> From a business point of view, should this page be able to exist on its own?

This question decides everything.

### Standalone pages

Examples:

* `/user/:id`
    
* `/order/:id`
    
* `/product/:id`
    

These pages:

* should open directly from a URL
    
* should survive refresh
    
* should not depend on parent state
    
* should be able to load themselves
    

### Flow-dependent pages

Examples:

* payment steps
    
* checkout confirmation
    
* OTP verification
    
* onboarding flows
    

These pages:

* only make sense inside a flow
    
* often should not be deep linked
    
* can depend on parent state
    
* usually redirect if opened directly
    

Not every page needs to be standalone.  
But the ones that should be, **must be designed that way from day one**.

---

## The common mistake: passing full objects through routes

Let’s look at a very common pattern.

### Fragile approach

```dart
// From user list page
context.go(
  '/user-details',
  extra: user,
);
```

```dart
// User details page
class UserDetailsPage extends StatelessWidget {
  final User user;

  const UserDetailsPage({required this.user});

  @override
  Widget build(BuildContext context) {
    return Text(user.name);
  }
}
```

This feels convenient.  
No extra API call.  
No loading state.

But this page has one big problem:

It **cannot exist on its own**.

If the user refreshes:

* `extra` is gone
    
* `user` is null
    
* now the page doesn’t know what to do
    

So people start patching:

* redirect back to list
    
* show error
    
* refetch with hacks
    

All of this complexity exists because the page depends on navigation state.

---

## A more resilient approach: routes as contracts

If a page is standalone, the route should describe **what the page needs**, not **what the parent happens to have**.

### Better route definition

```dart
GoRoute(
  path: '/user/:id',
  builder: (context, state) {
    final userId = state.pathParameters['id']!;
    return UserDetailsPage(userId: userId);
  },
);
```

Now the route itself answers one question clearly:

> “What does this page need to load?”

Answer: a `userId`.

---

## Move data responsibility into the page

The page should not care **where it came from**.  
It should only care about **what it needs**.

```dart
class UserDetailsPage extends StatelessWidget {
  final String userId;

  const UserDetailsPage({required this.userId});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => UserDetailsCubit(userId)..load(),
      child: const UserDetailsView(),
    );
  }
}
```

```dart
class UserDetailsCubit extends Cubit<UserDetailsState> {
  final String userId;

  UserDetailsCubit(this.userId) : super(UserDetailsLoading());

  Future<void> load() async {
    final user = await userRepository.getUser(userId);
    emit(UserDetailsLoaded(user));
  }
}
```

Now this page:

* works on refresh
    
* works with deep links
    
* works without parent context
    
* is easy to reason about
    

---

## “But why make another API call?” — the hybrid approach

This is a fair question.

If I already have the user object, why fetch it again?

You don’t have to.

### Hybrid page design

Let the page accept an optional object **without depending on it**.

```dart
class UserDetailsPage extends StatelessWidget {
  final String userId;
  final User? user;

  const UserDetailsPage({
    required this.userId,
    this.user,
  });

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => UserDetailsCubit(
        userId: userId,
        initialUser: user,
      )..load(),
      child: const UserDetailsView(),
    );
  }
}
```

```dart
class UserDetailsCubit extends Cubit<UserDetailsState> {
  final String userId;
  final User? initialUser;

  UserDetailsCubit({
    required this.userId,
    this.initialUser,
  }) : super(UserDetailsLoading());

  Future<void> load() async {
    if (initialUser != null) {
      emit(UserDetailsLoaded(initialUser!));
      return;
    }

    final user = await userRepository.getUser(userId);
    emit(UserDetailsLoaded(user));
  }
}
```

### What you gain

* No unnecessary API calls
    
* Refresh still works
    
* Deep links still work
    
* Page is not tied to navigation history
    

This pattern scales very well as the app grows.

---

## What about flow-based pages?

Not every page needs this treatment.

For example, a payment confirmation step:

* doesn’t make sense outside the flow
    
* probably should redirect if opened directly
    
* can depend on parent state safely
    

The key is **being intentional**.

Problems happen when we treat *every* page the same.

---

## Why this matters long-term

Pages that can load themselves:

* are easier to debug
    
* are easier to test
    
* are easier for new developers to understand
    
* break less often on web
    

Most “navigation bugs” are actually **page design bugs**.

---

## Final thoughts

Navigation is not just about moving between screens.  
It’s about defining boundaries and responsibilities.

Before adding a route, ask:

> Should this page survive refresh?

That one question will save you a lot of pain later.
