Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Cross-Platform UIs with Flutter
Cross-Platform UIs with Flutter

Cross-Platform UIs with Flutter: Unlock the ability to create native multiplatform UIs using a single code base with Flutter 3

eBook
€15.99 €22.99
Paperback
€27.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Table of content icon View table of contents Preview book icon Preview Book

Cross-Platform UIs with Flutter

Building a Race Standings App

In this chapter, we’ll create a more complex project than the counter app we built previously. We’re going to create the UI of a generic racing game that shows both the results of races and drivers’ standings.

Other than code quality, we will also pay a lot of attention to the user experience (UX); this includes localization, internationalization, responsiveness, color contrast, and more, which will be used to create a high-quality result. We will also learn how to manually work with the device’s pixels with CustomPainter, for those cases where Flutter widgets aren’t enough.

In this chapter, we will cover the following topics:

  • Creating responsive screens using the LayoutBuilder widget
  • Using the intl package to localize the app
  • Working with images – PNGs and vectorial files
  • Using custom painters to paint complex UI elements

Let’s get started!

Technical requirements

We recommend that you work on the stable channel and work with Flutter version 2.5 or newer. Any version after Flutter 2.0 could still be okay, but we can’t guarantee that you won’t encounter unexpected problems while trying to compile our source code.

Since we’re going to test the UI on various screen sizes, we will compile it for the web so that we can resize the browser window to easily emulate different viewports. While very convenient and quick to test, you could spin up various emulators with different screen sizes or use your own physical devices.

We also recommend using either Android Studio or Visual Studio Code (VS Code): choose the one you like more!

The complete source code for this project can be found at https://github.com/PacktPublishing/Cross-Platform-UIs-with-Flutter/tree/main/chapter_2.

Setting up the project

Before we start creating the app, we need to prepare the environment and make sure we care about the UX from the beginning.

Create a new Flutter project in your favorite IDE and make sure that you enable web support by clicking on the Add Flutter web support checkbox. A basic analysis_options.yaml file will be created for you. Even if it’s not strictly required, we strongly recommend that you add more rules to enhance your overall code quality.

Tip

If you want to easily set up the analysis_options.yaml file with the configuration we’ve recommended, just go to this project’s GitHub repository and copy the file into your project! You can also find a quick overview of the rules in the Setting up the project section of Chapter 1, Building a Counter App with History Tracking to Establish Fundamentals.

Since we aren’t uploading this project to https://pub.dev/, make sure that your pubspec.yaml file has the publish_to: 'none' directive uncommented. Before we start coding, we still need to set up localization, internationalization, route management, and custom text fonts.

Localization and internationalization

Localizing an app means adapting the content according to the device’s geographic settings to appeal to as many users as possible. In practical terms, for example, this means that an Italian user and an American user would see the same date but in different formats. In Italy, the date format is d-m-y, while in America, it’s m-d-y, so the app should produce different strings according to the device’s locale. It’s not only about the date, though, because localizing can also mean the following changes occur:

  • Showing prices with the proper currency (Euro, Dollar, Sterling, and so on)
  • Taking into account time zones and offsets while displaying dates for events
  • Choosing between a 24-hour or 12-hour time format
  • Deciding which decimal separator is used (a comma, a full stop, an apostrophe, and so on)

Internationalizing, which is part of the localization process, means translating your app’s text according to the device’s locale. For example, while an Italian user would read Ciao!, an American user would read Hello!, and all of this is done automatically by the app.

Setting up localization support in Flutter is very easy! Start by adding the SDK direct dependency to the pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.17.0

The intl package is maintained by the Dart team and offers numerous internationalization and localization utilities we will explore throughout this chapter, such as AppLocalization and DateFormat.

Still in the pubspec file, we need to add another line at the bottom of the flutter section:

flutter:
  generate: true

We must do this to bundle the various localization files into our app so that the framework will be able to pick the correct one based on the user’s locale settings.

The last file we need to create must be located at the root of our project, it must be called l10n.yaml exactly, and it must have the following contents:

