Routes and APIs
Learning objectives
- You know how to create and use routes with parameters.
Paths with variables go well with online APIs. As an example, the dummyJSON site at https://dummyjson.com/ hosts a few API endpoints for testing applications. Their API endpoint /products offers a JSON-formatted list of products, while the API endpoint /products/1 offers information of a specific product.
With go_router
, developing an application that relies on such an API is relatively straightforward. We would (1) wrap the API with an appropriate service, (2) create a screen to show data from the service, (3) create the routing that corresponds to the service. This is demonstrated below.
Wrapping an API
First, we would create a class ProductService
that could be used to retrieve content from the API. Further, to be able to represent products with objects, we would also create a class Product
. The following example outlines these two classes -- the method getProduct
is the key for retrieving new products.
import 'package:http/http.dart' as http;
import 'dart:convert';
class ProductService {
final _endpoint = 'https://dummyjson.com/products/';
Future<Product> getProduct(int id) async {
var response = await http.get(Uri.parse('$_endpoint$id'));
var data = jsonDecode(response.body);
return Product(data['title'], data['description']);
}
}
class Product {
String title;
String description;
Product(this.title, this.description);
}
Showing products and navigating
For showing a product and for navigating between products, we would create a ProductScreen
. The ProductScreen
would contain both a property that holds the identifier of the shown product and the description of how the widget used to display the product would be created. For the present case, we would create a separate class ProductInfo
that would display product information; in addition, we would have a button that could be used to navigate to the next product.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ProductScreen extends StatelessWidget {
final int productId;
const ProductScreen(this.productId);
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
ProductInfo(ProductService().getProduct(productId)),
ElevatedButton(
onPressed: () => context.go("/products/${productId + 1}"),
child: const Text("Next product!")),
])));
}
}
class ProductInfo extends StatelessWidget {
final Future<Product> product;
const ProductInfo(this.product);
Widget build(BuildContext context) {
return FutureBuilder<Product>(
future: product,
builder: (BuildContext context, AsyncSnapshot<Product> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading product.");
} else if (snapshot.hasError) {
return Text("Error retrieving product: ${snapshot.error}");
} else if (!snapshot.hasData) {
return const Text("No product data.");
} else {
Product product = snapshot.data!;
return Column(
children: [
Text(product.title),
Text(product.description),
],
);
}
}
);
}
}
In the above example, we use FutureBuilder
to create the widget used to display the product information. When the product is still being retrieved from the API, we would show the message "Loading product." to the user. Once the data is there, a Column
with two Text
widgets would be used to show the information.
Routing and application
For routing, we would create a home path /
that corresponds to a screen (here a screen with a button that navigates to /products/1
). Further, we would create a path /products/:id
that allows retrieving and showing a given product from the API. The path /products/:id
uses the ProductScreen
shown above.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => HomeScreen()),
GoRoute(
path: '/products/:id',
builder: (context, state) =>
ProductScreen(int.parse(state.pathParameters['id']!))),
],
);
runApp(MaterialApp.router(routerConfig: router));
}
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => context.go("/products/1"),
child: const Text("Show a product!"))));
}
}
Project in a single file
In a single file, the above project would look as follows.
State or no state?
You may have noticed that we have previously been using Stateful widgets and Riverpod, but now again used a Stateless widget.
Stateful widgets and Riverpod are used for managing state, where Riverpod is in addition particularly suitable for sharing state across the application.
In the above application, there is no explicit state to track. The key information is stored in the path, which is used to make the API request. In such a case, when there is no need for state, Stateless widgets are perfect.