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:

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:

Although path and query parameters in go_router only accept String values, you can pass objects and other kinds of values to a route using the extra field. However, this makes the route inaccessible by deep-link, so if you want the page to be shareable you should avoid it.

Now define the route in lib/router.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:

Now in _TransactionDataSource.getRow, use this in each tappable cell:

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:

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:

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:

Now swap out the strings in _TransactionDataSource.getRow.

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.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.

Unlock This Course

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.

Thumbnail for the \newline course Line-of-Business Mobile Apps with Flutter and Dart