HTTP Client 6.0


Most frontend apps communicate with backend services using the HTTP protocol. Dart web apps typically do this using the XMLHttpRequest (XHR) API, using either HttpRequest from the dart:html library or a higher level API, such as what the http package provides.

The following demos, which use the http package, illustrate server communication:

Try the live example (view source), which hosts both demos.

Providing HTTP services

The demos in this page use the http package’s Client interface. The following code registers a factory provider (which creates a BrowserClient instance) for Client:

web/main.dart

import 'package:angular/angular.dart';
import 'package:http/browser_client.dart';
import 'package:http/http.dart';
import 'package:server_communication/app_component.template.dart' as ng;

import 'main.template.dart' as self;

@GenerateInjector([
  ClassProvider(Client, useClass: BrowserClient),
])
final InjectorFactory injector = self.injector$Injector;

void main() {
  runApp(ng.AppComponentNgFactory, createInjector: injector);
}

HTTP client demo: Tour of Heroes

This demo is a shorter version of the Tour of Heroes app. It receives heroes from a server and displays them as a list. The user can add new heroes and save them to the server.

Here’s what the app’s UI looks like:

ToH mini app

This demo has a single component, the HeroListComponent. Here is its template:

lib/src/toh/hero_list_component.html

<h1>Tour of Heroes</h1>
<h3>Heroes:</h3>
<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>

<label>New hero name: <input #newHeroName /></label>
<button (click)="add(newHeroName.value); newHeroName.value=''">Add Hero</button>

<p class="error" *ngIf="errorMessage != null">{{errorMessage}}</p>

The template’s ngFor directive displays the list of heroes. Below the list are an input box and an Add Hero button, which allow the user to add new heroes.

A template reference variable, newHeroName, gives the (click) event binding access to the value of the input box. When the user clicks the button, the click handler passes the input value to the addHero() method of the component. The click handler also clears the input box.

Below the button is an area for an error message.

The HeroListComponent class

Here’s the component class:

lib/src/toh/hero_list_component.dart (class)

class HeroListComponent implements OnInit {
  final HeroService _heroService;
  String errorMessage;
  List<Hero> heroes = [];

  HeroListComponent(this._heroService);

  @override
  void ngOnInit() => _getHeroes();

  Future<void> _getHeroes() async {
    try {
      heroes = await _heroService.getAll();
    } catch (e) {
      errorMessage = e.toString();
    }
  }

  Future<void> add(String name) async {
    name = name.trim();
    if (name.isEmpty) return null;
    try {
      heroes.add(await _heroService.create(name));
    } catch (e) {
      errorMessage = e.toString();
    }
  }
}

Angular injects a HeroService into the constructor, and the component calls that service to fetch and save data.

The component doesn’t interact directly with the Client. Instead, it delegates data access to the HeroService.

Always delegate data access to a supporting service class.

Although at runtime the component requests heroes immediately after creation, this request is not in the component’s constructor. Instead, the request is in the ngOnInit lifecycle hook.

Keep constructors simple. Components are easier to test and debug when their constructors are simple, with all real work (such as calling a remote server) handled by a separate method.

The asynchronous methods in the hero service, getAll() and create(), return the Future values of the current hero list and the newly added hero, respectively. The methods in the hero list component, _getHeroes() and add(), specify the actions to be taken when the asynchronous method calls succeed or fail.

For more information about Future, see the futures tutorial and the resources at the end of that tutorial.

Fetching data

In the previous samples, the app faked interaction with the server by returning mock heroes in a service:

import 'dart:async';

import 'hero.dart';
import 'mock_heroes.dart';

class HeroService {
  Future<List<Hero>> getAll() async => mockHeroes;
}

It’s time to get real data. The following code makes HeroService get the heroes from the server:

lib/src/toh/hero_service.dart (revised)

import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart';

import 'hero.dart';

class HeroService {
  static final _headers = {'Content-Type': 'application/json'};
  static const _heroesUrl = 'api/heroes'; // URL to web API
  final Client _http;

  HeroService(this._http);

