Services

The Tour of Heroes is evolving and we anticipate adding more components in the near future.

Multiple components will need access to hero data and we don't want to copy and paste the same code over and over. Instead, we'll create a single reusable data service and learn to inject it in the components that need it.

Refactoring data access to a separate service keeps the component lean and focused on supporting the view. It also makes it easier to unit test the component with a mock service.

Because data services are invariably asynchronous, we'll finish the chapter with a Future-based version of the data service.

Run the for this part.

Where We Left Off

Before we continue with our Tour of Heroes, let’s verify we have the following structure. If not, we’ll need to go back and follow the previous chapters.

angular_tour_of_heroes
lib
app_component.dart
hero.dart
hero_detail_component.dart
web
index.html
main.dart
styles.css
pubspec.yaml

Keep the app compiling and running

Open a terminal/console window. Start the Dart compiler, watch for changes, and start our server by entering the command:

pub serve

The application runs and updates automatically as we continue to build the Tour of Heroes.

Creating a Hero Service

Our stakeholders have shared their larger vision for our app. They tell us they want to show the heroes in various ways on different pages. We already can select a hero from a list. Soon we'll add a dashboard with the top performing heroes and create a separate view for editing hero details. All three views need hero data.

At the moment the AppComponent defines mock heroes for display. We have at least two objections. First, defining heroes is not the component's job. Second, we can't easily share that list of heroes with other components and views.

We can refactor this hero data acquisition business to a single service that provides heroes, and share that service with all components that need heroes.

Create the HeroService

Create a file in the lib folder called hero_service.dart.

We've adopted a convention in which we spell the name of a service in lowercase followed by _service. If the service name were multi-word, we'd spell the base filename in lower underscore case (also called snake_case). The SpecialSuperHeroService would be defined in the special_super_hero_service.dart file.

We name the class HeroService.

lib/hero_service.dart (starting point)

import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { }

Injectable Services

Notice that we used an @Injectable() annotation.

Don't forget the parentheses! Neglecting them leads to an error that's difficult to diagnose.

Dart sees the @Injectable() annotation and emits metadata about our service, metadata that Angular may need to inject other dependencies into this service.

The HeroService doesn't have any dependencies at the moment. Add the annotation anyway. It is a "best practice" to apply the @Injectable() annotation ​from the start​ both for consistency and for future-proofing.

Getting Heroes

Add a getHeroes method stub.

lib/hero_service.dart (getHeroes stub)

@Injectable() class HeroService { List<Hero> getHeroes() {} // stub }

We're holding back on the implementation for a moment to make an important point.

The consumer of our service doesn't know how the service gets the data. Our HeroService could get Hero data from anywhere. It could get the data from a web service or local storage or from a mock data source.

That's the beauty of removing data access from the component. We can change our minds about the implementation as often as we like, for whatever reason, without touching any of the components that need heroes.

Mock Heroes

We already have mock Hero data sitting in the AppComponent. It doesn't belong there. It doesn't belong here either. We'll move the mock data to its own file.

Cut the mockHeroes list from app_component.dart and paste it to a new file in the lib folder named mock_heroes.dart. We copy the import 'hero.dart' statement as well because the heroes list uses the Hero class.

lib/mock_heroes.dart

import 'hero.dart'; final List<Hero> mockHeroes = [ new Hero(11, 'Mr. Nice'), new Hero(12, 'Narco'), new Hero(13, 'Bombasto'), new Hero(14, 'Celeritas'), new Hero(15, 'Magneta'), new Hero(16, 'RubberMan'), new Hero(17, 'Dynama'), new Hero(18, 'Dr IQ'), new Hero(19, 'Magma'), new Hero(20, 'Tornado') ];

Meanwhile, back in app_component.dart where we cut away the mockHeroes list, we leave behind an uninitialized heroes property:

lib/app_component.dart (heroes property)

List<Hero> heroes;

Return Mocked Heroes

Back in the HeroService we import the mock mockHeroes and return it from the getHeroes method. Our HeroService looks like this:

lib/hero_service.dart

import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { List<Hero> getHeroes() => mockHeroes; }

Use the Hero Service

We're ready to use the HeroService in other components starting with our AppComponent.

We begin, as usual, by importing the thing we want to use, the HeroService.

import 'hero_service.dart';

Importing the service allows us to reference it in our code. How should the AppComponent acquire a runtime concrete HeroService instance?

Do we new the HeroService? No way!

We could create a new instance of the HeroService with new like this:

HeroService heroService = new HeroService(); // don't do this

That's a bad idea for several reasons including

What if ... what if ... Hey, we've got work to do!

We get it. Really we do. But it is so ridiculously easy to avoid these problems that there is no excuse for doing it wrong.

Inject the HeroService

Three lines replace the one line of new:

