Dart2×AngularDart5 チュートリアル Part5. HTTP

この記事は「Webアプリ入門しようよ! in AngularDart」の続きです。

目次
Part1:The Hero Editor
Part2:Master/Detail
Part3:Multiple Components/Services
Part4:Routing
Part5:HTTP ←イマココ!

参考

これでようやくPart4も終わったようですね!Congratulation!

え?終わってない?
そんなあなたにはこちら↓を差し上げましょう。
Part4終了時点のソースコード

許せ...これで最後だ...トンッ(額に指を当てながら倒れる)

Heroのデータをサーバーから取得する!

現状、Heroのデータはモックそのままですが、ちゃんとしたWebアプリならばサーバーから取得することが必要でしょう。

さっそくやってみます。
(実際のサーバーと通信するとは言ってない←)

  1. pubspec.yamlにパッケージを追加!

    dependencies:
      angular: ^5.0.0
      angular_forms: ^2.0.0
      angular_router: ^2.0.0-alpha+19
      # ↓2つ追加!
      http: ^0.11.0
      stream_transform: ^0.0.6
    
  2. in_memory_data_service.dartを作成!

    import 'dart:async';
    import 'dart:convert';
    import 'dart:math';
    
    import 'package:http/http.dart';
    import 'package:http/testing.dart';
    
    import 'src/hero.dart';
    
    class InMemoryDataService extends MockClient {
      static final _initialHeroes = [
        {'id': 11, 'name': 'Mr. Nice'},
        {'id': 12, 'name': 'Narco'},
        {'id': 13, 'name': 'Bombasto'},
        {'id': 14, 'name': 'Celeritas'},
        {'id': 15, 'name': 'Magneta'},
        {'id': 16, 'name': 'RubberMan'},
        {'id': 17, 'name': 'Dynama'},
        {'id': 18, 'name': 'Dr IQ'},
        {'id': 19, 'name': 'Magma'},
        {'id': 20, 'name': 'Tornado'}
      ];
      static List<Hero> _heroesDb;
      static int _nextId;
      static Future<Response> _handler(Request request) async {
        if (_heroesDb == null) resetDb();
        var data;
        switch (request.method) {
          case 'GET':
            final id = int.tryParse(request.url.pathSegments.last);
            if (id != null) {
              data = _heroesDb
                  .firstWhere((hero) => hero.id == id); // throws if no match
            } else {
              String prefix = request.url.queryParameters['name'] ?? '';
              final regExp = RegExp(prefix, caseSensitive: false);
              data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList();
            }
            break;
          case 'POST':
            var name = json.decode(request.body)['name'];
            var newHero = Hero(_nextId++, name);
            _heroesDb.add(newHero);
            data = newHero;
            break;
          case 'PUT':
            var heroChanges = Hero.fromJson(json.decode(request.body));
            var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id);
            targetHero.name = heroChanges.name;
            data = targetHero;
            break;
          case 'DELETE':
            var id = int.parse(request.url.pathSegments.last);
            _heroesDb.removeWhere((hero) => hero.id == id);
            // No data, so leave it as null.
            break;
          default:
            throw 'Unimplemented HTTP method ${request.method}';
        }
        return Response(json.encode({'data': data}), 200,
            headers: {'content-type': 'application/json'});
      }
    
      static resetDb() {
        _heroesDb = _initialHeroes.map((json) => Hero.fromJson(json)).toList();
        _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1;
      }
    
      static String lookUpName(int id) =>
          _heroesDb.firstWhere((hero) => hero.id == id, orElse: null)?.name;
      InMemoryDataService() : super(_handler);
    }
    

    このin_memory_data_service.dartも所詮モックなので、詳細の説明はここでは省きます!

    (説明できるとは言ってない←)

  3. web/main.dartを修正!

    import 'package:angular/angular.dart';
    import 'package:angular_router/angular_router.dart';
    // ↓追加!
    import 'package:http/http.dart';
    
    import 'package:angular_app/app_component.template.dart' as ng;
    // ↓追加!
    import 'package:angular_app/in_memory_data_service.dart';
    
    import 'main.template.dart' as self;
    
    // ↓[]のつけ忘れに注意!
    @GenerateInjector([
      routerProvidersHash,
      // ↓追加!コンポーネントに追加するのと同じ意味!
      ClassProvider(Client, useClass: InMemoryDataService),
    ])
    
  4. src/hero.dartを修正!

    class Hero {
      final int id;
      String name;
    
      Hero(this.id, this.name);
    
      // ↓json形式でHttp通信するので、jsonとクラスを相互に変換する関数を実装!
      factory Hero.fromJson(Map<String, dynamic> hero) =>
          Hero(_toInt(hero['id']), hero['name']);
      Map toJson() => {'id': id, 'name': name};
    }
    
    int _toInt(id) => id is int ? id : int.parse(id);
    
  5. src/hero_service.dartを修正!

    // ↓インポート文を修正!
    import 'dart:async';
    import 'dart:convert';
    
    import 'package:http/http.dart';
    
    import 'hero.dart';
    
    // ↓getAll関数でサーバーからデータを取得するように変更!
    class HeroService {
      static const _heroesUrl = 'api/heroes';
    
      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((json) => Hero.fromJson(json))
              .toList();
          return heroes;
        } catch (e) {
          throw _handleError(e);
        }
      }
    
      // ↓get関数は修正しなくても動作する。
      // ↓が、毎回Heroリストを取得するのは無駄なので、
      // ↓idが合致するHeroのデータだけ取得するように修正!
      Future<Hero> get(int id) async {
        try {
          final response = await _http.get('$_heroesUrl/$id');
          return Hero.fromJson(_extractData(response));
        } catch (e) {
          throw _handleError(e);
        }
      }
    
      dynamic _extractData(Response resp) => json.decode(resp.body)['data'];
    
      Exception _handleError(dynamic e) {
        print(e); // for demo purposes only
        return Exception('Server error; cause: $e');
      }
    }
    
  6. mock_heroes.dartは不要になったので削除!

ブラウザを更新!
さっきと同じようにHeroリストが表示されたらOK!

ネクストッ!

Hero名の変更を保存する保存ボタンを追加!

もう茶番を繰り広げる余力は残されていません。
とにかく保存ボタンを追加しましょう!

  1. src/hero_service.dartを修正!

    // …
    
    class HeroService {
      static const _heroesUrl = 'api/heroes';
      // ↓追加!
      static final _headers = {'Content-Type': 'application/json'};
    
      // …
    
      // ↓Heroの変更を反映する関数を実装!
      Future<Hero> update(Hero hero) async {
        try {
          final url = '$_heroesUrl/${hero.id}';
          final response =
              await _http.put(url, headers: _headers, body: json.encode(hero));
          return Hero.fromJson(_extractData(response));
        } catch (e) {
          throw _handleError(e);
        }
      }
    
    // …
    
  2. src/hero_component.dartを修正!

    // ↓追加!
    import 'dart:async';
    
    // …
    
    class HeroComponent implements OnActivate {
    
        // …
    
        // ↓保存ボタン用の関数を実装!
        Future<void> save() async {
          await _heroService.update(hero);
          goBack();
        }
    }
    
  3. src/hero_component.htmlを修正!

    <div *ngIf="hero != null">
    
        <!-- … -->
    
        <!-- ↓保存ボタンを追加! -->
        <button (click)="save()">Save</button>
        <button (click)="goBack()">Back</button>
    </div>
    

ブラウザを更新!
詳細画面からHero名を変更して保存ボタンを押してみよう!

変更が反映されて、ダッシュボードとHeroリストを行き来しても変更した内容が維持されていればOK!

※モックサーバーなのでブラウザを更新すると元に戻るよ!

ネクストッ!

新しいHeroを作成するボタンを追加!

とにかく追加!Hurry!

  1. src/hero_service.dartを修正!

    class HeroService {
    
      // …
    
      // ↓文字列からHeroを作成して保存する関数を実装!
      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);
        }
      }
    
      // …
    }
    
  2. src/hero_list_component.dartを修正!

    class HeroListComponent implements OnInit {
    
      // …
    
      // ↓追加ボタン用の関数を実装!
      Future<void> add(String name) async {
        name = name.trim();
        if (name.isEmpty) return null;
        heroes.add(await _heroService.create(name));
        selected = null;
      }
    }
    
  3. src/hero_list_component.htmlを修正!

    <!-- ↓Hero名の入力フォームと追加ボタンを実装! -->
    <div>
        <label>Hero name:</label> <input #heroName />
        <button (click)="add(heroName.value); heroName.value=''">
            Add
        </button>
    </div>
    <h2>Heroes</h2>
    <!-- … -->
    

ブラウザを更新!
Heroリスト画面からHero名を入力して追加ボタンを押してみよう!

変更が反映されて、ダッシュボードとHeroリストを行き来しても変更した内容が維持されていればOK!

※モックサーバーなのでブラウザを更新すると元に戻るよ!(何度も言う)

ネクストッ!

Heroの削除ボタンを追加!

削除ッ!削除ッ!削除ォォーーッ!(取り憑かれたように)

  1. src/hero_service.dartを修正!

    class HeroService {
    
      // …
    
      // ↓IDからHeroを特定して削除する関数を実装!
      Future<void> delete(int id) async {
        try {
          final url = '$_heroesUrl/$id';
          await _http.delete(url, headers: _headers);
        } catch (e) {
          throw _handleError(e);
        }
      }
    
      // …
    }
    
  2. src/hero_list_component.dartを修正!

    class HeroListComponent implements OnInit {
    
      // …
    
      // ↓削除ボタン用の関数を実装!
      Future<void> delete(Hero hero) async {
        await _heroService.delete(hero.id);
        heroes.remove(hero);
        if (selected == hero) selected = null;
      }
    }
    
  3. src/hero_list_component.htmlを修正!

    <ul class="heroes">
        <li *ngFor="let hero of heroes" [class.selected]="hero === selected" (click)="onSelect(hero)">
            <!-- ↓IDと名前のスタイルを調整 -->
            <span class="badge">{{hero.id}}</span>
            <span>{{hero.name}}</span>
            <!-- 削除ボタンを追加! -->
            <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button>
        </li>
    
        <!-- … -->
    
    </ul>
    
  4. src/hero_list_component.cssを修正!

    /* … */
    
    /* ↓削除ボタンのスタイルを定義! */
    button.delete {
      float:right;
      margin-top: 2px;
      margin-right: .8em;
      background-color: gray !important;
      color:white;
    }
    

ブラウザを更新!
Heroリスト画面で削除ボタンを押してみよう!

変更が反映されて、ダッシュボードとHeroリストを行き来しても変更した内容が維持されていればOK!

※モックサーバーなのでブラウザを更新すると元に戻るよ!(しつこい)

ネクストッ!

ダッシュボードに検索ボックスを追加!

あれ、このHero登録したっけ?みたいなことってありますよね!

そんなとき、検索ボックスがあったら便利だと思いませんか!

思いますよね!(圧力)

  1. src/hero_search_service.dartを作る!

    • サーバーのWebAPIに検索クエリを送信するserch関数を定義!
    import 'dart:async';
    import 'dart:convert';
    
    import 'package:http/http.dart';
    
    import 'hero.dart';
    
    class HeroSearchService {
      final Client _http;
    
      HeroSearchService(this._http);
    
      Future<List<Hero>> search(String term) async {
        try {
          final response = await _http.get('app/heroes/?name=$term');
          return (_extractData(response) as List)
              .map((json) => Hero.fromJson(json))
              .toList();
        } catch (e) {
          throw _handleError(e);
        }
      }
    
      dynamic _extractData(Response resp) => json.decode(resp.body)['data'];
    
      Exception _handleError(dynamic e) {
        print(e); // for demo purposes only
        return Exception('Server error; cause: $e');
      }
    }
    
  2. src/hero_search_component.dartを作る!

    import 'dart:async';
    import 'package:angular/angular.dart';
    import 'package:angular_router/angular_router.dart';
    import 'package:stream_transform/stream_transform.dart';
    import 'route_paths.dart';
    import 'hero_search_service.dart';
    import 'hero.dart';
    
    @Component(
      selector: 'hero-search',
      templateUrl: 'hero_search_component.html',
      styleUrls: ['hero_search_component.css'],
      directives: [coreDirectives],
      providers: [ClassProvider(HeroSearchService)],
      pipes: [commonPipes],
    )
    class HeroSearchComponent implements OnInit {
      HeroSearchService _heroSearchService;
      Router _router;
    
      Stream<List<Hero>> heroes;
    
      // ↓このストリームはユーザーが入力したHero名の検索パターンを表す!
      StreamController<String> _searchTerms = StreamController<String>.broadcast();
    
      HeroSearchComponent(this._heroSearchService, this._router) {}
    
      // ↓templateから呼び出す関数!
      // ↓ストリームに、フォームに入力されたテキストを追加
      // ↓ユーザーの入力を直接サーバーへ送信すると、
      // ↓通信量が非常に多くなってしまうリスクがある!
      // ↓検索パターンを一旦ストリームに格納して文字列を精査することで、通信回数を抑えることができる!
      void search(String term) => _searchTerms.add(term);
    
      void ngOnInit() async {
        heroes = _searchTerms.stream
            // ↓300ms間ユーザーの入力が停止するのを待つ!
            // ↓これにより過度な通信を抑える!
            .transform(debounce(Duration(milliseconds: 300)))
            // ↓文字列が変更されたときだけ通信する!
            .distinct()
            // ↓以前の検索をキャンセルして、最新の検索のみ返す!
            .transform(switchMap((term) => term.isEmpty
                ? Stream<List<Hero>>.fromIterable([<Hero>[]])
                : _heroSearchService.search(term).asStream()))
            .handleError((e) {
          print(e); // for demo purposes only
        });
      }
    
      String _heroUrl(int id) =>
          RoutePaths.hero.toUrl(parameters: {idParam: '$id'});
    
      Future<NavigationResult> gotoDetail(Hero hero) =>
          _router.navigate(_heroUrl(hero.id));
    }
    
  3. src/hero_search_component.htmlを作る!

    <div id="search-component">
        <h4>Hero Search</h4>
        <!-- ↓検索ボックス -->
        <!-- ↓changeはユーザーがマウスでコピペしたとき等! -->
        <!-- ↓keyupはユーザーがキー入力したときに呼ばれる! -->
        <input #searchBox id="search-box" (change)="search(searchBox.value)" (keyup)="search(searchBox.value)" />
        <div>
            <!-- ↓検索結果のリスト -->
            <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result">
                {{hero.name}}
            </div>
        </div>
    </div>
    
  4. src/hero_search_component.cssを作る!

    .search-result {
      border-bottom: 1px solid gray;
      border-left: 1px solid gray;
      border-right: 1px solid gray;
      width:195px;
      height: 20px;
      padding: 5px;
      background-color: white;
      cursor: pointer;
    }
    #search-box {
      width: 200px;
      height: 20px;
    }
    
  5. src/dashboard_component.dartを修正!

    // …
    
    // ↓追加!
    import 'hero_search_component.dart';
    
    // …
    
      directives: [
        coreDirectives,
        routerDirectives,
        // ↓追加!
        HeroSearchComponent,
      ],
    
    // …
    
  6. src/dashboard_component.htmlを修正!

    <!-- … -->
    
    <!-- ↓検索ボックスを追加! -->
    <hero-search></hero-search>
    

ブラウザを更新!
検索ボックスに文字を入力してみよう!
こんな感じで表示されたらOK!
(ひさびさのスクショ!)
dart_13

フィニィーーーーーッシュ!!!!(マリ男の声で)

おわりに

いかがだったでしょうか。

今回の感想は、AngularDartの公式チュートリアル長すぎだろ...。の一言に尽きます。

そのぶん一通り説明してくれるので助かりますけどね。

Flutterもそうですが、公式のドキュメントが充実していると運営側(DartはGoogle先生)の本気感を感じられて、謎の信用というか未来性を感じてしまうKentaurosです。

何はともあれ、ここまで読んで頂きありがとうございます。
お疲れ様でした。

ご指摘などはTwitterで受け付けております。そちらもぜひ。

あなたのWeb開発力向上に貢献できれば幸いです
Kentaurosより