  Future<List<Hero>> getAll() async {
    try {
      final response = await _http.get(_heroesUrl);
      final heroes = (_extractData(response) as List)
          .map((value) => Hero.fromJson(value))
          .toList();
      return heroes;
    } catch (e) {
      throw _handleError(e);
    }
  }

Use a Client object

This demo uses a Client object that’s injected into the HeroService constructor:

HeroService(this._http);

Here’s the code that uses the client’s get() method to fetch data:

lib/src/toh/hero_service.dart (getAll)

static const _heroesUrl = 'api/heroes'; // URL to web API
// ···
Future<List<Hero>> getAll() async {
  try {
    final response = await _http.get(_heroesUrl);
    final heroes = (_extractData(response) as List)
        .map((value) => Hero.fromJson(value))
        .toList();
    return heroes;
  } catch (e) {
    throw _handleError(e);
  }
}

The get() method takes a resource URL, which it uses to contact the server that returns heroes.

Mock the server

When no server exists yet or you want to avoid network reliability issues during testing, don’t use a BrowserClient as the Client object. Instead, you can mock the server by using the in-memory web API, which is what the live example (view source) does.

Alternatively, use a JSON file:

static const _heroesUrl = 'heroes.json'; // URL to JSON file

Processing the response object

The getAll() method uses an _extractData() helper method to map the _http.get() response object to heroes:

lib/src/toh/hero_service.dart (excerpt)

dynamic _extractData(Response resp) => json.decode(resp.body)['data'];

The response object doesn’t hold the data in a form that the app can use directly. To use the response data, you must first decode it.

Decode JSON

The response data is in JSON string form. You must deserialize that string into objects, which you can do by calling the JSON.decode() method from the dart:convert library. For examples of decoding and encoding JSON, see the dart:convert section of the Dart library tour.

The decoded JSON doesn’t list the heroes. Instead, the server wraps JSON results in an object with a data property. This is conventional web API behavior, driven by security concerns.

Assume nothing about the server API. Not all servers return an object with a data property.

Don’t return the response object

Although it’s possible for getAll() to return the HTTP response, that’s not a good practice. The point of a data service is to hide the server interaction details from consumers. A component that calls the HeroService only wants the heroes. It is separated from from the code that’s responsible for getting the data, and from the response object.

Always handle errors

An important part of dealing with I/O is anticipating errors by preparing to catch them and do something with them. One way to handle errors is to pass an error message back to the component for presentation to the user, but only if the message is something that the user can understand and act upon.

This simple app handles a getAll() error as follows:

lib/src/toh/hero_service.dart (excerpt)

Future<List<Hero>> getAll() async {
  try {
    final response = await _http.get(_heroesUrl);
    final heroes = (_extractData(response) as List)
        .map((value) => Hero.fromJson(value))
        .toList();
    return heroes;
  } catch (e) {
    throw _handleError(e);
  }
}

Exception _handleError(dynamic e) {
  print(e); // for demo purposes only
  return Exception('Server error; cause: $e');
}

HeroListComponent error handling

In HeroListComponent, the call to _heroService.getAll() is in a try clause, and the errorMessage variable is conditionally bound in the template. When an exception occurs, the errorMessage variable is assigned a value as follows:

lib/src/toh/hero_list_component.dart (_getHeroes)

Future<void> _getHeroes() async {
  try {
    heroes = await _heroService.getAll();
  } catch (e) {
    errorMessage = e.toString();
  }
}

To create a failure scenario, reset the API endpoint to a bad value in HeroService. Afterward, remember to restore its original value.

Sending data to the server

So far you’ve seen how to retrieve data from a remote location using an HTTP service. The next task is adding the ability to create new heroes and save them in the backend.

First, the service needs a method that the component can call to create and save a hero. For this demo, the method is called create() and takes the name of a new hero:

Future<Hero> create(String name) async {

To implement the method, you must know the server’s API for creating heroes. This sample’s data server follows typical REST guidelines. It supports a POST request at the same endpoint as GET heroes. The new hero data must be in the body of the request, structured like a Hero entity but without the id property. Here’s an example of the body of the request:

{"name": "Windstorm"}

The server generates the id and returns the JSON representation of the new hero, including the generated ID. The hero is inside a response object with its own data property.

Now that you know the server’s API, here’s the implementation of create():

lib/src/toh/hero_service.dart (create)

Future<Hero> create(String name) async {
  try {
    final response = await _http.post(_heroesUrl,
        headers: _headers, body: json.encode({'name': name}));
    return Hero.fromJson(_extractData(response));
  } catch (e) {
    throw _handleError(e);
  }
}

Headers

In the _headers object, the Content-Type specifies that the body represents JSON.

JSON results

As in _getHeroes(), the _extractData() helper extracts the data from the response.

Back in HeroListComponent, the addHero() method waits for the service’s asynchronous create() method to create a hero. When create() is finished, addHero() puts the new hero in the heroes list:

lib/src/toh/hero_list_component.dart (add)

Future<void> add(String name) async {
  name = name.trim();
  if (name.isEmpty) return null;
  try {
    heroes.add(await _heroService.create(name));
  } catch (e) {
    errorMessage = e.toString();
  }
}

Cross-origin requests: Wikipedia example

Although making XMLHttpRequests (often with a helper API, such as BrowserClient) is a common approach to server communication in Dart web apps, this approach isn’t always possible.

For security reasons, web browsers block XHR calls to a remote server whose origin is different from the origin of the web page. The origin is the combination of URI scheme, hostname, and port number. This is called the same-origin policy.

Modern browsers allow XHR requests to servers from a different origin if the server supports the CORS protocol. You can enable user credentials in request headers.

Some servers do not support CORS but do support an older, read-only alternative called JSONP.

For more information about JSONP, see this Stack Overflow answer.

Search Wikipedia

The following simple search shows suggestions from Wikipedia as the user types in a text box:

Wikipedia search app (v.1)

Wikipedia offers a modern CORS API and a legacy JSONP search API.

This page is under construction. For now, see the demo source code for an example of using Wikipedia’s JSONP API.