Charts and graphs
Using fl_chart for responsive visualizations
You could offer insights about transactions in the database in any number of ways. First, you'll allow the user to slice and dice their data by selecting one of the cells in the table:
If they tap a Purchaser name, you'll show:
A line chart tracking what purchasers spend over time
A pie chart showing the proportion of spend for each product code by that purchaser
If they tap a Purchase date, you'll show:
A pie chart showing the proportion of spend by each purchaser on that date
A pie chart showing the proportion of spend for each product code on that date
If they tap a Product code, you'll show:
A line chart tracking the purchase of that product over time
A pie chart showing the proportion of spend on this product by each purchaser
You could create a separate page for each of these, but you can fairly easily combine them into one using route parameters.
The page and route#
In your lib/pages/
folder, create a new file detail.page.dart
that contains a RevExDetailPage
widget with a RevExScaffold
in it:
State<RevExDetailPage> createState() => _RevExDetailPageState();
The detail page needs to know two things: which database column (or class field) to filter on, and which value to filter by. Accept those as String parameters to the widget:
"Filtering ${widget.filterColumnName} with value ${widget.filterColumnValue}"),
Now define the route in lib/router.dart
.
import 'package:revenue_explorer/pages/overview.page.dart';
Path parameters in go_router
start with a colon and are accessed via state.params
in the pageBuilder
. The null assertion on those parameters could result in an error if you try to visit the route without defining them properly, so you'll need to watch out for that.
The name
field on a GoRoute is optional, but it allows you to use the context.goNamed
method to provide a more structured approach to changing routes, which is nice when you have path parameters like this.
Now, with some updates to RevExOverviewPage, you can provide a way to navigate to the Detail page whenever a Purchaser, Date, or Product Code cell is tapped. It may be nice to show an icon on cells that are tappable to clue the user in, so create a widget for that:
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Now in _TransactionDataSource.getRow
, use this in each tappable cell:
DateFormat.yMd().format(DateTime.parse(tx.purchaseDate));
You may wonder why you went to the trouble of creating a whole new StatelessWidget instead of simply writing a method that returns the widget tree you need, like so:
DateFormat.yMd().format(DateTime.parse(tx.purchaseDate));
The answer is you could. A widget is just a class instance, so it can be returned from a method. And in this case, there wouldn't be much of a penalty for doing so. In fact, you could write your whole app using methods that return widget trees, much like a React app built with functional components, instead of writing StatelessWidget and StatefulWidget classes with build
methods. But this would be a very bad habit to get into.
Flutter is packed with optimizations based on the assumption that your app is a tree of StatelessWidget and StatefulWidget instances. It can re-render things quickly and cheaply because it tracks the parameters and state of each of these and knows which ones to ignore (i.e., there's no reason they would have changed since the last render).
If you sidestep this by writing your own methods—effectively writing massive build
trees instead of breaking down your UI into smaller Stateless/StatefulWidget classes—you're missing out on a ton of zero-effort performance gains.
Your table cells look tappable now. Make them actually respond to taps:
onTap: () => _viewDetail('purchaserName', tx.purchaserName),
It would be better if you didn't have to use magic strings (e.g., 'purchaserName'
) in this code. Dart doesn't have string-valued enums, but you can define a class with static String fields for the same ergonomics:
static const String purchaserName = 'purchaserName';
Now swap out the strings in _TransactionDataSource.getRow
.
DateFormat.yMd().format(DateTime.parse(tx.purchaseDate));
Now when you tap on a cell, you'll navigate to the Transaction Detail page and see a message indicating which column and value you're filtering on.
There's one quick issue you should solve: once you've navigated to the Detail page, there's no easy way to go back to the Overview. You can fix that by using context.pushNamed
instead of context.goNamed
in the _viewDetail
method:
context.pushNamed('detail', params: <String, String>{
context.go
and context.goNamed
replace the current Navigator history, whereas context.push
and context.pushNamed
add a new entry to it. By default, Flutter's AppBar widget recognizes when you're above the first layer of the Navigator's history stack and automatically provides a Back button.
This lesson preview is part of the Line-of-Business Mobile Apps with Flutter and Dart course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Line-of-Business Mobile Apps with Flutter and Dart, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
