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

9. Service Interaction

Fu Cheng1 
(1)
Sandringham, Auckland, New Zealand
 

Many non-trivial mobile apps require interaction with backend services. This chapter covers essential concepts related to service interactions in Flutter.

9.1 Working with Futures

Problem

You want to work with Future objects .

Solution

Use then() and catchError() methods to handle results of Future objects.

Discussion

When using code from Flutter and Dart libraries, you may encounter functions that return Future objects. Future<T> class from dart:async library is a representation of delayed computations. A Future object represents a potential value or error that will be available in the future. When given a Future object, you can register callbacks to handle the value or error once it is available. Future class is one of the basic building blocks of asynchronous programming in Dart.

Given a Future object , there are three different cases regarding its result:
  • The computation never completes. No callbacks will be invoked.

  • The computation completes with a value. Value callbacks are invoked with the value.

  • The computation completes with an error. Error callbacks are invoked with the error.

To register callbacks to a Future object, you can use then() method to register a value callback and an optional error callback or use catchError() method to register an error callback only. It’s recommended to use then() method to only register a value callback. This is because if an error callback is registered using onError parameter of then() method, this error callback cannot handle the error thrown in the value callback. Most of the time, you want the error callback to handle all possible errors . If an error of a Future object is not handled by its error callbacks, this error will be handled by the global handler.

In Listing 9-1, the Future object may complete with value 1 or an Error object. Both value and error callbacks are registered to handle the result.
Future.delayed(
  Duration(seconds: 1),
  () {
    if (Random().nextBool()) {
      return 1;
    } else {
      throw Error();
    }
  },
).then((value) {
  print(value);
}).catchError((error) {
  print('error: $error');
});
Listing 9-1

Use then() and catchError() methods to handle result

Return values of then() and catchError() methods are also Future objects . Given a Future object A, the result of invoking A.then(func) is another Future object B. If the func callback runs successfully, the Future B will complete with the return value of invoking func function. Otherwise, Future B will complete with the error thrown when invoking func function. Invoking B.catchError(errorHandler) returns a new Future object C. The error handler can handle errors thrown in Future B, which may be thrown in Future A itself or in its value handler . By using then() and catchError() methods, Future objects form a chain of handling asynchronous computations.

In Listing 9-2, multiple then() methods are chained together to process the result in sequence .
Future.value(1)
  .then((value) => value + 1)
  .then((value) => value * 10)
  .then((value) => value + 2)
  .then((value) => print(value));
Listing 9-2

Chained then() methods

If you want to call functions when a future completes, you can use whenComplete() method. Functions added using whenComplete() are called when this future completes, no matter it completes with a value or an error. The whenComplete() method is equivalent of a finally block in other programming languages . The chain of then().catchError().whenComplete() is equivalent of “try-catch-finally”.

Listing 9-3 shows an example of using whenComplete() method.
Future.value(1).then((value) {
  print(value);
}).whenComplete(() {
  print('complete');
});
Listing 9-3

Using whenComplete()

It’s possible for the computation of Future object to take a long time to complete. You can use timeout() method to set the time limit on the computation. When invoking timeout() method, you need to provide a Duration object as the time limit and an optional onTimeout function to provide the value when a timeout happens. The return value of timeout() method is a new Future object. If the current Future object doesn’t complete before the time limit, the result of calling onTimeout function is the result of the new Future object. If no onTimeout function is provided, the new Future object will complete with a TimeoutException when current future is timed out.

In Listing 9-4, the Future object will complete in 5 seconds with value 1, but the time limit is set to 2 seconds. The value 10 returned by onTimeout function will be used instead.
Future.delayed(Duration(seconds: 5), () => 1)
  .timeout(
    Duration(seconds: 2),
    onTimeout: () => 10,
  )
  .then((value) => print(value));
Listing 9-4

Use timeout() method

9.2 Using async and await to Work with Futures

Problem

You want to work with Future objects like they are synchronous.

Solution

Use async and await .

Discussion

Future objects represent asynchronous computations. The usual way to work with Future objects is registering callbacks to handle results. This callback-based style may create a barrier for developers that are used to synchronous operations. Using async and await is a syntax sugar in Dart to make working with Future objects like normal synchronous operations.

Given a Future object, await can wait for its completion and return its value. The code after the await can use the returned value directly, just like it is the result of a synchronous call. When await is used, its enclosing function must be marked as async. This means the function returns a Future object.

In Listing 9-5, the return value of getValue() function is a Future object. In calculate() function , await is used to get the return value of getValue() function and assign to value variable. Since await is used, calculate() function is marked as async.
Future<int> getValue() {
  return Future.value(1);
}
Future<int> calculate() async {
  int value = await getValue();
  return value * 10;
}
Listing 9-5

Use async/await

When await is used to handle Future objects, you can use try-catch-finally to handle errors thrown in Future objects. This allows Future objects to be used just like normal synchronous operations. Listing 9-6 shows an example of using try-catch-finally and await/async together.
Future<int> getErrorValue() {
  return Future.error('invalid value');
}
Future<int> calculateWithError() async {
  try {
    return await getErrorValue();
  } catch (e) {
    print(e);
    return 1;
  } finally {
    print('done');
  }
}
Listing 9-6

Use try-catch-finally and await/async

9.3 Creating Futures

Problem

You want to create Future objects .

Solution

Use Future constructors Future(), Future.delayed(), Future.sync(), Future.value(), and Future.error() to create Future objects.

Discussion