arb-dir: lib/localization/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

This file is used to tell Flutter where the translations for the strings are located so that, at runtime, it can pick up the correct files based on the locale. We will be using English as the default, so we will set app_en.arb to template-arb-file.

Now, we need to create two ARB files inside lib/localization/l10n/ that will contain all of our app strings. The first file, app_en.arb, internationalizes our app in English:

{
    "app_title": "Results and standings",
    "results": "Results",
    "standings": "Standings",
}

The second file, app_it.arb, internationalizes our app in Italian:

{
    "app_title": "Risultati e classifiche",
    "results": "Risultati",
    "standings": "Classifica",
}

Every time you add a new entry to the ARB file, you must make sure that the ARB keys match! When you run your app, automatic code generation will convert the ARB files into actual Dart classes and generate the AppLocalization class for you, which is your reference for translated strings. Let’s look at an example:

final res = AppLocalizations.of(this)!.results;

If you’re running your app on an Italian device, the value of the res variable will be Risultati. On any other device, the variable would hold Results instead. Since English is the default language, when Flutter cannot find an ARB file that matches the current locale, it will fall back to the default language file.

Tip

Whenever you add or remove strings to/from your ARB files, make sure you always hit the Run button of your IDE to build the app! By doing this, the framework builds the newly added strings and bundles them into the final executable.

Extension methods, which are available from Dart 2.7 onwards, are a very nice way to add functionalities to a class without using inheritance. They’re generally used when you need to add some functions or getters to a class and make them available for any instance of that type.

Let’s create a very convenient extension method to be called directly on any Flutter string, which reduces the boilerplate code and saves some import statements. This is a very convenient shortcut to easily access the localization and internationalization classes that’s generated by Flutter. The following is the content of the lib/localization/localization.dart file:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
export 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Extension method on [BuildContext] which gives a quick 
/// access to the `AppLocalization` type.
extension LocalizationContext on BuildContext {
  /// Returns the [AppLocalizations] instance.
  AppLocalizations get l10n => AppLocalizations.of(this)!;
}

With this code, we can simply call context.l10n.results to retrieve the internationalized value of the Results word.

Last, but not least, we need to make sure that our root widget installs the various localization settings we’ve created so far:

/// The root widget of the app.
class RaceStandingsApp extends StatelessWidget {
  /// Creates an [RaceStandingsApp] instance.
  const RaceStandingsApp ({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Localized app title
      onGenerateTitle: (context) => context.l10n.app_title,
      // Localization setup
      localizationsDelegates: 
        AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      // Routing setup
      onGenerateRoute: RouteGenerator.generateRoute,
      // Hiding the debug banner
      debugShowCheckedModeBanner: false,
    );
  }
}

If you’re dealing with multiple languages and a lot of strings, manually working on ARB files may become very hard and error-prone. We suggest that you either look at Localizely, an online tool that handles ARB files, or install an ARB plugin manager in your IDE.

In the preceding code, you may have noticed the RouteGenerator class, which is responsible for route management. That’s what we’re going to set up now!

Routes management

In this app, we’re using Flutter’s built-in routing management system: the Navigator API. For simplicity, we will focus on Navigator 1.0; in Chapter 5, Exploring Navigation and Routing with a Hacker News Clone, we will cover the routing topic in more depth.

Let’s create a simple RouteGenerator class to handle all of the routing configurations of the app:

abstract class RouteGenerator {
  static const home = '/';
  static const nextRacesPage = '/next_races';
  /// Making the constructor private since this class is 
  /// not meant to be instantiated.
  const RouteGenerator._();
  static Route<dynamic> generateRoute(RouteSettings 
    settings) {
    switch (settings.name) {
      case home:
        return PageRouteBuilder<HomePage>(
          pageBuilder: (_, __, ___) => const HomePage(),
        );
      case nextRacesPage:
        return PageRouteBuilder<NextRacesPage>(
          pageBuilder: (_, __, ___) => 
            const NextRacesPage(),
        );
      default:
        throw const RouteException('Route not found');
    }
  }
}
/// Exception to be thrown when the route doesn't exist.
class RouteException implements Exception {
  final String message;
  /// Requires the error [message] for when the route is 
  /// not found.
  const RouteException(this.message);
}

