Navigating between screens in Flutter is crucial for any app. And while the built-in Navigator API provides functionality, it can get complex for larger projects.
This is where go_router
shines, offering a more declarative, URL-based, and feature-rich navigation system. This article delves deep into every detail of go_router
, guiding you from setup to advanced features like redirection and nested routes.
go_router
is a flexible and lightweight routing library for Flutter that simplifies the navigation process and provides a clean API for managing routes, passing parameters, and handling redirects. It’s designed to be easy to use while offering advanced features for more complex navigation requirements.
Navigation plays a crucial role in crafting seamless user experiences. While the built-in Navigator 2.0 offers versatility, it can become complex in larger projects. Here's where go_router
steps in and helps simplify the process significantly.
Table of Contents:
Prerequisites
To follow along with this article and build the example application, you'll need:
Flutter SDK: Ensure you have Flutter installed and configured on your development machine. You can find installation instructions on the official Flutter website.
Basic Flutter knowledge: Familiarity with Flutter widgets, state management (even basic
setState
), and general app development concepts will be helpful.Dart language basics: A good understanding of Dart syntax, classes, and functions is essential.
An IDE: Visual Studio Code or Android Studio with the Flutter and Dart plugins installed.
What We'll Build
By the end of this article, we will have built a minimalistic shopping application that demonstrates the core functionalities of go_router
. This application will have the following features:
A Product Listing screen displaying a grid of products.
A Product Details screen showing detailed information about a selected product.
A Product Purchase screen that confirms a product purchase.
Navigation between these screens using
go_router
, including passing data via query and path parameters.Route Redirection and Exit Guards for enhanced navigation control.
Installation
To begin, add go_router
to your pubspec.yaml
file:
dependencies:
go_router: ^13.0.0
This adds the go_router
package as a dependency to your project, allowing you to use its functionalities.
Import it in your Dart files:
import 'package:go_router/go_router.dart';
This statement makes all the classes and functions provided by the go_router
package available for use in your Dart file.
How to Define Routes
Create a list of GoRoute
objects, each defining a route:
final routes = [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/products/:id',
builder: (context, state) => ProductDetailsScreen(productId: state.params['id']!),
),
// ... more routes
];
Here’s what’s going on in this code:
final routes = [...]
: This declares a final list namedroutes
which will hold all our route configurations.GoRoute(...)
: This is the core class for defining a route. EachGoRoute
object represents a distinct path in your application.path: '/'
: Thepath
property defines the URL path for this route. In this case,/
represents the root or home screen of the application.builder: (context, state) => const HomeScreen()
: Thebuilder
property is a function that returns the widget to be displayed when this route is active.context
provides the build context, andstate
gives access to route-specific information like parameters. Here, it builds aHomeScreen
widget.path: '/products/:id'
: This route defines a dynamic path. The:id
part is a path parameter, meaning that whatever value is in that position in the URL will be captured as a parameter.builder: (context, state) => ProductDetailsScreen(productId: state.params['id']!)
: When this route is activated, it builds aProductDetailsScreen
.state.params['id']!
accesses the value of theid
path parameter. The!
asserts thatid
will not be null.
How to Create the Router
Instantiate a GoRouter
object, passing the routes and integrating it with your app's MaterialApp
:
MaterialApp.router(
routeInformationParser: GoRouter.of(context).routeInformationParser,
routerDelegate: GoRouter(routes: routes),
// ... other MaterialApp properties
)
Here’s what’s going on in this code:
MaterialApp.router(...)
: This is a special constructor forMaterialApp
that integrates with a router delegate, likeGoRouter
.routerConfig: router,
orrouteInformationParser: GoRouter.of(context).routeInformationParser, routerDelegate: GoRouter(routes: routes),
: These properties are crucial forgo_router
to manage navigation.routeInformationParser
: Responsible for parsing the route information from the platform (for example, URL in a web browser) into a data structure that the router can understand.routerDelegate
: Responsible for building and managing the navigation stack based on the parsed route information.
GoRouter(routes: routes)
: This creates an instance ofGoRouter
, passing the list ofGoRoute
objects we defined earlier.
How to Navigate Between Screens
You can navigate programmatically using GoRouter.of(context).go()
:
GoRouter.of(context).go('/products/123');
Here’s what this code is doing:
GoRouter.of(context)
: This static method retrieves the nearestGoRouter
instance from the widget tree..go('/products/123')
: This method navigates to the specified URL path. This will replace the current route in the navigation stack.
You can also navigate using named routes like this:
GoRouter.of(context).goNamed('productDetails', params: {'id': 123});
In this code:
.goNamed('productDetails', ...)
: This method navigates to a route identified by itsname
property (which needs to be defined in theGoRoute
configuration).params: {'id': 123}
: This map provides values for any path parameters defined in the named route.
How to Pass Parameters
In most real-world applications, we don’t just navigate between screens – we also need to pass information. For example:
From a product list to a product details page, you’ll want to pass the product’s ID.
From a checkout screen, you may need to pass the product description or price.
With go_router, you can pass parameters in two main ways:
Query Parameters: Added to the URL after a
?
. Useful for optional data or filters (for example,/products?id=123
).Path Parameters: Embedded directly in the route path. Best for required values (for example,
/products/123
).
Let’s explore both.
1. Passing Query Parameters
Query parameters are flexible key-value pairs attached to the URL. These are typically used for non-essential or optional information, such as filters, search queries, or IDs.
Example: tapping a product card to open its details screen.
GestureDetector(
onTap: () => context.goNamed(
ProductDetailsScreen.routeName,
queryParameters: {'id': product.id},
),
child: SingleProduct(product: product),
);
What’s happening here?
context.goNamed(...)
: Navigates to a route using its name (defined in your routes config).queryParameters: {'id': product.id}
: Appends the product ID to the URL like this:/product-details?id=abc123
On the destination screen, you retrieve the parameter like this:
GoRoute(
path: ProductDetailsScreen.routeName,
name: ProductDetailsScreen.routeName,
builder: (context, state) {
return ProductDetailsScreen(
productId: state.uri.queryParameters['id'] ?? "",
);
},
)
state.uri.queryParameters['id']
: Extracts theid
value from the URL.?? ""
: Provides a default empty string if the parameter is missing.
Use query parameters when:
The parameter is optional.
You want to allow multiple parameters without changing the base route.
The data doesn’t fundamentally change the structure of the route.
2. Passing Path Parameters
Path parameters are part of the route itself and are usually required. Without them, the route doesn’t make sense.
Example: a purchase flow where the product description is required.
Navigate to the route:
context.goNamed(
'pay-now',
pathParameters: {
'description': product.description,
},
);
Define the route:
GoRoute(
path: 'product-purchase/:description',
name: ProductPurchaseScreen.routeName,
builder: (context, state) {
return ProductPurchaseScreen(
description: state.pathParameters['description']!,
);
},
)
What’s happening here?
path: 'product-purchase/:description'
: The:description
part defines a dynamic segment.pathParameters: {'description': product.description}
: Replaces:description
with the actual value. The URL will look like:/product-purchase/AwesomeProduct
state.pathParameters['description']!
: Retrieves the parameter inside the screen.
Use path parameters when:
The value is required (for example, ID, username, slug).
The route should not exist without it.
Sub-routes and ShellRoute
As your app grows, you’ll need to organize routes in a hierarchy or keep persistent UI elements like a bottom navigation bar. go_router makes this possible with Sub-routes and ShellRoute.
1. Sub-routes
Sub-routes allow you to nest routes under a parent. This keeps related routes grouped together.
Example: Profile and its settings page.
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
routes: [
GoRoute(
path: 'settings',
builder: (context, state) => SettingsScreen(),
),
],
),
/profile
: OpensProfileScreen
./profile/settings
: OpensSettingsScreen
.
Use sub-routes to keep related screens organized under one parent route.
2. ShellRoute
ShellRoute is used when you need a persistent UI wrapper (like a BottomNavigationBar
or Drawer
) that stays visible while switching between child routes.
Example: A bottom navigation layout.
ShellRoute(
builder: (context, state, child) {
return MainScaffold(child: child); // contains BottomNavigationBar
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
],
),
ShellRoute
: Wraps a persistent widget (MainScaffold
). child
: Dynamically changes depending on which route is active.
Use ShellRoute when:
You need tabs or bottom navigation.
You want a layout to remain while only the inner content changes.
Redirection and Guards
In many apps, navigation isn’t just about moving between pages. It’s also about controlling who can access what and when. For example:
Redirecting a logged-out user to the login screen.
Preventing non-admins from entering admin routes.
go_router provides two main tools here: redirects and guards.
1. Redirection
A redirect automatically reroutes users if a condition is not met.
Example: redirecting old URLs or enforcing login.
GoRoute(
path: '/old-path',
redirect: (state) => '/new-path',
),
GoRoute(
path: '/dashboard',
builder: (context, state) => DashboardScreen(),
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn();
return isLoggedIn ? null : '/login';
},
),
/old-path
: Always redirects to/new-path
./dashboard
: Redirects to/login
if the user is not logged in.
2. Guards
Guards are like “checks” placed on routes. They decide if a user can access a route or not.
Example: restricting access to admins only.
GoRoute(
path: '/admin',
builder: (context, state) => AdminScreen(),
redirect: (context, state) {
final isAdmin = AuthService.isAdmin();
return isAdmin ? null : '/not-authorized';
},
),
If isAdmin
is true
, the user can enter /admin
. Otherwise, they’re redirected to /not-authorized
.
Use redirects and guards for:
Authentication flows (login/logout).
Role-based access (admin vs user).
Handling deprecated or changed routes.
How to Set Up a Real Flutter Project Using Go Router
Before diving into GoRouter, let's start by setting up a new Flutter project and organizing the codebase. The project structure includes the following folders and files:
go_router_project/
|-- lib/
| |-- main.dart
| |-- models/
| | |-- product.dart
| |-- controller/
| | |-- product_controller.dart
| |-- config/
| | |-- route_config.dart
| |-- screens/
| | |-- product_details_screen.dart
| | |-- product_list_screen.dart
| | |-- product_purchase_screen.dart
| |-- widgets/
| |-- bottom_container.dart
| |-- color_container.dart
| |-- ratings.dart
| |-- search_section.dart
| |-- show_modal.dart
| |-- single_product.dart
|-- pubspec.yaml
Now, open the pubspec.yaml
file and add the following dependency:
dependencies:
go_router: ^13.0.0
Save the file and run flutter pub get
in the terminal to fetch the dependency.
We'll be creating a minimalistic shopping app with just three pages.
Project Structure
1. main.dart
:
Replace the code in lib/main.dart
with this:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'go_router/config/route_config.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
return MaterialApp.router(
title: 'Flutter GoRouter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
useMaterial3: true,
),
routerConfig: router, // go router
);
}
}
Here’s what’s going on in this code:
main()
: The entry point of the Flutter application. It runs theMyApp
widget.SystemChrome.setSystemUIOverlayStyle(...)
: This configures the system UI overlay, specifically setting the status bar to be transparent and its icons to be dark.MaterialApp.router(...)
: This is the root widget of our application, configured withgo_router
.title: 'Flutter GoRouter'
: Sets the title of the application.theme: ThemeData(...)
: Defines the visual theme for the application, using abrown
seed color and Material 3 design.routerConfig: router
: This is wherego_router
is integrated.router
is theGoRouter
instance defined inroute_config.dart
.
2. Model
The model
folder is where we define our data structures. A model is simply a Dart class that represents the shape of the data you’ll be working with in your app.
For example, in this project, Product
is the model. It holds details such as id
, name
, imageUrl
, description
, price
, and so on. Models don’t handle logic or UI, they’re just blueprints for data.
Think of models as the foundation. Whenever your app fetches, stores, or manipulates product information, it uses this Product
model for consistency. We are going to create a model called product.dart
product.dart
:
Add this code to lib/models/product.dart
:
import 'package:flutter/foundation.dart';
class Product {
final String id;
final String name;
final String imageUrl;
final String description;
final double price;
final double previousPrice;
final String colors;
Product({
required this.id,
required this.name,
required this.imageUrl,
required this.description,
required this.previousPrice,
required this.price,
required this.colors,
});
factory Product.initial() => Product(
id: '',
name: '',
imageUrl: '',
description: '',
previousPrice: 0.0,
price: 0.0,
colors: '',
);
}
Product
class: This class defines the structure for a product, with properties likeid
,name
,imageUrl
,description
,price
,previousPrice
, andcolors
.Product.initial()
: A factory constructor to create an emptyProduct
object, useful for initialization.
3. Controller
The controllers
folder contains classes that manage business logic, how data flows in and out of your app. Controllers sit between your views (UI) and your models (data).
In this example, the ProductController
is a simple in-memory data provider. It:
Stores a list of
Product
objects.Exposes a
findById()
method so we can look up a product quickly.Provides access to the product list via the
products
getter.
In larger apps, controllers often fetch data from APIs, handle caching, or manage app state. Here, it’s kept simple for learning purposes. We are going to create a product controller.
product_controller.dart
:
Add this code to lib/controllers/product_controller.dart
:
import '../models/product.dart';
class ProductController {
Product findById(String? id) {
return _products.firstWhere((product) => product.id == id);
}
List<Product> get products => _products;
final List<Product> _products = [
Product(
id: 'p7',
name: 'Leather BackPack',
imageUrl:
'https://images.unsplash.com/photo-1571689936114-b16146c9570a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NzR8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'The stronger the better it is to load it with all that the eyes sees useful and needful too. BackPack is a all-fit leather strong bag for carrying anything the hands can store and it\'s literally worth any penny',
price: 30.9,
previousPrice: 40.9,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p1',
name: 'Smart Watch',
imageUrl:
'https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZHVjdHxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60',
description: 'A white smart watch with good features and more',
price: 29.99,
previousPrice: 39.99,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p16',
name: 'PowerBook',
imageUrl:
'https://get.pxhere.com/photo/laptop-computer-macbook-mac-screen-water-board-keyboard-technology-air-mouse-photo-airport-aircraft-tablet-aviation-office-black-monitor-keys-graphic-hardware-image-pc-exhibition-multimedia-calculator-vector-water-cooling-floppy-disk-phased-out-desktop-computer-netbook-personal-computer-computer-monitor-electronic-device-computer-hardware-display-device-448748.jpg',
description:
'Awesome hardware, crappy keyboard and a hefty price. Buy now before a one is released!',
price: 2299.99,
previousPrice: 3299.99,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p2',
name: 'Red Sneakers',
imageUrl:
'https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'Perfect for your joggers and black T-shirts and more. The sneakers comes in different sizes and colors. You never know when that T-shirt needs some styles with the soft layers of a sneakers',
price: 199.99,
previousPrice: 299.99,
colors: 'yellow,grey,black,red,teal',
),
Product(
id: 'p3',
name: 'Nikon Camera',
imageUrl:
'https://images.unsplash.com/photo-1564466809058-bf4114d55352?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjB8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'You can only see clearer with your eyes but a camera saves the memory in it\'s eyes',
price: 89.9,
previousPrice: 109.9,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p4',
name: 'HeadSets',
imageUrl:
'https://images.unsplash.com/photo-1583394838336-acd977736f90?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjJ8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'The louder the sound, the better it feels inside with the body',
price: 120.1,
previousPrice: 150.1,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p5',
name: 'Amazon SoundBox',
imageUrl:
'https://images.unsplash.com/photo-1543512214-318c7553f230?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjR8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'Automated soundbox with voice recognition and more. What could be more better',
price: 78.19,
previousPrice: 88.19,
colors: 'red,grey,black,indigo,purple',
),
Product(
id: 'p6',
name: 'Xbox 360 GamePads',
imageUrl:
'https://images.unsplash.com/photo-1600080972464-8e5f35f63d08?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mzd8fHByb2R1Y3R8ZW58MHx8MHx8&auto=format&fit=crop&w=800&q=60',
description:
'You never know when it is time to touch it better except the pads with xbox is there to assist',
price: 98.99,
previousPrice: 108.99,
colors: 'red,grey,black,indigo,purple',
),
];
}
ProductController
class: This class acts as a simple data source for our products.findById(String? id)
: A method to find a product by its ID from the_products
list.products
getter: Provides access to the list of_products
. iv._products
: A private list ofProduct
objects, pre-populated with sample product data.
4. Config
The config
folder stores all the configuration and setup files for your app. In this project, it’s where we keep route_config.dart
, which contains all the go_router setup and route definitions.
This is important because:
Routes can get complex as your app grows.
Having all navigation setup in one place keeps things clean and manageable.
Config files are also a great place to put things like app-wide constants, environment settings, or themes.
Think of config as the central wiring of your app. It’s not about data or logic, but about how the app is structured and tied together. We are going to create a route config here.
route_config.dart
:
Add this code to lib/config/route_config.dart
:
import 'package:flutter/material.dart';
import 'package:get_it_auto_router_go_router/go_router/controllers/product_controller.dart';
import 'package:go_router/go_router.dart';
import '../models/product.dart';
import '../screens/product_details_screen.dart';
import '../screens/product_list_screen.dart';
import '../screens/product_purchase_screen.dart';
/// The route configuration.
final GoRouter router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const ProductListScreen();
},
routes: <RouteBase>[
GoRoute(
path: ProductDetailsScreen.routeName,
name: ProductDetailsScreen.routeName,
builder: (BuildContext context, GoRouterState state) {
return ProductDetailsScreen(
productId: state.uri.queryParameters['id'] ?? "",
);
},
routes: <RouteBase>[
GoRoute(
path: 'product-purchase/:description',
name: ProductPurchaseScreen.routeName,
builder: (BuildContext context, GoRouterState state) {
return ProductPurchaseScreen(
productImage: state.uri.queryParameters['img']!,
productPrice: state.uri.queryParameters['price']!,
productName: state.uri.queryParameters['name']!,
description: state.pathParameters['description']!,
);
},
onExit: (BuildContext context) async {
final bool? confirmed = await showDialog<bool>(
context: context,
builder: (_) {
return AlertDialog(
content: const Text('Are you sure to leave this page?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
);
},
);
return confirmed ?? false;
},
)
],
)
],
),
],
);
Here’s what’s happening in this code:
final GoRouter router = GoRouter(...)
: This is the mainGoRouter
instance for our application.routes: <RouteBase>[...]
: Defines the top-level routes.GoRoute(path: '/', builder: ...)
: The root route, leading toProductListScreen
.Nested
GoRoute
forProductDetailsScreen
: This route is a child of the root.path: ProductDetailsScreen.routeName
: Uses a static constant for the path.name: ProductDetailsScreen.routeName
: Assigns a name to the route for easier navigation.builder
: Builds theProductDetailsScreen
, extractingproductId
from query parameters.v. Nested
GoRoute
forProductPurchaseScreen
: This route is a child ofProductDetailsScreen
.path: 'product-purchase/:description'
: Defines a path with adescription
path parameter.name: ProductPurchaseScreen.routeName
: Assigns a name for navigation.builder
: Builds theProductPurchaseScreen
, extractingproductImage
,productPrice
,productName
from query parameters anddescription
from path parameters.onExit: (BuildContext context) async { ... }
: This is anonExit
guard that triggers a confirmation dialog when the user tries to leave theProductPurchaseScreen
. If the user cancels, navigation is prevented.
5. Screens
Your screens are the UI pages the user interacts with. Each screen has a different role in the shopping flow:
ProductListScreen:
This is the entry screen of your app that shows all available products in a grid format.
Acts as a catalog/browse page.
Uses the
ProductController
to fetch product data.Includes a search bar (
SearchSection
) for filtering products.Navigates to the ProductDetailsScreen when a product is tapped.
ProductDetailsScreen:
This screen shows the full details of a selected product.
Displays product image, name, price, available colors, and description.
Allows the user to view a larger image modal by tapping the image.
Provides a "Buy Now" button at the bottom (via
bottomContainer
).Uses path and query parameters in navigation to pass product data to the next screen.
ProductPurchaseScreen:
This is the final confirmation screen before purchasing.
Shows the selected product image, name, price, and description.
Confirms the user’s intent to purchase with a FloatingActionButton (currently just an icon).
Completes the navigation flow: List > Details > Purchase.
Alright, now let’s go through them one by one:
product_list_screen.dart
:
Add this code to lib/screens/product_list_screen.dart
:
import 'package:flutter/material.dart';
import 'package:get_it_auto_router_go_router/go_router/controllers/product_controller.dart';
import 'package:get_it_auto_router_go_router/go_router/screens/product_details_screen.dart';
import 'package:get_it_auto_router_go_router/go_router/widgets/search_section.dart';
import 'package:get_it_auto_router_go_router/go_router/widgets/single_product.dart';
import 'package:go_router/go_router.dart';
import '../models/product.dart';
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
@override
Widget build(BuildContext context) {
ProductController productController = ProductController();
TextEditingController searchController = TextEditingController();
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
SearchSection(
searchController: searchController,
),
const SizedBox(height: 10),
Expanded(
child: GridView.builder(
itemCount: productController.products.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemBuilder: (context, index) {
Product product = productController.products[index];
return GestureDetector(
onTap: () => context.goNamed(
ProductDetailsScreen.routeName,
queryParameters: {'id': product.id},
),
child: SingleProduct(product: product),
);
},
),
),
],
),
),
);
}
}
Here’s what’s going on:
ProductListScreen
: AStatelessWidget
that displays a list of products.ProductController productController = ProductController()
: Creates an instance of theProductController
to access product data.AppBar
: Displays the title "Products".SearchSection
: A custom widget for a search bar.Expanded
withGridView.builder
: Displays the products in a scrollable grid.GestureDetector
onTap
: When a product is tapped, it navigates to theProductDetailsScreen
usingcontext.goNamed
, passing the product'sid
as a query parameter.SingleProduct
: A custom widget to display individual product information.
product_details_screen.dart
:
Add this code to lib/screens/product_details_screen.dart
:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import '../controllers/product_controller.dart';
import '../models/product.dart';
import '../widgets/bottom_container.dart';
import '../widgets/color_container.dart';
import '../widgets/ratings.dart';
import '../widgets/show_modal.dart';
class ProductDetailsScreen extends StatelessWidget {
static const routeName = 'product-details';
final String productId;
const ProductDetailsScreen({
super.key,
required this.productId,
});
@override
Widget build(BuildContext context) {
late Color colored;
// get color
Color getColor(String color) {
switch (color) {
case 'red':
colored = Colors.red;
break;
case 'purple':
colored = Colors.purple;
break;
case 'grey':
colored = Colors.grey;
break;
case 'black':
colored = Colors.black;
break;
case 'orange':
colored = Colors.orange;
break;
case 'indigo':
colored = Colors.indigo;
break;
case 'yellow':
colored = Colors.yellow;
break;
case 'blue':
colored = Colors.blue;
break;
case 'brown':
colored = Colors.brown;
break;
case 'teal':
colored = Colors.teal;
break;
default:
}
return colored;
}
ProductController productController = ProductController();
Product product = productController.findById(productId);
List<String> availableColors = product.colors.split(',');
// pay now
void payNow() {
context.goNamed(
'pay-now',
pathParameters: <String, String>{
'description': product.description,
},
queryParameters: <String, String>{
'img': product.imageUrl.toString(),
'price': product.price.toString(),
'name': product.name.toString(),
},
);
}
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent,
elevation: 0,
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.black,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: Column(
children: [
Expanded(
flex: 2,
child: GestureDetector(
onTap: () => showImageModal(context, product),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.zero,
bottom: Radius.circular(50),
),
child: Hero(
tag: product.id,
child: Image.network(
product.imageUrl,
fit: BoxFit.cover,
width: double.infinity,
),
),
),
),
),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 30,
),
),
const SizedBox(height: 5),
ratings(),
const SizedBox(height: 5),
Row(
children: [
Text(
'\$${product.price.toString()}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 3),
Text(
'\$${product.previousPrice.toString()}',
style: const TextStyle(
fontSize: 15,
color: Colors.grey,
decoration: TextDecoration.lineThrough,
),
),
],
),
const Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Available in stocks',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(
'Out of stocks',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.deepOrange,
decoration: TextDecoration.lineThrough,
),
),
],
),
const SizedBox(height: 10),
const Text(
'Colors Available',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (var color in availableColors)
buildContainer(
color,
getColor,
)
],
),
const SizedBox(height: 15),
const Text(
'About',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 10),
Text(
product.description,
textAlign: TextAlign.justify,
),
],
),
),
),
],
),
bottomSheet: bottomContainer(product, payNow),
);
}
}
That’s a lot – here’s what this code is doing:
ProductDetailsScreen
: AStatelessWidget
that displays the details of a single product.static const routeName = 'product-details'
: Defines a static constant for the route name, ensuring consistency.productId
: This is a required parameter for the screen, passed during navigation.getColor(String color)
: A helper function to convert color names (strings) intoColor
objects.ProductController productController = ProductController()
: Accesses product data.Product product = productController.findById(productId)
: Retrieves the specific product based on theproductId
received.payNow()
: A function that navigates to theProductPurchaseScreen
usingcontext.goNamed
, passing product details as both path and query parameters.AppBar
: Displays a back arrow to navigate back.Expanded
for product image: Displays the product image with aHero
animation for smooth transitions.GestureDetector
allows tapping the image to show a modal.Expanded
for product details: Displays product name, ratings, prices, availability, available colors, and description.bottomSheet: bottomContainer(product, payNow)
: Attaches a custombottomContainer
widget to theScaffold
, which includes the "Buy Now" button.
product_purchase_screen.dart
:
Add this code to lib/screens/product_purchase_screen.dart
:
import 'package:flutter/material.dart';
class ProductPurchaseScreen extends StatelessWidget {
const ProductPurchaseScreen({
super.key,
required this.productImage,
required this.productName,
required this.productPrice,
required this.description,
});
static const routeName = 'pay-now';
final String productName;
final String productPrice;
final String productImage;
final String description;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: const FloatingActionButton(
onPressed: null,
child: Icon(
Icons.check_circle,
),
),
appBar: AppBar(
title: const Text('Purchase Item'),
),
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(productImage),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
productName,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
Text(
'\$$productPrice',
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 16,
color: Colors.grey,
),
)
],
),
const SizedBox(height: 10),
Text(
description,
style: const TextStyle(
fontSize: 16,
),
),
],
),
),
),
),
);
}
}
Here’s what’s going on:
ProductPurchaseScreen
: AStatelessWidget
that confirms the product purchase.static const routeName = 'pay-now'
: Defines the route name.productImage
,productName
,productPrice
,description
: These are required parameters received from the previous screen.FloatingActionButton
: Displays a checkmark icon, thoughonPressed
is currently null.AppBar
: Displays the title "Purchase Item".SingleChildScrollView
: Makes the content scrollable.Image.network(productImage)
: Displays the product image received.Row
for product name and price: Shows the product's name and its price.Text(description)
: Displays the product's description.
6. Widgets
Widgets are the reusable building blocks of your UI. Instead of duplicating UI code in multiple screens, you break them into widgets.
bottom_container.dart
:
Add this code to lib/widgets/bottom_container.dart
:
// bottom container
import 'package:flutter/material.dart';
import '../models/product.dart';
Container bottomContainer(Product productDetails,Function payNow) {
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 18.0,
vertical: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Price',
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
const SizedBox(height: 5),
Text(
'\$${productDetails.price}',
style: const TextStyle(
color: Colors.brown,
fontWeight: FontWeight.w700,
fontSize: 25,
),
)
],
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
height: 50,
width: 80,
decoration: BoxDecoration(
color: Colors.brown.withOpacity(0.3),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(5),
topLeft: Radius.circular(5),
),
),
child: const Center(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Icon(
Icons.shopping_cart_checkout,
color: Colors.white,
),
SizedBox(width: 15),
Text(
'1',
style: TextStyle(
color: Colors.white,
),
),
],
),
),
),
GestureDetector(
onTap: () => payNow(),
child: Container(
height: 50,
width: 120,
decoration: const BoxDecoration(
color: Colors.brown,
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(5),
topRight: Radius.circular(5),
),
),
child: const Center(
child: Text(
'Buy Now',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
),
),
)
],
)
],
),
),
);
}
In this code:
bottomContainer
: A function that returns aContainer
widget for the bottom sheet. It displays the product price and a "Buy Now" button.GestureDetector
onTap
: The "Buy Now" button triggers thepayNow
function passed as an argument.
ratings.dart
:
Add this code to lib/widgets/ratings.dart
:
import 'package:flutter/material.dart';
Widget ratings() => const Row(
children: [
Icon(Icons.star, color: Colors.deepOrange, size: 15),
Icon(Icons.star, color: Colors.deepOrange, size: 15),
Icon(Icons.star, color: Colors.deepOrange, size: 15),
Icon(Icons.star, color: Colors.deepOrange, size: 15),
Icon(Icons.star, color: Colors.deepOrange, size: 15),
SizedBox(width: 20),
Text('(3400 Reviews)')
],
);
ratings()
: A simple widget that displays a row of five orange stars and a review count.
color_container.dart
:
Add this code to lib/widgets/color_container.dart
:
// build container for color
import 'package:flutter/cupertino.dart';
Widget buildContainer(String color,Function getColor) {
return Container(
height: 5,
width: 40,
decoration: BoxDecoration(
color: getColor(color),
borderRadius: BorderRadius.circular(20),
),
);
}
Here’s what’s going on:
buildContainer
: A function that creates a small, roundedContainer
to represent an available product color. It takes the color name as a string and agetColor
function to convert it to aColor
object.
search_section.dart
:
Add this code to lib/widgets/search_section.dart
:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SearchSection extends StatelessWidget {
const SearchSection({
super.key,
required this.searchController,
});
final TextEditingController searchController;
@override
Widget build(BuildContext context) {
return TextField(
controller: searchController,
decoration: InputDecoration(
prefixIcon: const Icon(
CupertinoIcons.search,
color: Colors.black,
),
hintText: 'Enter search keyword',
label: const Text(
'Search Here',
),
fillColor: Colors.grey.withOpacity(0.1),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
In this code:
SearchSection
: AStatelessWidget
that displays a search input field.searchController
: ATextEditingController
to manage the text input.InputDecoration
: Styles the text field with a search icon, hint text, label, and rounded borders.
show_modal.dart
:
Add this code to lib/widgets/show_modal.dart
:
// show modal for image
import 'package:flutter/material.dart';
import '../models/product.dart';
void showImageModal(BuildContext context,Product product) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(12),
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Stack(children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image(
width: double.infinity,
fit: BoxFit.cover,
image: NetworkImage(product.imageUrl),
),
),
Positioned(
right: 1,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey.withOpacity(0.5),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Text(product.name),
const SizedBox(width: 5),
Text(
'\$${product.price}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
)
]),
),
);
},
);
}
In this code:
showImageModal
: A function that displays a dialog with a larger view of the product image and its name and price.Dialog
: A material design dialog.Stack
withPositioned
: Used to overlay the product name and price on top of the image.
single_product.dart
:
Add this code to lib/widgets/single_product.dart
:
import 'package:flutter/material.dart';
import '../models/product.dart';
class SingleProduct extends StatelessWidget {
const SingleProduct({
super.key,
required this.product,
});
final Product product;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
topLeft: Radius.circular(10),
),
child: Hero(
tag: product.id,
child: Image.network(
product.imageUrl,
height: 120,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 10),
Text(
product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${product.price}'),
Text(
'\$${product.price}',
style: const TextStyle(
decoration: TextDecoration.lineThrough,
),
),
],
),
)
],
),
);
}
}
Here’s what’s happening:
SingleProduct
: AStatelessWidget
that displays a single product item in the grid.product
: TheProduct
object to be displayed.Container
: Provides a background color and rounded borders.Hero
animation for image: Facilitates a smooth animation when transitioning to theProductDetailsScreen
.Text
for product name: Displays the product name, truncated if too long.Row
for prices: Shows the current price and the previous price with a strikethrough.
A Few Screenshots:
Conclusion
go_router
is a powerful and flexible routing library for Flutter, offering a clean and intuitive API for navigation. Whether you're building a simple app or a complex navigation structure, go_router
provides the tools you need to create a seamless user experience.
By following this comprehensive guide, you should now be well-equipped to integrate and leverage go_router
in your Flutter projects. The provided example of a minimalistic shopping app demonstrates practical application of its features.
References
For more advanced features and detailed code examples, refer to the official
go_router
documentationYou can also check out the
go_router
GitHub repository