Skip to content

Navigator Basics

In React you call useNavigate() to get a navigate function, then call navigate('/detail') to change the current route. Flutter’s equivalent is Navigator.push(context, route) — but instead of a URL string, you pass a Route object that knows how to build the next screen. The key difference is that Flutter maintains a real widget stack: the previous screen stays alive in memory below the new one, just as browser history keeps previous pages alive in the tab’s session.

The Android back button and iOS swipe-back gesture are handled automatically by the Navigator — you do not need to wire up a back handler unless you want to intercept it.

React
// React — navigate programmatically
import { useNavigate } from 'react-router-dom';
function HomeScreen() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/detail')}>
Go to Detail
</button>
);
}
// Go back
function DetailScreen() {
const navigate = useNavigate();
return (
<button onClick={() => navigate(-1)}>
Back
</button>
);
}
Flutter
// Flutter — push / pop
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
),
);
},
child: const Text('Go to Detail'),
);
}
}
// Go back
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Back'),
);
}
}

Since Flutter 3.38 the default Android page transition is PredictiveBackPageTransitionsBuilder. As the user drags from the screen edge, Android peels the current route away and shows a live preview of the screen underneath — so they can see where “back” leads before committing to it. On devices without predictive back support it falls back to FadeForwardsPageTransitionsBuilder.

The closest thing you know is the iOS Safari edge-swipe preview, or a React Native screen transition wired to a gesture handler. In the browser, history.back() gives you no preview at all: the page is simply replaced.

You get this for free with MaterialPageRoute — there is nothing to opt into. To go back to the pre-3.38 animation, set it explicitly in your theme:

MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
},
),
),
home: const MyFirstScreen(),
);

One side effect worth knowing when you write widget tests: the transition is now 450ms rather than 300ms. Pumping a hard-coded duration will break. Use TransitionDurationObserver and observer.pumpPastTransition(tester) so the test stays independent of the animation length.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'You are on the Home screen.',
              style: TextStyle(fontSize: 16),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const DetailScreen(),
                  ),
                );
              },
              child: const Text('Go to Detail'),
            ),
          ],
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Detail')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'You are on the Detail screen.',
              style: TextStyle(fontSize: 16),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Go Back'),
            ),
          ],
        ),
      ),
    );
  }
}
What does Navigator.push add to the Flutter navigation stack?
Why does Navigator.push require a BuildContext?
What is MaterialPageRoute responsible for?
How does Flutter handle the Android back button without any extra code?