By doing this, we can use Navigator.of(context).pushNamed(RouteGenerator.home) to navigate to the desired page. Instead of hard-coding the route names or directly injecting the PageRouteBuilders in the methods, we can gather everything in a single class.

As you can see, the setup is very easy because all of the management is done in Flutter internally. We just need to make sure that you assign a name to the page; then, Navigator will take care of everything else.

Last but not least, let’s learn how to change our app’s look a bit more with some custom fonts.

Adding a custom font

Instead of using the default text font, we may wish to use a custom one to make the app’s text look different from usual. For this purpose, we need to reference the google_fonts package in the dependencies section of pubspec and install it:

return MaterialApp(
  // other properties here…
  theme: ThemeData.light().copyWith(
    textTheme: GoogleFonts.latoTextTheme(),
  ),
);

It couldn’t be easier! Here, the Google font package fetches the font assets via HTTP on the first startup and caches them in the app’s storage. However, if you were to manually provide font files assets, the package would prioritize those over HTTP fetching. We recommend doing the following:

  1. Go to https://fonts.google.com and download the font files you’re going to use.
  2. Create a top-level directory (if one doesn’t already exist) called assets and then another sub-folder called fonts.
  3. Put your font files into assets/fonts without renaming them.
  4. Make sure that you also include the OFL.txt license file, which will be loaded at startup. Various license files are included in the archive you downloaded from Google Fonts.

Once you’ve done that, in the pubspec file, make sure you have declared the path to the font files:

assets:
  - assets/fonts/

The package only downloads font files if you haven’t provided them as assets. This is good for development but for production, it’s better to bundle the fonts into your app executable to avoid making HTTP calls at startup. In addition, font fetching assumes that you have an active internet connection, so it may not always be available, especially on mobile devices.

Finally, we need to load the license file into the licenses registry:

void main() {
  // Registering fonts licences
  LicenseRegistry.addLicense(() async* {
    final license = await rootBundle.loadString(
      'google_fonts/OFL.txt',
    );
    yield LicenseEntryWithLineBreaks(['google_fonts'],
      license);
  });
  // Running the app
  runApp(
    const RaceStandingsApp(),
  );
}

Make sure that you add the LicenseRegistry entry so that, if you use the LicensePage widget, the licensing information about the font is bundled into the executable correctly.

Now that we’ve set everything up, we can start creating the app!

Creating the race standings app

The home page of the app is going to immediately provide our users with a quick way to see both race results and driver standings. Other kinds of information, such as upcoming races or a potential settings page, should be placed on other pages.

This is what the app looks like on the web, desktop, or any other device with a large horizontal viewport:

Figure 2.1 – The app’s home page on a device with a large horizontal viewport

Figure 2.1 – The app’s home page on a device with a large horizontal viewport

Having everything on a single screen would make the UI too dense because there would be too much information for the user to see. Tabs are great when it comes to splitting contents into multiple pages and they’re also very easy to handle – it’s just a matter of swiping!

On mobile devices, or smaller screen sizes, the home page looks like this:

Figure 2.2 – The app’s home page on smaller devices

Figure 2.2 – The app’s home page on smaller devices

As you can see, since there is less horizontal space, we need to rearrange the contents so that it fits with less space. Laying down contents on two columns would take too much space, so we’ve decided to create a sort of dropdown menu. The black arrow slides up and down to show or hide contents. The app has two main pages:

  • The HomePage widget, where we show the results of the past races and the current drivers’ standings.
  • The NextRaces widget, where we show a brief list of the upcoming races.

Now, let’s start creating the HomePage widget!

The HomePage widget

The home page is going to have two main tabs to display core information. This already gives us a pretty important hint regarding what we need to do: we need to create two widgets to hold the contents of each tab and we want them to be constant.

The following is the build() method for the HomePage widget:

@override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text(context.l10n.app_title),
          elevation: 5,
          bottom: TabBar(
            tabs: [
              Tab(
                icon: const Icon(Icons.list),
                text: context.l10n.results,
              ),
              Tab(
                icon: const Icon(Icons.group),
                text: context.l10n.standings,
              ),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            ResultsTab(),
            StandingsTab(),
          ],
        ),
      ),
    );
  }

Thanks to widget composition, we can use const TabBarView because both children have a constant constructor. Now, let’s learn how to build the ResultsTab and StandingsTab widgets.

The results tab

This page is responsive because it dynamically rearranges its contents to best fit the current horizontal and vertical viewport constraints. In other words, this widget lays out the contents in different ways based on the different screen sizes, thanks to LayoutBuilder:

return LayoutBuilder(
  builder: (context, dimensions) {
    // Small devices
    if (dimensions.maxWidth <= mobileResultsBreakpoint) {
      return ListView.builder(
        itemCount: resultsList.length,
        itemBuilder: (context, index) =>
          _CompactResultCard(
          results: resultsList[index],
        ),
      );
    }
    // Larger devices
    return Padding(
      padding: const EdgeInsets.symmetric(
        vertical: 20,
      ),
      child: ListView.builder(
        itemCount: resultsList.length,
        itemBuilder: (context, index) => 
          _ExpandedResultCard(
          results: resultsList[index],
        ),
      ),
    );
  },
);

Here, the mobileResultsBreakpoint constant has been put in lib/utils/breakpoints.dart. We are gathering all of our responsive breakpoint constants into a single file to simplify both maintenance and testing. Thanks to LayoutBuilder, we can retrieve the viewport dimensions and decide which widget we want to return.

The ExpandedResultCard widget is meant to be displayed or larger screens, so we can safely assume that there is enough horizontal space to lay down contents in two columns. Let’s learn how to do this:

Card(
  elevation: 5,
  child: Row(
    children: [
      // Race details
      Expanded(
        flex: leftFlex,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [ … ],
        ),
      ),
      // Drivers final positions
      Expanded(
        flex: 3,
        child: DriversList(
          results: results,
        ),
      ),
    ],
  ),
),

To make this widget even more responsive, we can also control the relative widths of the columns. We’re still using LayoutBuilder to decide on the flex of the Expanded widget to ensure that the content fits the space in the best possible way:

return LayoutBuilder(
  builder: (context, dimensions) {
    var cardWidth = max<double>(
      mobileResultsBreakpoint,
      dimensions.maxWidth,
    );
    if (cardWidth >= maxStretchResultCards - 50) {
      cardWidth = maxStretchResultCards;
    }
    final leftFlex = 
      cardWidth < maxStretchResultCards ? 2 : 3;
    return Center(
      child: SizedBox(
        width: cardWidth - 50,
        child: Card( ... ),
      ),
    );
);

Here, we compute the overall width of the surrounding Card and then determine the widths of the columns by computing the flex value.

The _CompactResultCard widget is meant to be displayed on smaller screens, so we need to arrange the widgets along the vertical axis using a single column. To do this, we must create a simple widget called Collapsible that has a short header and holds the contents on a body that slides up and down:

Figure 2.3 – On the left, the content is hidden; on the right, the content is visible

Figure 2.3 – On the left, the content is hidden; on the right, the content is visible

This approach is very visually effective because, considering there isn’t much horizontal space available, we immediately show the most important information. Then, if the user wants to know more, they can tap the arrow to reveal additional (but still important) information. First, we store the open/closed state of the card in an inherited widget:

/// Handles the state of a [Collapsible] widget.
class CollapsibleState extends InheritedWidget {
  /// The state of the [Collapsible] widget.
  final ValueNotifier<bool> state;
  /// Creates a [CollapsibleState] inherited widget.
  const CollapsibleState({
    Key? key,
    required this.state,
    required Widget child,
  }) : super(key: key, child: child);
  /// Conventional static access of the instance above the 
  /// tree.
  static CollapsibleState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<
      CollapsibleState>()!;
  }
  @override
  bool updateShouldNotify(CollapsibleState oldWidget) =>
      state != oldWidget.state;
}