  1. We add a property.
  2. We add a constructor that sets the property.
  3. We add to the component's providers metadata.

Here are the property and the constructor:

lib/app_component.dart (constructor)

final HeroService _heroService; AppComponent(this._heroService);

The constructor does nothing except set the _heroService property. The HeroService type of _heroService identifies the constructor's parameter as a HeroService injection site.

Now Angular will know to supply an instance of the HeroService when it creates a new AppComponent.

Angular has to get that instance from somewhere. That's the role of the Angular Dependency Injector. The Injector has a container of previously created services. Either it finds and returns a pre-existing HeroService from its container or it creates a new instance, adds it to the container, and returns it to Angular.

Learn more about Dependency Injection in the Dependency Injection chapter.

The injector does not know yet how to create a HeroService. If we ran our code now, Angular would fail with an error:

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)

We have to teach the injector how to make a HeroService by registering a HeroService provider. Do that by adding the following providers parameter to the bottom of the component metadata in the @Component annotation.

providers: const [HeroService])

The providers parameter tells Angular to create a fresh instance of the HeroService when it creates a new AppComponent. The AppComponent can use that service to get heroes and so can every child component of its component tree.

Services and the component tree

Recall that the AppComponent creates an instance of HeroDetail by virtue of the <my-hero-detail> tag at the bottom of its template. That HeroDetail is a child of the AppComponent.

If the HeroDetailComponent needed its parent component's HeroService, it would ask Angular to inject the service into its constructor which would look just like the one for AppComponent:

lib/hero_detail_component.dart (constructor)

final HeroService _heroService; AppComponent(this._heroService);

The HeroDetailComponent must not repeat its parent's providers list! Guess why.

The AppComponent is the top level component of our application. There should be only one instance of that component and only one instance of the HeroService in our entire app.

getHeroes in the AppComponent

We've got the service in a _heroService private variable. Let's use it.

We pause to think. We can call the service and get the data in one line.

heroes = _heroService.getHeroes();

We don't really need a dedicated method to wrap one line. We write it anyway:

void getHeroes() { heroes = _heroService.getHeroes(); }

The ngOnInit Lifecycle Hook

AppComponent should fetch and display heroes without a fuss. Where do we call the getHeroes method? In a constructor? We do not!

Years of experience and bitter tears have taught us to keep complex logic out of the constructor, especially anything that might call a server as a data access method is sure to do.

The constructor is for simple initializations like wiring constructor parameters to properties. It's not for heavy lifting. We should be able to create a component in a test and not worry that it might do real work — like calling a server! — before we tell it to do so.

If not the constructor, something has to call getHeroes.

Angular will call it if we implement the Angular ngOnInit Lifecycle Hook. Angular offers a number of interfaces for tapping into critical moments in the component lifecycle: at creation, after each change, and at its eventual destruction.

Each interface has a single method. When the component implements that method, Angular calls it at the appropriate time.

Learn more about lifecycle hooks in the Lifecycle Hooks chapter.

Here's the essential outline for the OnInit interface:

lib/app_component.dart (ngOnInit stub)

import 'package:angular2/core.dart'; class AppComponent implements OnInit { void ngOnInit() { } }

We write an ngOnInit method with our initialization logic inside and leave it to Angular to call it at the right time. In our case, we initialize by calling getHeroes.

void ngOnInit() { getHeroes(); }

Our application should be running as expected, showing a list of heroes and a hero detail view when we click on a hero name.

We're getting closer. But something isn't quite right.

Async Services and Future

Our HeroService returns a list of mock heroes immediately. Its getHeroes signature is synchronous

heroes = _heroService.getHeroes();

Ask for heroes and they are there in the returned result.

Someday we're going to get heroes from a remote server. We don’t call http yet, but we aspire to in later chapters.

When we do, we'll have to wait for the server to respond and we won't be able to block the UI while we wait, even if we want to (which we shouldn't) because the browser won't block.

We'll have to use some kind of asynchronous technique and that will change the signature of our getHeroes method.

We'll use Futures.

The Hero Service returns a Future

We ask an asynchronous service to do some work and give us the result in the Future. The service does that work (somewhere) and eventually it updates the Future with the results of the work or an error.

We are simplifying. Learn about Futures in the tutorial Asynchronous Programming: Futures.

Update the HeroService with this Future-returning getHeroes method:

lib/hero_service.dart (excerpt)

Future<List<Hero>> getHeroes() async => mockHeroes;

We're still mocking the data. We're simulating the behavior of an ultra-fast, zero-latency server, by returning a Future that will quickly resolve with our mock heroes as the result.

Marking the method's body with async makes the method immediately return a Future object. That Future later completes with the method's return value. For more information on async functions, see Declaring async functions in the Dart language tour.

Act on the Future

Returning to the AppComponent and its getHeroes method, we see that it still looks like this:

lib/app_component.dart (getHeroes - old)

void getHeroes() { heroes = _heroService.getHeroes(); }

As a result of our change to HeroService, we're now setting heroes to a Future rather than a list of heroes.

We have to change our implementation to act on the Future when it resolves. We can await for the Future to resolve, and then display the heroes:

lib/app_component.dart (getHeroes - revised)

Future<Null> getHeroes() async { heroes = await _heroService.getHeroes(); }

Our code waits until the Future completes, and then sets the component's heroes property to the list of heroes returned by the service. That's all there is to it!

Our app should still be running, still showing a list of heroes, and still responding to a name selection with a detail view.

Check out the "Take it slow" appendix to see what the app might be like with a poor connection.

Review the App Structure

Let’s verify that we have the following structure after all of our good refactoring in this chapter:

angular_tour_of_heroes
lib
app_component.dart
hero.dart
hero_detail_component.dart
hero_service.dart
mock_heroes.dart
web
index.html
main.dart
styles.css
pubspec.yaml

Here are the code files we discussed in this chapter.

import 'dart:async'; import 'package:angular2/core.dart'; import 'hero.dart'; import 'mock_heroes.dart'; @Injectable() class HeroService { Future<List<Hero>> getHeroes() async => mockHeroes; } import 'dart:async'; import 'package:angular2/core.dart'; import 'hero.dart'; import 'hero_detail_component.dart'; import 'hero_service.dart'; @Component( selector: 'my-app', template: ''' <h1>{{title}}</h1> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes" [class.selected]="hero == selectedHero" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <my-hero-detail [hero]="selectedHero"></my-hero-detail> ''', styles: const [ ''' .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 10em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0em; height: 1.6em; border-radius: 4px; } .heroes li.selected:hover { color: white; } .heroes li:hover { color: #607D8B; background-color: #EEE; left: .1em; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0em 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0px 0px 4px; } ''' ], directives: const [ HeroDetailComponent ], providers: const [ HeroService ]) class AppComponent implements OnInit { String title = 'Tour of Heroes'; List<Hero> heroes; Hero selectedHero; final HeroService _heroService; AppComponent(this._heroService); Future<Null> getHeroes() async { heroes = await _heroService.getHeroes(); } void ngOnInit() { getHeroes(); } void onSelect(Hero hero) { selectedHero = hero; } } import 'hero.dart'; final List<Hero> mockHeroes = [ new Hero(11, 'Mr. Nice'), new Hero(12, 'Narco'), new Hero(13, 'Bombasto'), new Hero(14, 'Celeritas'), new Hero(15, 'Magneta'), new Hero(16, 'RubberMan'), new Hero(17, 'Dynama'), new Hero(18, 'Dr IQ'), new Hero(19, 'Magma'), new Hero(20, 'Tornado') ];

The Road We’ve Travelled

Let’s take stock of what we’ve built.

The Road Ahead

Our Tour of Heroes has become more reusable using shared components and services. We want to create a dashboard, add menu links that route between the views, and format data in a template. As our app evolves, we’ll learn how to design it to make it easier to grow and maintain.

We learn about Angular Component Router and navigation among the views in the next tutorial chapter.

Appendix: Take it slow

We can simulate a slow connection.

Add the following getHeroesSlowly method to the HeroService:

lib/hero_service.dart (getHeroesSlowly)

Future<List<Hero>> getHeroesSlowly() { return new Future.delayed(const Duration(seconds: 2), getHeroes); }

Like getHeroes, it also returns a Future. But this Future waits 2 seconds before resolving the Future with mock heroes.

Back in the AppComponent, replace _heroService.getHeroes with _heroService.getHeroesSlowly and see how the app behaves.

Appendix: Shadowing the parent's service

We stated earlier that if we injected the parent AppComponent HeroService into the HeroDetailComponent, we must not add a providers list to the HeroDetailComponent metadata.

Why? Because that tells Angular to create a new instance of the HeroService at the HeroDetailComponent level. The HeroDetailComponent doesn't want its own service instance; it wants its parent's service instance. Adding the providers list creates a new service instance that shadows the parent instance.

Think carefully about where and when to register a provider. Understand the scope of that registration. Be careful not to create a new service instance at the wrong level.

下一步

Routing