Ahmet Balaman LogoAhmet Balaman

Navigation and Data Transfer Between Pages in Flutter

personAhmet Balaman
calendar_today
FlutterNavigatorRoutingNavigationState Management

Page Navigation - Using Navigator

The first time I tried to navigate between pages, I was a bit confused. "The web has links, how does Flutter do it?" I wondered. Then I discovered Navigator and everything became clear.

In Flutter, the Navigator class is used for page navigation. It works like a stack data structure: You open a page, it's added to the top; you press back, the top page is removed.

Live Demo: Using Navigator

Try page navigation and data transfer interactively:

Basic Navigator Usage

The simplest way to navigate from one page to another:

// Home page
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(
        child: ElevatedButton(
          child: Text('Go to Detail Page'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => DetailPage()),
            );
          },
        ),
      ),
    );
  }
}

// Detail page
class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Detail Page')),
      body: Center(
        child: Text('This is the detail page'),
      ),
    );
  }
}

Navigator.push() opens a new page. MaterialPageRoute provides the page transition animation.

Going Back - pop()

There are two ways to go back:

1. Automatic Back Button

If you use AppBar, Flutter automatically adds a back button. The user clicks it to go back.

2. Programmatic Back Navigation

ElevatedButton(
  child: Text('Go Back'),
  onPressed: () {
    Navigator.pop(context);
  },
)

Navigator.pop() removes the top page from the stack.

Sending Data Between Pages

This is where it gets interesting. We use constructors to send data to pages.

Page Sending Data

class ProductListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Products')),
      body: ListView(
        children: [
          ListTile(
            title: Text('iPhone 15'),
            subtitle: Text('\$999'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ProductDetailPage(
                    productName: 'iPhone 15',
                    price: 999,
                    description: 'Next generation smartphone',
                  ),
                ),
              );
            },
          ),
          ListTile(
            title: Text('Samsung S24'),
            subtitle: Text('\$899'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ProductDetailPage(
                    productName: 'Samsung S24',
                    price: 899,
                    description: 'The best of Android',
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

Page Receiving Data

class ProductDetailPage extends StatelessWidget {
  final String productName;
  final int price;
  final String description;

  const ProductDetailPage({
    Key? key,
    required this.productName,
    required this.price,
    required this.description,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(productName)),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              productName,
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 8),
            Text(
              '\$${price.toString()}',
              style: TextStyle(fontSize: 20, color: Colors.green),
            ),
            SizedBox(height: 16),
            Text(
              description,
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

We use required in the constructor to make parameters mandatory. This way, when opening the page, this information must be provided.

Returning Data When Going Back

Sometimes you want to receive data from the page you opened. For example, the user selected something and you want to send this selection to the previous page.

Page Requesting Data

class SelectCityPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Select City')),
      body: Center(
        child: ElevatedButton(
          child: Text('Choose City'),
          onPressed: () async {
            // Open page and wait for result
            final selectedCity = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => CityListPage()),
            );
            
            // Result received
            if (selectedCity != null) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Selected: $selectedCity')),
              );
            }
          },
        ),
      ),
    );
  }
}

Page Returning Data

class CityListPage extends StatelessWidget {
  final List<String> cities = [
    'New York',
    'Los Angeles',
    'Chicago',
    'Houston',
    'Phoenix',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('City List')),
      body: ListView.builder(
        itemCount: cities.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(cities[index]),
            onTap: () {
              // Send selected city back
              Navigator.pop(context, cities[index]);
            },
          );
        },
      ),
    );
  }
}

With Navigator.pop(context, value), we send data as the second parameter.

pushReplacement - Preventing Back Navigation

Sometimes you don't want the user to go back to the previous page using the back button. A typical example: Don't return to the login page after logging in.

Normal Navigation with push()

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
);
// User can press back to return to login page

Navigation with pushReplacement()

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
);
// Back button goes to the page before login, skipping login

Practical Example: After Login

class LoginPage extends StatelessWidget {
  void _login(BuildContext context) {
    // Login successful
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => HomePage()),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          child: Text('Login'),
          onPressed: () => _login(context),
        ),
      ),
    );
  }
}

