© Fu Cheng 2019
F. ChengFlutter Recipeshttps://doi.org/10.1007/978-1-4842-4982-6_13

13. Miscellaneous

Fu Cheng1 
(1)
Sandringham, Auckland, New Zealand
 

This chapter covers recipes of miscellaneous topics in Flutter.

13.1 Using Assets

Problem

You want to bundle static assets in the app.

Solution

Use assets.

Discussion

Flutter apps can include both code and static assets. There are two types of assets:
  • Data files including JSON, XML, and plain text files

  • Binary files including images and videos

Assets are declared in the flutter/assets section of the pubspec.yaml file. During the build process, these assets files are bundled into the app’s binary files. These assets can be accessed in the runtime. It’s common to put assets under the assets directory. In Listing 13-1, two files are declared as assets in pubspec.yaml file.
flutter:
  assets:
    - assets/dog.jpg
    - assets/data.json
Listing 13-1

Assets in pubspec.yaml file

In the runtime, subclasses of AssetBundle class are used to load content from the assets. The load() method retrieves the binary content, while loadString() method retrieves the string content. You need to provide the assets key when using these two methods. The key is the same as asset path declared in pubspec.yaml file. The static application-level rootBundle property refers to the AssetBundle object that contains assets packaged with the app. You can use this property directly to load assets. It’s recommended to use static DefaultAssetBundle.of() method to get the AssetBundle object from build context.

In Listing 13-2, the JSON file assets/data.json is loaded as string using loadString() method .
class TextAssets extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: DefaultAssetBundle.of(context)
          .loadString('assets/data.json')
          .then((json) {
        return jsonDecode(json)['name'];
      }),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          return Center(child: Text(snapshot.data));
        } else {
          return Center(child: CircularProgressIndicator());
        }
      },
    );
  }
}
Listing 13-2

Load string assets

If the assets file is an image, you can use AssetImage class with Image widget to display it. In Listing 13-3, AssetImage class is used to display the assets/dog.jpg image.
Image(
  image: AssetImage('assets/dog.jpg'),
)
Listing 13-3

Use AssetImage

For an image asset, it’s common to have multiple variants with different resolutions for the same file. When using AssetImage class to load an asset image, the variant that most closely matches the current device pixel ratio will be used.

In Listing 13-4, the assets/2.0x/dog.jpg file is the variant of assets/dog.jpg with resolution ratio 2.0. If the device pixel ratio is 1.6, the assets/2.0x/dog.jpg file is used.
flutter:
  assets:
    - assets/dog.jpg
    - assets/2.0x/dog.jpg
    - assets/3.0x/dog.jpg
Listing 13-4

Image assets variants

13.2 Using Gestures

Problem

You want to allow user using gestures to perform actions.

Solution

Use GestureDetector widget to detect gestures.

Discussion

Users of mobiles app are used to gestures when performing actions. For example, when viewing pictures gallery, using swiping gesture can easily navigate between different pictures. In Flutter, we can use GestureDetector widget to detect gestures and invoke specified callbacks for gestures. GestureDetector constructor has a large number of parameters to provide callbacks for different events. A gesture may dispatch multiple events during its lifecycle. For example, the gesture of horizontal drag can dispatch three events. The following are the handler parameters for these three events:
  • onHorizontalDragStart callback means the pointer may begin to move horizontally.

  • onHorizontalDragUpdate callback means the pointer is moving in the horizontal direction.

  • onHorizontalDragEnd callback means the pointer is longer in contact with the screen.

Callbacks of different events can receive details about the events. In Listing 13-5, the GestureDetector widget wraps a Container widget. In the onHorizontalDragEnd callback handler, the velocity property of DragEndDetails object is the moving velocity of the pointer. We use this property to determine the drag direction.
class SwipingCounter extends StatefulWidget {
  @override
  _SwipingCounterState createState() => _SwipingCounterState();
}
class _SwipingCounterState extends State<SwipingCounter> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('$_count'),
        Expanded(
          child: GestureDetector(
            child: Container(
              decoration: BoxDecoration(color: Colors.grey.shade200),
            ),
            onHorizontalDragEnd: (DragEndDetails details) {
              setState(() {
                double dx = details.velocity.pixelsPerSecond.dx;
                _count += (dx > 0 ? 1 : (dx < 0 ? -1 : 0));
              });
            },
          ),
        ),
      ] ,
    );
  }
}
Listing 13-5

Use GestureDetector

13.3 Supporting Multiple Locales

Problem

You want the app to support multiple locales.

Solution

Use Localizations widget and LocalizationsDelegate class .

Discussion

Flutter has built-in support for internalization. If you want to support multiple locales, you need to use Localizations widget. Localizations class uses a list of LocalizationsDelegate objects to load localized resources. LocalizationsDelegate<T> class is a factory of a set of localized resources of type T. The set of localized resources is usually a class with properties and methods to provide localized values.

To create a Localizations object, you need to provide the Locale object and a list of LocalizationsDelegate objects . Most of the time, you don’t need to explicitly create a Localizations object. WidgetsApp widget already creates a Localizations object. WidgetsApp constructor has parameters that are used by the Localizations object. When you need to use localized values, you can use static Localizations.of<T>(BuildContext context, Type type) method to get the nearest enclosing localized resources object of the given type.

By default, Flutter only provides US English localizations. To support other locales, you need to add Flutter’s own localizations for those locales first. This is done by adding flutter_localizations package to the dependencies of pubspec.yaml file; see Listing 13-6. With this package, you can use localized values defined in MaterialLocalizations class.
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
Listing 13-6

flutter_localizations

After adding the flutter_localizations package, we need to enable those localized values. In Listing 13-7, this is done by adding GlobalMaterialLocalizations.delegate and GlobalWidgetsLocalizations.delegate to the localizationsDelegates list of MaterialApp constructor. The value of localizationsDelegates parameter is passed to the Localizations constructor. The supportedLocales parameter specifies the supported locales.
MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: [
    const Locale('en'),
    const Locale('zh', 'CN'),
  ],
);
Listing 13-7

Enable Flutter localized values

In Listing 13-8, MaterialLocalizations.of() method gets the MaterialLocalizations object from the build context. The copyButtonLabel property is a localized value defined in MaterialLocalizations class. In the runtime, the label of the button depends on the device’s locale. MaterialLocalizations.of() method uses Localizations.of() internally to look up the MaterialLocalizations object.
RaisedButton(
  child: Text(MaterialLocalizations.of(context).copyButtonLabel),
  onPressed: () {},
);
Listing 13-8

Use localized values

MaterialLocalizations class only provides a limit set of localized values. For your own apps, you need to create custom localized resources classes. AppLocalizations class in Listing 13-9 is a custom localized resources class. AppLocalizations class has the appName property as an example of simple localizable strings. The greeting() method is an example of localizable strings that require parameters. AppLocalizationsEn and AppLocalizationsZhCn classes are implementations of AppLocalizations class for en and zh_CN locales, respectively.
abstract class AppLocalizations {
  String get appName;
  String greeting(String name);
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }
}
class AppLocalizationsEn extends AppLocalizations {
  @override
  String get appName => 'Demo App';
  @override
  String greeting(String name) {
    return 'Hello, $name';
  }
}
class AppLocalizationsZhCn extends AppLocalizations {
  @override
  String get appName => '示例应用';
  @override
  String greeting(String name) {
    return '你好, $name';
  }
}
Listing 13-9

AppLocalizations and localized subclasses

We also need to create a custom LocalizationsDelegate class to load AppLocalizations objects. There are three methods need to be implemented:
  • isSupported() method checks whether a locale is supported.

  • load() method loads the localized resources object for a given locale.

  • shouldReload() method checks whether the load() method should be called to load the resource again.

In the load() method of Listing 13-10, AppLocalizationsEn or AppLocalizationsZhCn object is returned based on the given locale.
class _AppLocalizationsDelegate
    extends LocalizationsDelegate<AppLocalizations> {
  const _AppLocalizationsDelegate();
  static const List<Locale> _supportedLocales = [
    const Locale('en'),
    const Locale('zh', 'CN')
  ];
  @override
  bool isSupported(Locale locale) {
    return _supportedLocales.contains(locale);
  }
  @override
  Future<AppLocalizations> load(Locale locale) {
    return Future.value(locale == Locale('zh', 'CN')
        ? AppLocalizationsZhCn()
        : AppLocalizationsEn());
  }
  @override
  bool shouldReload(LocalizationsDelegate<AppLocalizations> old) {
    return false;
  }
}
Listing 13-10

Custom LocalizationsDelegate

_AppLocalizationsDelegate object needs to be added to the list of localizationsDelegates in Listing 13-7. Listing 13-11 shows an example of using AppLocalizations class.
Text(AppLocalizations.of(context).greeting('John'))
Listing 13-11

Use AppLocalizations

13.4 Generating Translation Files

Problem

You want to extract localizable strings from code and integrate translated strings.

Solution

Use tools in intl_translation package .

Discussion

Recipe 13-3 describes how to support multiple locales using Localizations widget and LocalizationsDelegate class. The major drawback of solution in Recipe 13-3 is that you need to manually create localized resources classes for all supported locales. Because localized strings are directly embedded in source code, it’s hard to get translators involved. A better choice is to use tools provided by intl_translation package to automate the process. You need to add intl_translation: ^0.17.3 to the dev_dependencies of the pubspec.yaml file.

Listing 13-12 shows the new AppLocalizations class which has the same appName property and greeting() method as Listing 13-9. Intl.message() method describes a localized string. Only the message string is required. Parameters like name, desc, args, and examples are used to help translators to understand the message string.
class AppLocalizations {
  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations);
  }
  String get appName {
    return Intl.message(
      'Demo App',
      name: 'appName',
      desc: 'Name of the app',
    );
  }
  String greeting(String name) {
    return Intl.message(
      'Hello, $name',
      name: 'greeting',
      args: [name],
      desc: 'Greeting message',
      examples: const {'name': 'John'},
    );
  }
}
Listing 13-12

AppLocalizations using Intl.message()