If you need to create Future objects , you can use its constructors, Future(), Future.delayed(), Future.sync(), Future.value(), and Future.error():
  • Future() constructor creates a Future object that runs the computation asynchronously.

  • Future.delayed() constructor creates a Future object that runs the computation after a delay specified using a Duration object.

  • Future.sync() constructor creates a Future object that runs the computation immediately.

  • Future.value() constructor creates a Future object that completes with the given value.

  • Future.error() constructor creates a Future object that completes with the given error and optional stack trace.

Listing 9-7 shows examples of using different Future constructors.
Future(() => 1).then(print);
Future.delayed(Duration(seconds: 3), () => 1).then(print);
Future.sync(() => 1).then(print);
Future.value(1).then(print);
Future.error(Error()).catchError(print);
Listing 9-7

Create Future objects

9.4 Working with Streams

Problem

You want to work with a stream of events.

Solution

Use Stream<T> class and its subclasses.

Discussion

With Future class, we can represent a single value which may be available in the future. However, we may also need to work with a sequence of events. Stream<T> class in dart:async library represents a source of asynchronous events. To help with this, the Future class has asStream() method to create a Stream containing the result of the current Future object.

If you have experiences with Reactive Streams ( www.reactive-streams.org/ ), you may find Stream in Dart is a similar concept. There can be three types of events in a stream:
  • Data event represents actual data in the stream. These events are also called elements in the stream.

  • Error event represents errors occurred.

  • Done event represents that the end of stream has reached. No more events will be emitted.

To receive events from a stream, you can use the listen() method to set up listeners. The return value of listen() method is a StreamSubscription object representing the active subscription. Depending on the number of subscriptions allowed on the stream, there are two types of streams:
  • A single-subscription stream allows only a single listener during the whole lifecycle of the stream. It only starts emitting events when a listener is set up, and it stops emitting events when the listener unsubscribes.

  • A broadcast stream allows any number of listeners. Events are emitted when they are ready, even though there are no subscribed listener.

Given a Stream object, the property isBroadcast can be used to check whether it is a broadcast stream. You can use the asBroadcastStream() method to create a broadcast stream from a single-subscription stream.

Stream Subscription

Table 9-1 shows parameters of listen() method . You can provide any number of handlers for different events and ignore those uninterested events.
Table 9-1

Parameters of listen() method

Name

Type

Description

onData

void (T event)

Handler of data events.

onError

Function

Handler of error events.

onDone

void ()

Handler of done event.

cancelOnError

bool

Whether to cancel the subscription when the first error event is emitted.

In Listing 9-8, handlers for three types of events are provided.
Stream.fromIterable([1, 2, 3]).listen(
  (value) => print(value),
  onError: (error) => print('error: $error'),
  onDone: () => print('done'),
  cancelOnError: true,
);
Listing 9-8

Use listen() method

With the StreamSubscription object returned by listen() method, you can manage the subscription. Table 9-2 show methods of StreamSubscription class.
Table 9-2

Methods of StreamSubscription

Name

Description

cancel()

Cancels this subscription.

pause([Future resumeSignal])

Requests the stream to pause events emitting. If resumeSignal is provided, the stream will resume when the future completes.

resume()

Resumes the stream after a pause.

onData()

Replaces the data event handler.

onError()

Replaces the error event handler.

onDone()

Replaces the done event handler.

asFuture([E futureValue])

Returns a future that handles the completion of stream.

The asFuture() method is useful when you want to handle the completion of a stream. Since a stream can complete normally or with an error, using this method overwrites existing onDone and onError callbacks. In the case of an error event, the subscription is cancelled, and the returned Future object is completed with the error. In the case of a done event, the Future object completes with the given futureValue.

Stream Transformation

The power of stream is to apply various transformations on the stream to get another stream or a value. Table 9-3 shows methods in Stream class that return another Stream object.
Table 9-3

Stream transformations

Name

Description

asyncExpand<E>(Stream<E> convert(T event))

Transforms each element into a stream and concatenates elements in these streams as the new stream.

asyncMap<E>(FutureOr<E> convert(T event))

Transforms each element into a new event.

distinct([bool equals(T previous, T next) ])

Skips duplicate elements.

expand<S>(Iterable<S> convert(T element))

Transforms each element into a sequence of elements.

handleError(Function onError, { bool test(dynamic error) })

Handles errors in the stream.

map<S>(S convert(T event))

Transforms each element into a new event.

skip(int count)

Skips elements in the stream.

skipWhile(bool test(T element))

Skips elements while they match the predicate.

take(int count)

Takes only the first count elements from the stream.

takeWhile(bool test(T element))

Takes elements while they match the predicate.

timeout(Duration timeLimit, { void onTimeout(EventSink<T> sink) })

Handles error when the time between two events exceeds the time limit.

transform<S>(StreamTransformer<T, S> streamTransformer)

Transforms the stream.

where(bool test(T event))

Filters elements in the stream.

Listing 9-9 shows examples of using stream transformations. Code below each statement shows the result of the execution.
Stream.fromIterable([1, 2, 3]).asyncExpand((int value) {
  return Stream.fromIterable([value * 5, value * 10]);
}).listen(print);
// -> 5, 10, 10, 20, 15, 30
Stream.fromIterable([1, 2, 3]).expand((int value) {
  return [value * 5, value * 10];
}).listen(print);
// -> 5, 10, 10, 20, 15, 30
Stream.fromIterable([1, 2, 3]).asyncMap((int value) {
  return Future.delayed(Duration(seconds: 1), () => value * 10);
}).listen(print);
// -> 10, 20, 30
Stream.fromIterable([1, 2, 3]).map((value) => value * 10).listen(print);
// -> 10, 20, 30
Stream.fromIterable([1, 1, 2]).distinct().listen(print);
// -> 1, 2
Stream.fromIterable([1, 2, 3]).skip(1).listen(print);
// -> 2, 3
Stream.fromIterable([1, 2, 3])
    .skipWhile((value) => value % 2 == 1)
    .listen(print);
// -> 2, 3
Stream.fromIterable([1, 2, 3]).take(1).listen(print);
// -> 1
Stream.fromIterable([1, 2, 3])
    .takeWhile((value) => value % 2 == 1)
    .listen(print);
// -> 1
Stream.fromIterable([1, 2, 3]).where((value) => value % 2 == 1).listen(print);
// -> 1, 3
Listing 9-9

Stream transformations

There are other methods in Stream class that return a Future object; see Table 9-4. These operations return a single value instead of a stream.
Table 9-4

Methods for single values

Name

Description

any(bool test(T element))

Checks whether any element in the stream matches the predicate.

every(bool test(T element))

Checks whether all elements in the stream match the predicate.

contains(Object needle)

Checks whether the stream contains the given element.

drain<E>([E futureValue ])

Discards all elements in the stream.

elementAt(int index)

Gets the element at the given index.

firstWhere(bool test(T element), { T orElse() })

Finds the first element matching the predicate.

lastWhere(bool test(T element), { T orElse() })

Finds the last element matching the predicate.

singleWhere(bool test(T element), { T orElse() })

Finds the single element matching the predicate.

fold<S>(S initialValue, S combine(S previous, T element))

Combines elements in the stream into a single value.

forEach(void action(T element))

Runs an action on each element of the stream.

join([String separator = "" ])

Combines the elements into a single string.

pipe(StreamConsumer<T> streamConsumer)

Pipes the events into a StreamConsumer.

reduce(T combine(T previous, T element))

Combines elements in the stream into a single value.

toList()

Collects the elements into a list.

toSet()

Collects the elements into a set.

Listing 9-10 shows examples of using methods in Table 9-4. Code below each statement shows the result of the execution.
Stream.fromIterable([1, 2, 3]).forEach(print);
// -> 1, 2, 3
Stream.fromIterable([1, 2, 3]).contains(1).then(print);
// -> true
Stream.fromIterable([1, 2, 3]).any((value) => value % 2 == 0).then(print);
// -> true
Stream.fromIterable([1, 2, 3]).every((value) => value % 2 == 0).then(print);
// -> false
Stream.fromIterable([1, 2, 3]).fold(0, (v1, v2) => v1 + v2).then(print);
// -> 6
Stream.fromIterable([1, 2, 3]).reduce((v1, v2) => v1 * v2).then(print);
// -> 6
Stream.fromIterable([1, 2, 3])
    .firstWhere((value) => value % 2 == 1)
    .then(print);
// -> 1
Stream.fromIterable([1, 2, 3])
    .lastWhere((value) => value % 2 == 1)
    .then(print);
// -> 3
Stream.fromIterable([1, 2, 3])
    .singleWhere((value) => value % 2 == 1)
    .then(print);
// -> Unhandled exception: Bad state: Too many elements
Listing 9-10

Methods return Future objects

9.5 Creating Streams

Problem

You want to create Stream objects .

Solution

Use different Stream constructors.

Discussion

There are different Stream constructors to create Stream objects:
  • Stream.empty() constructor creates an empty broadcast stream.

  • Stream.fromFuture() constructor creates a single-subscription stream from a Future object.

  • Stream.fromFutures() constructor creates a stream from a list of Future objects.

  • Stream.fromInterable() constructor creates a single-subscription stream from elements of an Iterable object.

  • Stream.periodic() constructor creates a stream that periodically emits data events at the given intervals.

Listing 9-11 shows examples of different Stream constructors .
Stream.fromIterable([1, 2, 3]).listen(print);
Stream.fromFuture(Future.value(1)).listen(print);
Stream.fromFutures([Future.value(1), Future.error('error'), Future.value(2)])
    .listen(print);
Stream.periodic(Duration(seconds: 1), (int count) => count * 2)
    .take(5)
    .listen(print);
Listing 9-11

Use Stream constructors

Another way to create streams is using StreamController class. A StreamController object can send different events to the stream it controls. The default StreamController() constructor creates a single-subscription stream, while StreamController.broadcast() constructor creates a broadcast stream. With StreamController, you can generate elements in stream programmatically.

In Listing 9-12, different events are sent to the stream controlled by the StreamController object.
StreamController<int> controller = StreamController();
controller.add(1);
controller.add(2);
controller.stream.listen(print, onError: print, onDone: () => print('done'));
controller.addError('error');
controller.add(3);
controller.close();
Listing 9-12

Use StreamController

9.6 Building Widgets Based on Streams and Futures

Problem

You want to build a widget that updates its content based on the data in a stream or a future.

Solution

Use StreamBuilder<T> or FutureBuilder<T> widget.

Discussion

Given a Steam or Future object, you may want to build a widget that updates its content based on the data in it. You can use StreamBuilder<T> widget to work with Stream objects and FutureBuilder<T> widget to work with Future objects. Table 9-5 shows parameters of StreamBuilder<T> constructor.
Table 9-5

Parameters of StreamBuilder<T>

Name

Type

Description

stream

Stream<T>

The stream for the builder.

builder

AsyncWidgetBuilder<T>

Builder function for the widget.

initialData

T

Initial data to build the widget.

AsyncWidgetBuilder is a typedef of function type Widget (BuildContext context, AsyncSnapshot<T> snapshot). AsyncSnapshot class represents the snapshot of interaction with an asynchronous computation. Table 9-6 shows properties of AsyncSnapshot<T> class.
Table 9-6

Properties of AsyncSnapshot<T>

Name

Type

Description

connectionState

ConnectionState

State of connection to the asynchronous computation.

data

T

The latest data received by the asynchronous computation.

error

Object

The latest error object received by the asynchronous computation.

hasData

bool

Whether data property is not null.

hasError

bool

Whether error property is not null.

You can determine the connection state using the value of connectionState. Table 9-7 shows values of ConnectionState enum.
Table 9-7

Values of ConnectionState

Name

Description

none

Not connected to the asynchronous computation.

waiting

Connected to the asynchronous computation and waiting for interaction.

active

Connected to an active asynchronous computation.

done

Connected to a terminated asynchronous computation.

When using StreamBuilder widget to build the UI, the typical way is to return different widgets according to the connection state. For example, if the connection state is waiting, then a process indicator may be returned.

In Listing 9-13, the stream has five elements that are generated every second. If the connection state is none or waiting, a CircularProgressIndicator widget is returned. If the state is active or done, a Text widget is returned according to the value of data and error properties.
class StreamBuilderPage extends StatelessWidget {
  final Stream<int> _stream =
      Stream.periodic(Duration(seconds: 1), (int value) => value * 10).take(5);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stream Builder'),
      ),
      body: Center(
        child: StreamBuilder(
          stream: _stream,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            switch (snapshot.connectionState) {
              case ConnectionState.none:
              case ConnectionState.waiting:
                return CircularProgressIndicator();
              case ConnectionState.active:
              case ConnectionState.done:
                if (snapshot.hasData) {
                  return Text('${snapshot.data ?? "}');
                } else if (snapshot.hasError) {
                  return Text(
                    '${snapshot.error}',
                    style: TextStyle(color: Colors.red),
                  );
                }
            }
            return null;
          },
        ),
      ),
    );
  }
}
Listing 9-13

Use StreamBuilder

The usage of FutureBuilder widget is similar with StreamBuilder widget. When using a FutureBuilder with a Future object, you can convert the Future object to a Stream object using asStream() method first, then use StreamBuilder with the converted Stream object .

In Listing 9-14, we use a different way to build the UI. Instead of checking the connection state, hasData and hasError properties are used to check the status.
class FutureBuilderPage extends StatelessWidget {
  final Future<int> _future = Future.delayed(Duration(seconds: 1), () {
    if (Random().nextBool()) {
      return 1;
    } else {
      throw 'invalid value';
    }
  });
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Future Builder'),
      ),
      body: Center(
        child: FutureBuilder(
          future: _future,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            if (snapshot.hasData) {
              return Text('${snapshot.data}');
            } else if (snapshot.hasError) {
              return Text(
                '${snapshot.error}',
                style: TextStyle(color: Colors.red),
              );
            } else {
              return CircularProgressIndicator();
            }
          },
        ),
      ),
    );
  }
}
Listing 9-14

Use FutureBuilder

9.7 Handle Simple JSON Data

Problem

You want to have a simple way to handle JSON data.

Solution

Use jsonEncode() and jsonDecode() functions from dart:convert library.

Discussion

JSON is a popular data format for web services. To interact with backend services, you may need to handle JSON data in two scenarios:
  • JSON data serialization converts objects in Dart to JSON strings.

  • JSON data deserialization converts JSON strings to objects in Dart.

For both scenarios, if you only need to handle simple JSON data occasionally, then using jsonEncode() and jsonDecode() functions from dart:convert library is a good choice. The jsonEncode() function converts Dart objects to strings, while jsonDecode() function converts strings to Dart objects. In Listing 9-15, data object is serialized to JSON string first, then the JSON string is deserialized to Dart object again.
var data = {
  'name': 'Test',
  'count': 100,
  'valid': true,
  'list': [
    1,
    2,
    {
      'nested': 'a',
      'value': 123,
    },
  ],
};
String str = jsonEncode(data);
print(str);
Object obj = jsonDecode(str);
print(obj);
Listing 9-15

Handle JSON data

The JSON encoder in dart:convert library only supports a limited number of data types, including numbers, strings, booleans, null, lists, and maps with string keys. To encode other types of objects, you need to use the toEncodable parameter to provide a function which converts the object to an encodable value first. The default toEncodable function calls toJson() method on the object. It’s a common practice to add toJson() method to custom classes that need to be serialized as JSON strings.

In Listing 9-16, toJson() method of ToEncode class returns a list which will be used as the input of JSON serialization.
class ToEncode {
  ToEncode(this.v1, this.v2);
  final String v1;
  final String v2;
  Object toJson() {
    return [v1, v2];
  }
}
print(jsonEncode(ToEncode('v1', 'v2')));
Listing 9-16

Use toJson() function

If you want to have indent in the serialized JSON strings, you need to use JsonEncoder class directly. In Listing 9-17, two spaces are used as the indent.
String indentString = JsonEncoder.withIndent('  ').convert(data);
print(indentString);
Listing 9-17

Add indent

9.8 Handle Complex JSON Data

Problem

You want to have a type-safe way to handle JSON data.

Solution

Use json_annotation and json_serializable libraries.

Discussion

Using jsonEncode() and jsonDecode() functions from dart:convert library can easily work with simple JSON data. When the JSON data has a complicated structure, using these two functions is not quite convenient. When deserializing JSON strings, the results are usually lists or maps. If the JSON data has a nested structure, it’s not easy to extract the values from lists or maps. When serializing objects, you need to add toJson() methods to these classes to build the lists or maps. These tasks can be simplified using code generation with json_annotation and json_serializable libraries.

The json_annotation library provides annotations to customize JSON serialization and deserialization behavior. The json_serializable library provides the build process to generate code that handles JSON data. To use these two libraries, you need to add them into pubspec.yaml file. In Listing 9-18, json_serializable library is added to dependencies, while json_serializable library is added to dev_dependencies.
dependencies:
  json_annotation: ^2.0.0
dev_dependencies:
  build_runner: ^1.0.0
  json_serializable: ^2.0.0
Listing 9-18

Add json_annotation and json_serializable

In Listing 9-19, Person class is in the json_serialize.dart file. The annotation @JsonSerializable() means generating code for Person class. The generated code is in the json_serialize.g.dart file . Functions _$PersonFromJson() and _$PersonToJson() used in Listing 9-19 come from the generated file. The _$PersonFromJson() function is used in the Person.fromJson() constructor, while _$PersonToJson() function is used in the toJson() method.
import 'package:json_annotation/json_annotation.dart';
part 'json_serialize.g.dart';
@JsonSerializable()
class Person {
  Person({this.firstName, this.lastName, this.email});
  final String firstName;
  final String lastName;
  final String email;
  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
  Map<String, dynamic> toJson() => _$PersonToJson(this);
}
Listing 9-19

Use json_serializable

To generate the code, you need to run flutter packages pub run build_runner build command. Listing 9-20 shows the generated file.
part of 'json_serialize.dart';
Person _$PersonFromJson(Map<String, dynamic> json) {
  return Person(
      firstName: json['firstName'] as String,
      lastName: json['lastName'] as String,
      email: json['email'] as String);
}
Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'firstName': instance.firstName,
      'lastName': instance.lastName,
      'email': instance.email
    };
Listing 9-20

Generated code to handle JSON data

JsonSerializable annotation has different properties to customize the behavior; see Table 9-8.
Table 9-8

Properties of JsonSerializable

Name

Default value

Description

anyMap

false

When true, use Map as the map type; otherwise, Map<String, dynamic> is used.

checked

false

Whether to add extra checks to validate data types.

createFactory

true

Whether to generate the function that converts maps to objects.

createToJson

true

Whether to generate the function that can be used as toJson() function.

disallowUnrecognizedKeys

false

When true, unrecognized keys are treated as an error; otherwise, they are ignored.

explicitToJson

false

When true, generated toJson() function uses toJson on nested objects.

fieldRename

FieldRename.none

Strategy to convert names of class fields to JSON map keys.

generateToJsonFunction

true

When true, generate top-level function; otherwise, generate a mixin class with toJson() function.

includeIfNull

true

Whether to include fields with null values.

nullable

true

Whether to handle null values gracefully.

useWrappers

false

Whether to use wrapper classes to minimize the usage of Map and List instances during serialization.

The generateToJsonFunction property determines how toJson() functions are generated. When the value is true, top-level functions like _$PersonToJson() in Listing 9-20 will be generated. In Listing 9-21, generateToJsonFunction property is set to false for User class.
@JsonSerializable(
  generateToJsonFunction: false,
)
class User extends Object with _$UserSerializerMixin {
  User(this.name);
  final String name;
}
Listing 9-21

User class

In Listing 9-22, instead of a function, the _$UserSerializerMixin class is generated with toJson() method. User class in Listing 9-21 only needs to use this mixin class.
User _$UserFromJson(Map<String, dynamic> json) {
  return User(json['name'] as String);
}
abstract class _$UserSerializerMixin {
  String get name;
  Map<String, dynamic> toJson() => <String, dynamic>{'name': name};
}
Listing 9-22

Generated code for User class

JsonKey annotation specifies how a field is serialized. Table 9-9 shows properties of JsonKey.
Table 9-9

Properties of JsonKey

Name

Description

name

JSON map key. If null, the field name is used.

nullable

Whether to handle null values gracefully.

includeIfNull

Whether to include this field if the value is null.

ignore

Whether to ignore this field.

fromJson

A function to deserialize this field.

toJson

A function to serialize this field.

defaultValue

The value to use as the default value.

required

Whether this field is required in the JSON map.

disallowNullValue

Whether to disallow null values.

Listing 9-23 shows an example of using JsonKey.
@JsonKey(
  name: 'first_name',
  required: true,
  includeIfNull: true,
)
final String firstName;
Listing 9-23

Use JsonKey

JsonValue annotation specifies the enum value used for serialization. In Listing 9-24, JsonValue annotation is added to all enum values of Color.
enum Color {
  @JsonValue('R')
  Red,
  @JsonValue('G')
  Green,
  @JsonValue('B')
  Blue
}
Listing 9-24

Use JsonValue

JsonLiteral annotation reads JSON data from a file and converts the content into an object. It allows easy access to content of static JSON data files. In Listing 9-25, JsonLiteral annotation is added to the data getter. _$dataJsonLiteral is the generated variable of the data in the JSON file.
@JsonLiteral('data.json', asConst: true)
Map get data => _$dataJsonLiteral;
Listing 9-25

Use JsonLiteral

9.9 Handling XML Data

Problem

You want to handle XML data in Flutter apps.

Solution

Use xml library.

Discussion

XML is a popular data exchange format. You can use xml library to handle XML data in Flutter apps. You need to add xml: ^3.3.1 to dependencies of pubspec.yaml file first. Similar with JSON data, there are two usage scenarios of XML data:
  • Parse XML documents and query data.

  • Build XML documents.

Parse XML Documents

To parse XML documents, you need to use parse() function which takes a XML string as the input and returns parsed XmlDocument object. With the XmlDocument object, you can query and traverse the XML document tree to extract data from it.

To query the document tree, you can use findElements() and findAllElements() methods. These two methods accept a tag name and an optional namespace as the parameters and return an Iterable<XmlElement> object. The difference is that findElements() method only searches direct children, while findAllElements() method searches all descendant children. To traverse the document tree, you can use properties shown in Table 9-10.
Table 9-10

Properties of XmlParent

Name

Type

Description

children

XmlNodeList<XmlNode>

Direct children of this node.

ancestors

Iterable<XmlNode>

Ancestors of this node in reverse document order.

descendants

Iterable<XmlNode>

Descendants of this node in document order.

attributes

List<XmlAttribute>

Attribute nodes of this node in document order.

preceding

Iterable<XmlNode>

Nodes preceding the opening tag of this node in document order.

following

Iterable<XmlNode>

Nodes following the closing tag of this node in document order.

parent

XmlNode

Parent of this node, can be null.

firstChild

XmlNode

First child of this node, can be null.

lastChild

XmlNode

Last child of this node, can be null.

nextSibling

XmlNode

Next sibling of this node, can be null.

previousSibling

XmlNode

Previous sibling of this node, can be null.

root

XmlNode

Root of the tree.

In Listing 9-26, the input XML string (excerpt from https://msdn.microsoft.com/en-us/windows/desktop/ms762271 ) is parsed and queried for the first book element. Then text of the title element and value of the id attribute are extracted.
String xmlStr = "'
  <?xml version="1.0"?>
  <catalog>
    <book id="bk101">
      <Author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications
        with XML.</description>
    </book>
    <book id="bk102">
      <Author>Ralls, Kim</author>
      <title>Midnight Rain</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-12-16</publish_date>
      <description>A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.</description>
    </book>
  </catalog>
"';
XmlDocument document = parse(xmlStr);
XmlElement firstBook = document.rootElement.findElements('book').first;
String title = firstBook.findElements('title').single.text;
String id = firstBook.attributes
    .firstWhere((XmlAttribute attr) => attr.name.local == 'id')
    .value;
print('$id => $title');
Listing 9-26

XML document parsing and querying

Build XML Documents

To build XML documents, you can use XmlBuilder class. XmlBuilder class provides methods to build different components of XML documents; see Table 9-11. With these methods, we can build XML documents in a top-down fashion, which starts from the root element and build nested content layer by layer.
Table 9-11

Methods of XmlBuilder

Name

Description

element()

Creates a XmlElement node with specified tag name, namespaces, attributes, and nested content.

attribute()

Creates a XmlAttribute node with specified name, value, namespace, and type.

text()

Creates a XmlText node with specified text.

namespace()

Binds namespace prefix to the uri.

cdata()

Creates a XmlCDATA node with specified text.

comment()

Creates a XmlComment node with specified text.

processing()

Creates a XmlProcessing node with specified target and text .

After finishing the building, the build() method of XmlBuilder can be used to build the XmlNode as the result. In Listing 9-27, the root element is a note element with id attribute. Value of nest parameter is a function which uses builder methods to build the content of the node element.
XmlBuilder builder = XmlBuilder();
builder.processing('xml', 'version="1.0"');
builder.element(
  'note',
  attributes: {
    'id': '001',
  },
  nest: () {
    builder.element('from', nest: () {
      builder.text('John');
    });
    builder.element('to', nest: () {
      builder.text('Jane');
    });
    builder.element('message', nest: () {
      builder
        ..text('Hello!')
        ..comment('message to send');
    });
  },
);
XmlNode xmlNode = builder.build();
print(xmlNode.toXmlString(pretty: true));
Listing 9-27

Use XmlBuilder

Listing 9-28 shows the built XML document by code in Listing 9-27.
<?xml version="1.0"?>
<note id="001">
  <from>John</from>
  <to>Jane</to>
  <message>Hello!
    <!--message to send-->
  </message>
</note>
Listing 9-28

Built XML document

9.10 Handling HTML Data

Problem

You want to parse HTML document in Flutter apps.

Solution

Use html library.

Discussion

Even though JSON and XML data format are popular in Flutter apps, you may still need to parse HTML document to extract data. This process is called screen scraping. You can use html library to parse HTML document. To use this library, you need to add html: ^0.13.4+1 to the dependencies of pubspec.yaml file.

The parse() function parses HTML strings into Document objects. These Document objects can be queried and manipulated using W3C DOM API. In Listing 9-29, HTML string is parsed first, then getElementsByTagName() method is used to get the li elements, and finally id attribute and text are extracted from li elements.
import 'package:html/dom.dart';
import 'package:html/parser.dart' show parse;
void main() {
  String htmlStr = "'
  <ul>
    <li id="001">John</li>
    <li id="002">Jane</li>
    <li id="003">Mary</li>
  </ul>
  "';
  Document document = parse(htmlStr);
  var users = document.getElementsByTagName('li').map((Element element) {
    return {
      'id': element.attributes['id'],
      'name': element.text,
    };
  });
  print(users);
}
Listing 9-29

Parse HTML document

9.11 Sending HTTP Requests

Problem

You want to send HTTP requests to backend services.

Solution

Use HttpClient from dart:io library.

Discussion

HTTP protocol is a popular choice to expose web services. The representation can be JSON or XML. By using HttpClient class from dart:io library, you can easily interact with backend services over HTTP.

To use HttpClient class, you need to choose a HTTP method first, then prepare the HttpClientRequest object for the request, and process the HttpClientResponse object for the response. HttpClient class has different pairs of methods corresponding to different HTTP methods. For example, get() and getUrl() methods are both used to send HTTP GET requests. The difference is that get() method accepts host, port, and path parameters, while getUrl() method accepts url parameter of type Uri. You can see other pairs like post() and postUrl(), put() and putUrl(), patch() and patchUrl(), delete() and deleteUrl(), and head() and headUrl().

These methods return Future<HttpClientRequest> objects. You need to chain the returned Future objects with then() method to prepare HttpClientRequest object. For example, you can modify HTTP request headers or write request body. The then() method needs to return the value of HttpClientRequest.close() method, which is a Future<HttpClientResponse> object. In the then() method of the Future<HttpClientResponse> object, you can use this object to get response body, headers, cookies, and other information.

In Listing 9-30, request.close() method is called directly in the first then() method, because we don’t need to do anything to the HttpClientRequest object. The _handleResponse() function decodes HTTP response as UTF-8 strings and prints them out. HttpClientResponse class implements Stream<List<int>>, so the response body can be read as streams.
void _handleResponse(HttpClientResponse response) {
  response.transform(utf8.decoder).listen(print);
}
HttpClient httpClient = HttpClient();
httpClient
    .getUrl(Uri.parse('https://httpbin.org/get'))
    .then((HttpClientRequest request) => request.close())
    .then(_handleResponse);
Listing 9-30

Send HTTP GET request

If you need to send HTTP POST, PUT, and PATCH requests with body, you can use HttpClientRequest.write() method to write the body; see Listing 9-31.
httpClient
    .postUrl(Uri.parse('https://httpbin.org/post'))
    .then((HttpClientRequest request) {
  request.write('hello');
  return request.close();
}).then(_handleResponse);
Listing 9-31

Write HTTP request body

If you need to modify HTTP request headers, you can use the HttpClientRequest.headers property to modify the HttpHeaders object; see Listing 9-32.
httpClient
    .getUrl(Uri.parse('https://httpbin.org/headers'))
    .then((HttpClientRequest request) {
  request.headers.set(HttpHeaders.userAgentHeader, 'my-agent');
  return request.close();
}).then(_handleResponse);
Listing 9-32

Modify HTTP request headers

If you need to support HTTP basic authentication, you can use HttpClient.addCredentials() method to add HttpClientBasicCredentials objects; see Listing 9-33.
String username = 'username', password = 'password';
Uri uri = Uri.parse('https://httpbin.org/basic-auth/$username/$password');
httpClient.addCredentials(
    uri, null, HttpClientBasicCredentials(username, password));
httpClient
    .getUrl(uri)
    .then((HttpClientRequest request) => request.close())
    .then(_handleResponse);
Listing 9-33

Basic authentication

9.12 Connecting to WebSocket

Problem

You want to connect to WebSocket servers in Flutter apps.

Solution

Use WebSocket class in dart:io library.

Discussion

WebSockets are widely used in web apps to provide bidirectional communications between browser and server. They can also provide real-time updates of data in the backend. If you already have a WebSocket server that interacts with the web app running in the browser, you may also want the same feature to be available in Flutter apps . WebSocket class in dart:io library can be used to implement the WebSocket connections.

The static WebSocket.connect() method connects to a WebSocket server. You need to provide the server URL with scheme ws or wss. You can optionally provide a list of subprotocols and a map of headers. The return value of connect() method is a Future<WebSocket> object. WebSocket class implements Stream class, so you can read data sent from server as streams. To send data to the server, you can use add() and addStream() methods.

In Listing 9-34, the WebSocket connects to the demo echo server. By using listen() method to subscribe to the WebSocket object, we can process data sent from the server. The two add() method calls send two messages to the server.
WebSocket.connect('ws://demos.kaazing.com/echo').then((WebSocket webSocket) {
  webSocket.listen(print, onError: print);
  webSocket.add('hello');
  webSocket.add('world');
  webSocket.close();
}).catchError(print);
Listing 9-34

Connect to WebSocket

9.13 Connecting to Socket

Problem

You want to connect to socket servers .

Solution

Use Socket class in dart:io library.

Discussion

If you want to connect to socket servers in Flutter apps, you can use Socket class from dart:io library. The static Socket.connect() method connects to a socket server at specified host and port and returns a Future<Socket> object. Socket class implements Stream<List<int>>, so you can read data from server by subscribing to the stream . To send data to the server, you can use add() and addStream() methods.

In Listing 9-35, a socket server is started on port 10080. This server converts the received strings into uppercase and sends back the results.
import 'dart:io';
import 'dart:convert';
void main() {
  ServerSocket.bind('127.0.0.1', 10080).then((serverSocket) {
    serverSocket.listen((socket) {
      socket.addStream(socket
          .transform(utf8.decoder)
          .map((str) => str.toUpperCase())
          .transform(utf8.encoder));
    });
  });
}
Listing 9-35

Simple socket server

In Listing 9-36, Socket.connect() method is used to connect to the socket server shown in Listing 9-35. Data received from the server is printed out. Two strings are sent to the server.
void main() {
  Socket.connect('127.0.0.1', 10080).then((socket) {
    socket.transform(utf8.decoder).listen(print);
    socket.write('hello');
    socket.write('world');
    socket.close();
  });
}
Listing 9-36

Connect to socket server

9.14 Interacting JSON-Based REST Services

Problem

You want to use JSON-based REST services .

Solution

Use HttpClient, json_serialize library, and FutureBuilder widget.

Discussion

It’s a popular choice for mobile apps backend to expose services over HTTP protocol with JSON as the representation. By using HttpClient, json_serialize library, and FutureBuilder widget, you can build the UI to work with these REST services. This recipe provides a concrete example which combines content in Listings 9-6, 9-8, and 9-11.

This example uses GitHub Jobs API ( https://jobs.github.com/api ) to get job listings on GitHub web site . In Listing 9-37, Job class represents a job listing. In the JsonSerializable annotation, createToJson property is set to false, because we only need to parse JSON response from the API. The _parseDate function parses the string in created_at field of the JSON object. You need to add intl library to use DateFormat class .
part 'github_jobs.g.dart';
DateFormat _dateFormat = DateFormat('EEE MMM dd HH:mm:ss yyyy');
DateTime _parseDate(String str) =>
    _dateFormat.parse(str.replaceFirst(' UTC', "), true);
@JsonSerializable(
  createToJson: false,
)
class Job {
  Job();
  String id;
  String type;
  String url;
  @JsonKey(name: 'created_at', fromJson: _parseDate)
  DateTime createdAt;
  String company;
  @JsonKey(name: 'company_url')
  String companyUrl;
  @JsonKey(name: 'company_logo')
  String companyLogo;
  String location;
  String title;
  String description;
  @JsonKey(name: 'how-to-apply')
  String howToApply;
  factory Job.fromJson(Map<String, dynamic> json) => _$JobFromJson(json);
}
Listing 9-37

Job class

In Listing 9-38, a HttpClient object is used to send a HTTP GET request to GitHub Jobs API and parse the JSON response using jsonDecode() function. The Future object of type Future<List<Job>> is used by FutureBuilder widget to build the UI . JobsList widget takes a List<Job> object and displays the list using ListView widget.
class GitHubJobsPage extends StatelessWidget {
  final Future<List<Job>> _jobs = HttpClient()
      .getUrl(Uri.parse('https://jobs.github.com/positions.json'
          '?description=java&location=new+york'))
      .then((HttpClientRequest request) => request.close())
      .then((HttpClientResponse response) {
    return response.transform(utf8.decoder).join(").then((String content) {
      return (jsonDecode(content) as List<dynamic>)
          .map((json) => Job.fromJson(json))
          .toList();
    });
  });
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GitHub Jobs'),
      ),
      body: FutureBuilder<List<Job>>(
        future: _jobs,
        builder: (BuildContext context, AsyncSnapshot<List<Job>> snapshot) {
          if (snapshot.hasData) {
            return JobsList(snapshot.data);
          } else if (snapshot.hasError) {
            return Center(
              child: Text(
                '${snapshot.error}',
                style: TextStyle(color: Colors.red),
              ),
            );
          } else {
            return Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }
}
class JobsList extends StatelessWidget {
  JobsList(this.jobs);
  final List<Job> jobs;
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (BuildContext context, int index) {
        Job job = jobs[index];
        return ListTile(
          title: Text(job.title),
          subtitle: Text(job.company),
        );
      },
      separatorBuilder: (BuildContext context, int index) {
        return Divider();
      },
      itemCount: jobs.length,
    );
  }
}
Listing 9-38

Widget to show jobs

9.15 Interacting with gRPC Services

Problem

You want to interact with gRPC services .

Solution

Use grpc library.

Discussion

gRPC ( https://grpc.io/ ) is a high-performance, open-source universal RPC framework. This recipe shows how to interact with gRPC services . The gRPC service to interact is the greeter service from gRPC official examples ( https://github.com/grpc/grpc/tree/master/examples/node ). You need to start the gRPC server first.

To use this gRPC service in Flutter apps, you need to install Protocol Buffers compiler ( https://github.com/protocolbuffers/protobuf ) first. After downloading the release file for your platform and extracting its content, you need to add the extracted bin directory to the PATH environment variable. You can run protoc --version command to verify the installation . The version used in this recipe is 3.7.1.

You also need to install Dart protoc plugin ( https://github.com/dart-lang/protobuf/tree/master/protoc_plugin ). The easiest way to install is to run the following command.
$ flutter packages pub global activate protoc_plugin
Because we use flutter packages to run the installation, the binary file is put under the .pub-cache/bin directory of the Flutter SDK. You need to add this path to PATH environment variable. The plugin requires dart command to be available, so you also need to add bin/cache/dart-sdk/bin directory of Flutter SDK to PATH environment variable. Now we can use protoc to generate Dart files for interactions with the greeter service . In the following command, lib/grpc/generated is the output path of generated files. proto_file_path is the path of proto files. helloworld.proto file contains the definition for greeter service. Libraries protobuf and grpc also need to be added to the dependencies of pubspec.yaml file.
$ protoc --dart_out=grpc:lib/grpc/generated --proto_path=<proto_file_path> <proto_file_path>/helloworld.proto
The generated helloworld.pbgrpc.dart file provides GreeterClient class to interact with the service. In Listing 9-39, a ClientChannel is created to connect to the gRPC server. The channel is required when creating a GreeterClient object. The sayHello() method sends requests to the server and receives responses.
import 'package:grpc/grpc.dart';
import 'generated/helloworld.pbgrpc.dart';
void main() async {
  final channel = new ClientChannel('localhost',
      port: 50051,
      options: const ChannelOptions(
          credentials: const ChannelCredentials.insecure()));
  final stub = new GreeterClient(channel);
  try {
    var response = await stub.sayHello(new HelloRequest()..name = 'John');
    print('Received: ${response.message}');
  } catch (e) {
    print('Caught error: $e');
  }
  await channel.shutdown();
}
Listing 9-39

Interact with gRPC service

9.16 Summary

This chapter focuses on different ways to interact with backend services, including HTTP, WebSocket, Socket, and gRPC. Futures and Streams play an important role in asynchronous computations. This chapter also discusses how to handle JSON, XML, and HTML data. In the next chapter, we’ll discuss state management in Flutter apps.

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

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