Now when pressing back on HomePage, it won't return to LoginPage, but will exit the app.

After Registration Example

class RegisterPage extends StatelessWidget {
  void _register(BuildContext context) {
    // Registration successful
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => LoginPage()),
    );
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Registration successful! You can now login.')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Register')),
      body: Center(
        child: ElevatedButton(
          child: Text('Complete Registration'),
          onPressed: () => _register(context),
        ),
      ),
    );
  }
}

pushAndRemoveUntil - Clearing the Entire Stack

Sometimes you want to clear not just one page, but the entire stack.

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
  (route) => false, // Remove all pages
);

This is used to redirect the user to a completely new flow. For example, when logging out, all pages are cleared, only the login screen remains.

Logout Example

void logout(BuildContext context) {
  Navigator.pushAndRemoveUntil(
    context,
    MaterialPageRoute(builder: (context) => LoginPage()),
    (route) => false,
  );
}

Named Routes

For larger projects, writing MaterialPageRoute every time is tedious. You can use named routes.

Defining Routes in main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(),
        '/detail': (context) => DetailPage(),
        '/profile': (context) => ProfilePage(),
        '/settings': (context) => SettingsPage(),
      },
    );
  }
}

Navigation with Named Routes

// Normal navigation
Navigator.pushNamed(context, '/detail');

// Prevent back navigation
Navigator.pushReplacementNamed(context, '/home');

// Clear stack
Navigator.pushNamedAndRemoveUntil(
  context,
  '/login',
  (route) => false,
);

Sending Data with Named Routes

// Sending data
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {
    'id': 123,
    'name': 'John',
  },
);

// Receiving data
class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as Map;
    
    return Scaffold(
      appBar: AppBar(title: Text(args['name'])),
      body: Center(
        child: Text('ID: ${args['id']}'),
      ),
    );
  }
}

Navigation with Animation

You can add different transition animations:

Slide from Right (Default)

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => DetailPage()),
);

Slide from Bottom

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailPage(),
    fullscreenDialog: true,
  ),
);

Custom Animation

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => DetailPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(0.0, 1.0);
      const end = Offset.zero;
      const curve = Curves.easeInOut;

      var tween = Tween(begin: begin, end: end).chain(
        CurveTween(curve: curve),
      );

      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  ),
);

Intercepting Back Button

Performing custom action when back button is pressed on Android:

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // Ask user
        final shouldPop = await showDialog<bool>(
          context: context,
          builder: (context) => AlertDialog(
            title: Text('Are you sure you want to exit?'),
            actions: [
              TextButton(
                child: Text('No'),
                onPressed: () => Navigator.pop(context, false),
              ),
              TextButton(
                child: Text('Yes'),
                onPressed: () => Navigator.pop(context, true),
              ),
            ],
          ),
        );
        
        return shouldPop ?? false;
      },
      child: Scaffold(
        appBar: AppBar(title: Text('Page')),
        body: Center(child: Text('Content')),
      ),
    );
  }
}

Practical Tips

1. Use Context Correctly

// ❌ Wrong - build context usage
Navigator.push(context, ...);

// ✅ Correct - proper context
Builder(
  builder: (BuildContext context) {
    return ElevatedButton(
      onPressed: () => Navigator.push(context, ...),
      child: Text('Go'),
    );
  },
)

2. Using Async/Await

final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SelectPage()),
);

if (result != null) {
  print('Result: $result');
}

3. Avoid Unnecessary Navigator Usage

// ❌ Unnecessary - within the same page
Navigator.push(context, MaterialPageRoute(...));

// ✅ Use state update
setState(() {
  selectedIndex = 1;
});

Summary

Navigation in Flutter:

  • push(): Open new page
  • pop(): Go back
  • pushReplacement(): Prevent back navigation
  • pushAndRemoveUntil(): Clear stack
  • pushNamed(): Use named routes

Data Transfer:

  • Send data forward using constructor
  • Return data using pop(context, data)

Back Stack Management:

  • Use pushReplacement after Login/Register
  • Clear stack with pushAndRemoveUntil when logging out

Navigator is one of Flutter's most fundamental features. Once you get used to it, it's very easy to use!


Stuck with navigation?

See you in the next article! 🚀