Now we can use the tool provided by intl_translation package to extract localized messages from source code. The following command extracts messages declared with Intl.message() from lib/app_intl.dart file and saves to lib/l10n directory. After running this command, you should see the generated intl_messages.arb file in lib/l10n directory. Generated files are in ARB (Application Resource Bundle) format ( https://github.com/googlei18n/app-resource-bundle ) which can be used as input of translation tools like Google Translator Toolkit. ARB files are actually JSON files; you can simply use text editors to modify them.
$ flutter packages pub run intl_translation:extract_to_arb --locale=en --output-dir=lib/l10n lib/app_intl.dart
Now you can duplicate the intl_messages.arb file for each supported locale and get them translated. For example, the intl_messages_zh.arb file is the translated version for zh locale. After translated files are ready, you can use the following command to generate Dart files. After running this command, you should see a messages_all.dart file and messages_*.dart files for each locale.
$ flutter packages pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/app_intl.dart lib/l10n/intl_*.arb
The initializeMessages() function in messages_all.dart file can be used to initialize messages for a given locale. The static load() method in Listing 13-13 uses initializeMessages() function to initialize messages first, then sets the default locale.
class AppLocalizations {
  static Future<AppLocalizations> load(Locale locale) {
    final String name =
        locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);
    return initializeMessages(localeName).then((_) {
      Intl.defaultLocale = localeName;
      return AppLocalizations();
    });
  }
}
Listing 13-13

Load messages

This static AppLocalizations.load() method can be used by the load() method of LocalizationsDelegate class to load AppLocalizations object.

13.5 Painting Custom Elements

Problem

You want to paint custom elements.

Solution

Use CustomPaint widget with CustomPainter and Canvas classes.

Discussion

If you want to completely customize the painting of a widget, you can use CustomPaint widget . CustomPaint widget provides a canvas on which to draw custom elements. Table 13-1 shows the parameters of CustomPaint constructor. During the painting process, the painter paints on the canvas first, then the child widget is painted, and finally the foregroundPainter paints on the canvas.
Table 13-1

Parameters of CustomPaint

Name

Type

Description

painter

CustomPainter

The painter that paints before the child.

foregroundPainter

CustomPainter

The painter that paints after the child.

size

Size

The size to paint.

child

Widget

The child widget.

To create CustomPainter objects, you need to create subclasses of CustomPainter and override paint() and shouldRepaint() methods. In paint() method, the canvas parameter can be used to draw custom elements. Canvas class has a set of methods to draw different elements; see Table 13-2.
Table 13-2

Methods of Canvas

Name

Description

drawArc()

Draw an arc.

drawCircle()

Draw a circle with specified center and radius.

drawImage()

Draw an Image object.

drawLine()

Draw a line between two points.

drawOval()

Draw an oval.

drawParagraph()

Draw text.

drawRect()

Draw a rectangle with specified Rect object.

drawRRect()

Draw a rounded rectangle.

Most of the methods in Canvas class have a parameter of type Paint to describe the style to use when drawing on the canvas. In Listing 13-14, Shapes class draws a rectangle and a circle on the canvas. In the CustomShapes widget, the Text widget is painted above the Shapes painter.
class CustomShapes extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 300,
      height: 300,
      child: CustomPaint(
        painter: Shapes(),
        child: Center(child: Text('Hello World')),
      ),
    );
  }
}
class Shapes extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Rect rect = Offset(5, 5) & (size - Offset(5, 5));
    canvas.drawRect(
      rect,
      Paint()
        ..color = Colors.red
        ..strokeWidth = 2
        ..style = PaintingStyle.stroke,
    );
    canvas.drawCircle(
      rect.center,
      (rect.shortestSide / 2) - 10,
      Paint()..color = Colors.blue,
    );
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}
Listing 13-14

Use CustomPaint

13.6 Customizing Themes

Problem

You want to customize themes in Flutter apps.

Solution

Use ThemeData class for Material Design and CupertinoThemeData class for iOS.

Discussion

It’s a common requirement to customize look and feel of an app. For Flutter apps, if Material Design is used, you can use ThemeData class to customize the theme. ThemeData class has a large number of parameters to configure different aspects of the theme. MaterialApp class has the theme parameter to provide the ThemeData object . For iOS style, CupertinoThemeData class has the same purpose to specify the theme. CupertinoApp class also has the theme parameter of type CupertinoThemeData to customize the theme.

If you need to access the current theme object, you can use static Theme.of() method to get nearest enclosing ThemeData object for a build context in Material Design. The similar CupertinoTheme.of() method can be used for iOS style.

In Listing 13-15, the first Text widget uses the textTheme.headline property of current Theme object as the style. The second Text widget uses the colorScheme.error property as the color to display error text.
class TextTheme extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('Headline', style: Theme.of(context).textTheme.headline),
        Text('Error',
            style: TextStyle(color: Theme.of(context).colorScheme.error)),
      ],
    );
  }
}
Listing 13-15

Use Theme

13.7 Summary

This chapter discusses miscellaneous topics in Flutter that are useful in different scenarios. In the next chapter, we’ll discuss testing and debugging in Flutter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.153.38