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.
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.
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.
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”.
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.
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.
Use async/await
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
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.
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.
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.
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
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. |
Use listen() method
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
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. |
Stream transformations
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. |
Methods return Future objects
9.5 Creating Streams
Problem
You want to create Stream objects .
Solution
Use different Stream constructors.
Discussion
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.
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.
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
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. |
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. |
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.
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 .
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 data serialization converts objects in Dart to JSON strings.
JSON data deserialization converts JSON strings to objects in Dart.
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.
Use toJson() function
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.
Add json_annotation and json_serializable
Use json_serializable
Generated code to handle JSON data
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. |
User class
Generated code for User class
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. |
Use JsonKey
Use JsonValue
Use JsonLiteral
9.9 Handling XML Data
Problem
You want to handle XML data in Flutter apps.
Solution
Use xml library.
Discussion
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.
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. |
XML document parsing and querying
Build XML Documents
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 . |
Use XmlBuilder
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.
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.
Send HTTP GET request
Write HTTP request body
Modify HTTP request headers
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.
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.
Simple socket server
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.
Job class
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.
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.