Then, we use the SizeTransition widget to make the contents underneath appear and disappear with a sliding transition. The animation is driven by ValueListenableBuilder:

return ValueListenableBuilder<bool>(
  valueListenable: CollapsibleState.of(context).state,
  builder: (context, value, child) {
    if (!value) {
      controller.reverse();
    } else {
      controller.forward();
    }
    return child!;
  },
  child: Padding(
    padding: widget.edgeInsets,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: regions,
    ),
  ),
);

We use the child parameter to ensure that the builder won’t unnecessarily rebuild Column over and over. We only need to make sure that reverse() or forward() is called whenever the boolean’s state is changed.

Since both _CompactResultCard and _ExpandedResultCard need to display a date, we have created a mixin for the state class to be able to easily share a common formatting method:

/// A date generator utility.
mixin RandomDateGenerator on Widget {
  /// Creates a random date in 2022 and formats it as 'dd 
  /// MMMM y'.
  /// For more info on the format, check the [Intl] 
  /// package.
  String get randomDate {
    final random = Random();
    final month = random.nextInt(12) + 1;
    final day = random.nextInt(27) + 1;
    return DateFormat('dd MMMM y').format(
      DateTime(2022, month, day + 1),
    );
  }
}

The DateFormat class is included in the intl package, and it can automatically translate the date string into various languages. In this case, the 'dd MMMM y' combination prints the day in double digits, the name of the month with a capital letter, and the year in 4-digit format.

Tip

You can format the date in many ways – you just need to change the tokens in the string. We won’t cover them all here because there are thousands of possible combinations; if you do want to know more, we recommend that you look at the documentation: https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html.

Now, let’s create the drivers’ standings tab.

The drivers’ standings tab

Even though this page contains a simple list of people and their country flags and scores, there are still some considerations to make. The first one is that we don’t want to always use the entirety of the viewport’s width, like this:

Figure 2.4 – Example of bad space management in the drivers list

Figure 2.4 – Example of bad space management in the drivers list

The user may have trouble gathering all of the information at first glance because there is too much space between the important UI parts. We need to make sure that the content can shrink to fit smaller sizes, but we don’t want to always use the entire available width.

As shown in the preceding screenshot, always using the entire horizontal viewport may lead to bad user experiences. To avoid this, we’re going to set up a breakpoint that limits how the list can grow in the horizontal axis:

Figure 2.5 – Example of good space management in the drivers list

Figure 2.5 – Example of good space management in the drivers list

Here, we’ve created a new breakpoint called maxStretchStandingsCards that imposes horizontal bounds to the list so that it doesn’t grow too much. This is how the standings list is being built:

ListView.separated(
  shrinkWrap: true,
  itemCount: standingsList.length,
  itemBuilder: (context, index) {
    final item = standingsList[index];
    return ListTile(
      title: Text(item.name),
      subtitle: Text('${context.l10n.points}:
        ${item.points}'),
      leading: Column( ... ),
      trailing: NumberIndicator( ... ),
    );
  },
  separatorBuilder: (_, __) {
    return const Divider(
      thickness: 1,
      height: 10,
    );
  }
),

The official Flutter documentation states that both ListView.builder() and ListView.separated() are very efficient builders when you have a fixed, long list of children to paint. They build children on demand because the builder is only called on visible widgets.

We could have achieved the same result by wrapping a Column in a scrollable widget, but it wouldn’t be as efficient as using lazy builders, as we did in the previous code block. For example, we don’t suggest that you do this with fixed-length lists:

SingleChildScrollView(
  child: Column(
    children: [
      for (item in itemsList)
        item,
    ],
  ),
)

The Column widget always renders all of its children, even if they’re out of the currently visible viewport. If the user doesn’t scroll the column, the widgets that aren’t in the viewport would still be rendered, even if they never appeared on the screen. This is why we suggest that you use list builders rather than columns when you have a long list of widgets to render.

Another point we want to touch on is using SVG and PNG files for images. We’ve been using both and we recommend that you do too because vectorial images are not always a good choice.

Vectorial images guarantee that you keep the quality high on scaling, and probably have a smaller file size than a PNG, but they may be very complicated to parse. PNGs may not scale very well but they’re quick to load and, when compressed, they can be really small. Here are some suggestions:

  • Always compress the SVG and PNG files you use to make sure they occupy the least possible amount of memory.
  • When you see that the SVG file is big and takes a few seconds to load, consider using a PNG image instead.
  • When you know that the image is going to scale a lot and the width/height ratio may now linearly change, consider using vectorial images instead.

In this project, we have used PNG images for country flags since they’re small, and we aren’t resizing them.

For our vectorial assets, we’ve used a popular and well-tested package called flutter_svg that makes managing vectorial assets very easy. For example, here’s how we load an SVG file in the project:

SvgPicture.asset(
  'assets/svg/trophy.svg',
  width: square / 1.8,
  height: square / 1.8,
  placeholderBuilder: (_) => const Center(
    child: CircularProgressIndicator(),
  ),
),

We can dynamically define its dimensions with width and height and also use placeholderBuilder to show a progress indicator in case the file vectorial was expensive to parse.

Now, let’s create the NextRaces widget.

The NextRaces widget

While showing the upcoming races of the championship is still part of the app, this isn’t its primary focus. The user can still check this data but it’s optional, so let’s create a new route to hide it on a separated page. So that we don’t have a static list with a few colors on it, we want to split the page into two main pieces:

  • At the top, we want to show how many races there are left in the championship. To make it visually attractive, we’ve used an image and a fancy circular progress indicator.
  • At the bottom, we have the list of upcoming races.

The page is simple, but it only shows data about the upcoming races and nothing more. We haven’t filled the UI with distracting background animations, low-contrast colors, or widgets that are too complex.

Tip

Always try to strive for a good balance between providing the necessary content and making the app as simple as possible. Having too many animations, images, or content on a page might be distracting. However, at the same time, a UI that is too minimal may not impress the user and give the feeling of a poorly designed app.

Here’s what the Next races UI is going to look like:

Figure 2.6 – The app’s Next races page

Figure 2.6 – The app’s Next races page

At the top, you can see a trophy surrounded by something similar to CircularProgressIndicator. Flutter doesn’t have a widget that allows us to achieve that exact result and nor do we have an easy way to build it. We may start with a Stack but then the rail and the progress bar may be difficult to render with common widgets.

In this case, we want to create a specific widget with particular constraints and shapes that’s not built in the Flutter framework. All of these hints lead us in a single direction: custom painters! Once again, we’re making the sizes responsive by dynamically calculating the width and height using the square variable:

LayoutBuilder(
  builder: (context, dimensions) {
    final square = min<double>(
      maxCircularProgress,
      dimensions.maxWidth,
    );
    return Center(
      child: CustomPaint(
        painter: const CircularProgressPainter(
          progression: 0.65,
        ),
        child: SizedBox(
          width: square,
          height: square,
          child: Center(
            child: SvgPicture.asset(
              'assets/svg/trophy.svg',
            ),
          ),
        ),
      ),
    );
  }
);

Thanks to CustomPaint, we can normally render a child and additionally paint some custom graphics in the background using the painter parameter. In the same way, we could have painted the same circular progress indicator in the foreground using foregroundPainter.

Custom painters aren’t the easiest thing to use but they give you a lot of power. You’re given a Canvas object where you can paint everything: lines, Bézier curves, shapes, images, and more. Here’s how we’ve created the painter for the circular progress indicator:

/// A circular progress indicator with a grey rail and a
/// blue line.
class CircularProgressPainter extends CustomPainter {
  /// The progression status.
  final double progression;
  /// Creates a [CircularProgressPainter] painter.
  const CircularProgressPainter({
    required this.progression,
  });
  @override
  void paint(Canvas canvas, Size size) {
    // painting the arcs...
  }
  @override
  bool shouldRepaint(covariant CircularProgressPainter old)
  {
    return progression != old.progression;
  }
}

We need to extend CustomPainter and override two very important methods:

  • shouldRepaint: This method tells the custom painter when it should repaint the contents. If you have no external dependencies, this method can safely just return false. In our case, if the progression changes, we need to also change the arc span, so we need to check whether progression != old.progression.
  • paint: This method provides a Canvas, along with its dimensions. It’s responsible for painting the content to the UI.

Here’s how we have implemented paint to draw the arcs:

// The background rail
final railPaint = Paint()
  ..color = Colors.grey.withAlpha(150)
  ..strokeCap = StrokeCap.round
  ..style = PaintingStyle.stroke
  ..strokeWidth = 8;
// The arc itself
final arcPaint = Paint()
  ..color = Colors.blue
  ..strokeCap = StrokeCap.round
  ..style = PaintingStyle.stroke
  ..strokeWidth = 8;
// Drawing the rail
final center = size.width / 2;
canvas.drawArc(
  Rect.fromCircle(
    center: Offset(center, center),
    radius: center,
  ),
  -pi / 2,
  pi * 2,
  false,
  railPaint,
);
// Drawing the arc
canvas.drawArc(
  Rect.fromCircle(
    center: Offset(center, center),
    radius: center,
  ),
  -pi / 2,
  pi * 2 * progression,
  false,
  arcPaint,
);

The Paint class defines the properties (thickness, color, border fill style, and more) of the lines or shapes we’re going to paint, while the Canvas class contains a series of methods for drawing various things on the UI, such as the following:

  • drawLine
  • drawCircle
  • drawImage
  • drawOval
  • drawRect
  • clipPath

And much more! Some mathematical skills are required here because we need to compute the arc length of the progress bar based on the progression percentage. The background track is just a full arc, so it’s easy to paint. On the other hand, the swipe of the progress bar needs to start from the top (-pi / 2) and be as wide as the percentage allows (pi * 2 * progression).

We’ve done it! The app now has two main pages: the first one shows rankings and standings, while the other one is about the upcoming races in the championship.

Summary

In this chapter, we learned how internationalization and localization work in Flutter and we also used some custom fonts from Google Fonts. Thanks to the intl package, we can, for example, format currencies and dates based on the device’s locale.

The race standings app is responsive because it dynamically rearranges the UI elements based on the viewport’s sizes. Thanks to breakpoints and the LayoutBuilder widget, we were able to easily handle the screen size changes.

The builder() and separated() constructors of ListViews are very efficient when it comes to painting a fixed series of widgets since they lazily load children.

We also used both PNG and SVG image assets. To render more complex widgets, such as the circular progress indicator, we used CustomPainter to go a bit more low level.

In the next chapter, we’re going to cover Flutter’s built-in state management solution: InheritedWidget. We will also use the popular provider package, which is a wrapper of InheritedWidget that’s easier to use and test.

Further reading

For more information about the topics that were covered in this chapter, take a look at the following resources:

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Discover state management solutions with InheritedWidget and the Provider package
  • Create responsive and beautiful UIs with the Material and Cupertino libraries
  • Explore animations, forms, gestures, and backend integration with Supabase

Description

Flutter is a UI toolkit for building beautiful, natively compiled applications for mobile, web, desktop, and embedded devices from a single code base. With Flutter, you can write your code once and run it anywhere using a single code base to target multiple platforms. This book is a comprehensive, project-based guide for new and emerging Flutter developers that will help empower you to build bulletproof applications. Once you start reading book, you’ll quickly realize what sets Flutter apart from its competition and establish some of the fundamentals of the toolkit. As you work on various project applications, you’ll understand just how easy Flutter is to use for building stunning UIs. This book covers navigation strategies, state management, advanced animation handling, and the two main UI design styles: Material and Cupertino. It’ll help you extend your knowledge with good code practices, UI testing strategies, and CI setup to constantly keep your repository’s quality at the highest level possible. By the end of this book, you'll feel confident in your ability to transfer the lessons from the example projects and build your own Flutter applications for any platform you wish.

Who is this book for?

This book is for software developers with a good grasp of Flutter, who want to learn best practices and techniques for building clean, intuitive UIs using a single codebase for mobile and the web. Prior experience with Flutter, Dart, and object-oriented programming (OOP) will help you understand the concepts covered in the book.

What you will learn

  • Create responsive and attractive UIs for any device
  • Get to grips with caching and widget trees and learn some framework performance tips
  • Manage state using Flutter's InheritedWidget system
  • Orchestrate the app flow with Navigator 1.0 and 2.0
  • Explore the Material and Cupertino built-in themes
  • Breathe life into your apps with animations
  • Improve code quality with golden tests, CI setup, and linter rules
Estimated delivery fee Deliver to Belgium

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Aug 26, 2022
Length: 260 pages
Edition : 1st
Language : English
ISBN-13 : 9781801810494
Vendor :
Google
Languages :
Concepts :
Tools :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Estimated delivery fee Deliver to Belgium

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Publication date : Aug 26, 2022
Length: 260 pages
Edition : 1st
Language : English
ISBN-13 : 9781801810494
Vendor :
Google
Languages :
Concepts :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 104.97
Flutter for Beginners
€44.99
Taking Flutter to the Web
€31.99
Cross-Platform UIs with Flutter
€27.99
Total 104.97 Stars icon
Banner background image

Table of Contents

11 Chapters
Building a Counter App with History Tracking to Establish Fundamentals Chevron down icon Chevron up icon
Building a Race Standings App Chevron down icon Chevron up icon
Building a Todo Application Using Inherited Widgets and Provider Chevron down icon Chevron up icon
Building a Native Settings Application Using Material and Cupertino Widgets Chevron down icon Chevron up icon
Exploring Navigation and Routing with a Hacker News Clone Chevron down icon Chevron up icon
Building a Simple Contact Application with Forms and Gesturess Chevron down icon Chevron up icon
Building an Animated Excuses Application Chevron down icon Chevron up icon
Build an Adaptive, Responsive Note-Taking Application with Flutter and Dart Frog Chevron down icon Chevron up icon
Writing Tests and Setting Up GitHub Actions Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Half star icon Empty star icon 3.3
(4 Ratings)
5 star 50%
4 star 0%
3 star 0%
2 star 25%
1 star 25%
HappyCoding Nov 29, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
If you didn't know why you used 'Constant' when you normally use it, I recommend this book. This book is a concept that ordinary Flutter developers tend to miss, but it explains very important concepts in an easy-to-understand way to users with various UIs examples. This book is sure to be essential reading for all Flutter developers. :]
Amazon Verified review Amazon
Josué E. Oct 18, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
An excellent book with a wide range of examples covering fundamental topics such as navigation, state manager, forms, responsive design and testing, which will surely elevate your skills to develop cross-platform applications at a professional level.
Amazon Verified review Amazon
Adam LaJeunesse Feb 19, 2024
Full star icon Full star icon Empty star icon Empty star icon Empty star icon 2
I have only gotten through chapter 1 at this point and I'm already finding this book is not well written. This book holds your hand and walks you through a project per chapter which seems great. Chapter 1 starts off good, however, as it progresses and introduces new ideas regarding state it seems like the topic isn't explained very well. Also, details are missing that are needed to get the first project working. I had to look at the book's GitHub code to see what I missed and what I did miss I could not find referenced in the book. I hope the 8 remaining chapters improve in quality.
Amazon Verified review Amazon
roboli Feb 02, 2024
Full star icon Empty star icon Empty star icon Empty star icon Empty star icon 1
Examples in book are out of sync with the github code. You end guessing which is what and what is where, awful! And don't be fooled, this book uses Flutter 2, not 3.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact [email protected] with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at [email protected] using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on [email protected] with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on [email protected] within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on [email protected] who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on [email protected] within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela