Ionic, Angularでポケモン検索アプリを作ってみう

~ With PokeAPI ~

皆さん、こんにちは。

新田いおりです。今回は初めてのブログ投稿ということで、

IonicとAngularを使用したポケモン検索アプリを作りたいと思います。

実際はもう作っているのですが、、、w

一から書きながらこの記事を記述していきます。w

初めての記事なので、なれていないことがたくさんですが、多めにみていただきたいです。。💦

今回のブログはIonicアプリの基本の様な物を書きましたが、時間があるときにIonicの開発環境を作成するブログも書きたいと思っています。

Ionicプロジェクトの作成と開発

※今回のアプリケーションはIonicの開発環境はあり、学んでみたいが、何からしたらいいかわからない人や、簡単なアプリを作成して見たい人向けに書いています。

一応GitHubの方にソースコードを載せているので、わからなくなったらここから確認して欲しいです。

今回の開発で完成するのはこのアプリです。(スマホ向けに作成しているのでデザイン等は崩れていると思います。。。💦

https://nittaiori.github.io/IonicPokemonSearch

今回使用するものは

  • Ionic
  • Capacitor
  • Angular
  • PokeAPI

になります。

プロジェクトを作成したいディレクトリに行き下記のコマンドを実行し、プロジェクトを作成します。

ionic start IonicPokemonSearch

※プロジェクトタイプは「tabs」を選択して下さい。

※フレームワークは「Angular」を選択して下さい。

プロジェクトが作成できたらそこのディレクトリに移動して下記のコマンドを入力して下さい。

npm install @ionic/storage-angular

「@ionic/storage-angular」を使用することで、Ionicでローカルストレージを使用することができます。

Webアプリを開発したい人は下記のコマンドを実行してださい。

ionic serve -lc

iOS, Androidアプリを開発したいひとは下記をお願いします。※下記ではiOSのアプリケーションを作成しています。Android向けに開発したい方は「ios」を「android」に書き換えて実行して下さい。

ionic capacitor add ios
ionic capacitor sync ios
ionic capacitor run ios -lc

コマンドを入力すると、

このような画面が表示されて、Twitterなどのような画面の下にタブがある簡単なアプリケーションが作成されます。

PokeAPIとは

今回のアプリケーションに使用するAPIです。

PokeAPIとは無料で使用できるポケモンの情報を取得することができるAPIです。

ごめんなさい。説明が下手で、、、、、💦

内容が気になる方はここ(https://pokeapi.co/)からサイトを見てみて下さい。

プロバイダーの作成

src/appディレクトリの下に「providers」フォルダを作成します。

providersフォルダに「aplistorage.ts」と「api.ts」ファイルを作成して下さい。

aplistorage.ts

import { Injectable } from "@angular/core";
import { Storage } from "@ionic/storage-angular";

@Injectable()
export class AplistorageProvider {
 
  static key = {
    error: 'error',
    search: 'search',
    history: 'history',
  };
 
  constructor(
    private storage: Storage,
  ) {
    this.storage.create();
  }
 
  // ストレージにデータを入れる
  set(key: string, data: any): Promise<any> {
    return new Promise((resolve, reject) => {
      this.storage.set(key, data)
        .then(data => {
          resolve(data);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
 
  // ストレージからデータを取得する
  get(key: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.storage.get(key)
        .then(data => {
          resolve(data);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
 
  // データの削除をする このアプリでは使用しません。
  remove(key: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.storage.remove(key)
        .then(data => {
          resolve(data);
        })
        .catch(error => {
          reject(error);
        });
    });
  }

  // データを全削除する このアプリでは使用しません。
  clear(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.storage.clear()
        .then(data => {
          resolve(data);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
}

このプロバイダーはストレージの操作をするプロバイダーになります。

api.ts

import { Injectable } from "@angular/core";
import { CapacitorHttp, HttpOptions, HttpResponse } from "@capacitor/core";
import { AplistorageProvider } from "./aplistorage";

@Injectable()
export class ApiProvider {
  constructor(
    private aplistorage: AplistorageProvider,
  ) {}

  // apiからデータを取得する
  public get(url: string): Promise<any> {
    return new Promise(async (resolve, reject) => {
      const optinos: HttpOptions = {
        url: url,
      };
      await CapacitorHttp.get(optinos)
        .then((data: HttpResponse) => {
          this.aplistorage.set(AplistorageProvider.key.search, {options: optinos, return: data});
          resolve(data);
        })
        .catch(error => {
          this.aplistorage.set(AplistorageProvider.key.error, error);
          console.error(error);
          reject(error);
        });
    });
  }

  // apiにデータをポストする このアプリでは使用しないです。
  public post(url: string, data: any): Promise<any> {
    return new Promise(async (resolve, reject) => {
      const optinos: HttpOptions = {
        url: url,
        data: data,
      };
      await CapacitorHttp.post(optinos)
        .then((data: HttpResponse) => {
          this.aplistorage.set(AplistorageProvider.key.search, {options: optinos, return: data});
          resolve(data);
        })
        .catch(error => {
          this.aplistorage.set(AplistorageProvider.key.error, error);
          console.error(error);
          reject(error);
        });
    });
  }
}

このプロバイダーはAPIとの通信を操作するプロバイダーになります。

プロバイダーのセット

プロバイダーを作成したらsrc/app/app.module.tsにプロバイダーをセットしてください。

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { Storage } from '@ionic/storage-angular'; // ここを記述

import { AplistorageProvider } from './providers/aplistorage'; // ここを記述
import { ApiProvider } from './providers/api'; // ここを記述

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    Storage, // ここに入れる
    AplistorageProvider, // ここに入れる
    ApiProvider, // ここに入れる
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

ホーム画面と検索画面の作成

今回はテストアプリということで、デフォルトで存在するTab1, Tab2, Tab3を変更して作成します。実際にちゃんとアプリを作成する場合は名前の変更等してください。w

コンスタントファイルの作成

src/app/constant.ts

コンスタントファイルですが、内容が大きいので、ここからダウンロードまたはコピーして貼り付けてください。

タブページの変更

src/app/tabs/tabs.page.html

<ion-tabs>

  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon aria-hidden="true" name="home-outline"></ion-icon>
      <ion-label>Home</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="search">
      <ion-icon aria-hidden="true" name="search-outline"></ion-icon>
      <ion-label>Search</ion-label>
    </ion-tab-button>
  </ion-tab-bar>

</ion-tabs>

src/app/tabs/tabs-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: 'tabs',
    component: TabsPage,
    children: [
      {
        path: 'home', // ここの変更
        loadChildren: () => import('../tab1/tab1.module').then(m => m.Tab1PageModule)
      },
      {
        path: 'search', // ここの変更
        loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
      },
      // ここは使用しないので削除
      // {
      //   path: 'tab3',
      //   loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule)
      // },
      {
        path: '',
        redirectTo: '/tabs/home', // ここの変更
        pathMatch: 'full'
      }
    ]
  },
  {
    path: '',
    redirectTo: '/tabs/home', // ここの変更
    pathMatch: 'full'
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
})
export class TabsPageRoutingModule {}

ここまで記述すると画面下のタブがこのようになります。

ホームページの作成

src/app/tab1/tab1.page.ts

import { Component } from '@angular/core';

import { AplistorageProvider } from '../providers/aplistorage';

import * as constant from 'src/app/constant';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss']
})
export class Tab1Page {
  pokemon_name: any = constant.pokemon_name;
  history: any = [];

  constructor(
    private aplistorage: AplistorageProvider,
  ) {}

  // このページが開かられるたびに呼び出される
  ionViewWillEnter() {
    // ストレージからデータを取得する
    this.aplistorage.get(AplistorageProvider.key.history)
      .then(data => {
        this.history = data;
      })
      .catch(error => {
        console.error(error);
      });
  }
}

src/app/tab1/tab1.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Home
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Home</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-card>
    <ion-card-header>
      <ion-card-title>Pokemon Search App</ion-card-title>
      <ion-card-subtitle>Blog第一弾</ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      <p>
        こんにちは。<br>
        「新田いおり」と言います。<br>
        ポケモンの検索アプリを作成してみました。皆さんがIonicでの開発をする際の参考になれば嬉しいです。<br>
        今後ともよろしくお願いします。
      </p>
      <br> <!-- CSSでデザインを治してね。。w -->
      <p>
        <font color="red">※</font> このアプリは最新のポケモンに対応していません。<br>
        多分レジェンドアルセウスくらいまでだと思われます。。。<br>
        ごめんなさい。言語対応がめんどくさいので、、、、🙇
      </p>
    </ion-card-content>
  </ion-card>

  <ion-item>
    <h1>履歴</h1>
  </ion-item>

  <ion-card *ngFor="let pokemon of history">
    <ion-card-header>
      <ion-card-title>{{pokemon_name[pokemon.name]}}</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      <img src="{{pokemon.sprites.front_default}}">
    </ion-card-content>
  </ion-card>
</ion-content>

ここまで記述するとホーム画面がこのようになります。

検索画面の作成

src/app/tab2/tab2.page.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { ApiProvider } from '../providers/api';

import * as constant from 'src/app/constant';

@Component({
  selector: 'app-tab2',
  templateUrl: 'tab2.page.html',
  styleUrls: ['tab2.page.scss']
})
export class Tab2Page {
  
  types: any[] = constant.types;
  pokemon_name: any = constant.pokemon_name;

  search_result_pokemons = [];

  // search_type -> name, type, number
  search_type: string = 'name';

  input_name_value: string = '';
  input_number_value: number = 1;
  input_type_value: number = 1;

  constructor(
    private api: ApiProvider,
    private router: Router,
  ) {}

  // 検索ボタンをクリックした時
  search() {
    this.search_result_pokemons = [];

    let search_url = 'pokemon/1'
    if (this.search_type === 'name') {
      const english_name = Object.keys(this.pokemon_name).find(key => this.pokemon_name[key] === this.input_name_value);
      search_url = 'pokemon/' + english_name;
    } else if (this.search_type === 'number') {
      search_url = 'pokemon/' + this.input_number_value;
    } else if (this.search_type === 'type') {
      search_url = 'type/' + this.input_type_value;
    }
    const url = constant.urls.api_base + search_url;

    // apiからデータを取得する
    this.api.get(url)
      .then(data => {
        console.log(data);
        if (data.status === 200) {
          if (this.search_type === 'name') {
            if (data.data.name) {
              // 画面の移動
              this.router.navigate(['/detail', {name: data.data.name}]);
            } else {
              alert('ポケモンは見つかりませんでした。');
            }
          } else if (this.search_type === 'number') {
            if (data.data.name) {
              // 画面の移動
              this.router.navigate(['/detail', {name: data.data.name}]);
            } else {
              alert('ポケモンは見つかりませんでした。');
            }
          } else if (this.search_type === 'type') {
            if (data.data.pokemon) {
              this.search_result_pokemons = data.data.pokemon;
            } else  {
              alert("ポケモンは見つかりませんでした。");
            }
          }
        } else if (data.status === 404) {
          alert("ポケモンは見つかりませんでした。");
        }
      })
      .catch(error => {
        console.error(error);
      });
  }

  // ポケモンを選択した時
  clickPokemon(pokemon: any) {
    // 画面の移動
    this.router.navigate(['/detail', {name: pokemon['pokemon']['name']}]);
  }
}

src/app/tab2/tab2.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Search
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Search</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-item>
    <ion-select [(ngModel)]="search_type" placeholder="検索">
      <ion-select-option value="name">名前から</ion-select-option>
      <ion-select-option value="number">番号から</ion-select-option>
      <ion-select-option value="type">タイプから</ion-select-option>
    </ion-select>
  </ion-item>

  <ion-item>
    <ion-input
      *ngIf="search_type === 'name'"
      type="text"
      fill="solid"
      label="名前"
      labelPlacement="floating"
      [(ngModel)]="input_name_value"
    ></ion-input>
    <ion-input
      *ngIf="search_type === 'number'"
      type="number"
      fill="solid"
      label="図鑑番号"
      labelPlacement="floating"
      [(ngModel)]="input_number_value"
    ></ion-input>
    <ion-select
      *ngIf="search_type === 'type'"
      [(ngModel)]="input_type_value"
      placeholder="検索">
      <ion-select-option
        *ngFor="let type of types"
        value="{{type.id}}">
        {{type.name}}
      </ion-select-option>
    </ion-select>
  </ion-item>
  <div class="search-button-wrapper">
    <ion-button (click)="search()">
      <ion-icon name="search"></ion-icon>
      検索
    </ion-button>
  </div>

  <div *ngIf="0 < search_result_pokemons.length">
    <ion-item>
      <h2>検索結果</h2>
    </ion-item>
    <ion-card *ngFor="let pokemon of search_result_pokemons" (click)="clickPokemon(pokemon)">
      <ion-card-header>
        <ion-card-title>
          {{pokemon_name[pokemon['pokemon']['name']]}}
        </ion-card-title>
      </ion-card-header>
    </ion-card>
  </div>
</ion-content>

ここまで記述するとSearch画面がこの様になります。

※まだ詳細画面を作成していないので、検索しても表示はできません。

詳細画面の作成

詳細画面のルートを作成

src/app.app-routing.module.ts

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
  }, // ここの修正
  // ここの追加
  {
    path: 'detail',
    loadChildren: () => import('./tab3/tab3.module').then(m => m.Tab3PageModule)
  },
];
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

ここを記述すると詳細画面に行ける様になります。

詳細画面の作成

src/app/tab3/tab3.page.ts

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { ApiProvider } from '../providers/api';
import { AplistorageProvider } from '../providers/aplistorage';

import * as constant from 'src/app/constant';

@Component({
  selector: 'app-tab3',
  templateUrl: 'tab3.page.html',
  styleUrls: ['tab3.page.scss']
})
export class Tab3Page {
  pokemon_name: any = constant.pokemon_name;
  pokemon: any;

  constructor(
    private api: ApiProvider,
    private aplistorage: AplistorageProvider,
    private activatedRoute: ActivatedRoute,
  ) {
    // Search画面からのデータの取得
    this.activatedRoute.params
      .subscribe((param: any) => {

        const name = param['name'];
        // apiからポケモンの詳細を取得
        this.api.get(constant.urls.api_base + 'pokemon/' + name)
          .then(data => {

            this.pokemon = data.data;
            // ストレージに検索履歴の追加
            this.aplistorage.get(AplistorageProvider.key.history)
              .then(data => {
                const history: any = data || [];
                history.push(this.pokemon);
                this.aplistorage.set(AplistorageProvider.key.history, history);
              })
              .catch(error => {
                console.error(error);
              });
          })
          .catch(error => {
            console.error(error);
          });
      });
  }

  // タイプの表示を英語から日本語に変換
  createTypeHtml(types: any[]): string {
    return types.map(type => type.type.name).join(' : ');
  }
}

src/app/tab3/tab3.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-back-button slot="start"></ion-back-button>
    <ion-title>
      Detail
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">

  <ion-card *ngIf="pokemon">
    <ion-card-header>
      <ion-card-title>{{pokemon_name[pokemon.name]}}</ion-card-title>
      <ion-card-subtitle>
        {{createTypeHtml(pokemon.types)}}
      </ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      <img src="{{pokemon.sprites.front_default}}">
      <p>
        高さ: {{pokemon.height / 10}}cm<br>
        重さ: {{pokemon.weight / 10}}kg<br>
      </p>

      <h1>ステータス</h1>
      <div *ngFor="let stat of pokemon.stats">
        <label>{{stat.stat.name}} : {{stat.base_stat}}</label>
        <ion-range [disabled]="true" value="{{stat.base_stat / 180 * 100}}"></ion-range>
      </div>
    </ion-card-content>
  </ion-card>

</ion-content>

ここまで記述したら詳細画面がこのように作成できていると思います。

最後に

ここまで読んでいただいてありがとうございます。

ここまで記述出来たのであればアプリは完成してると思います。

Ionicの学習お疲れ様でした。

※まだ初心者の方は、コードをみながら、Home画面の履歴からクリックしたときに詳細画面に移動できる様に自分で書いて見てください。💦

自分もブログ初心者ですが、頑張ってこの記事を書きました。デザインや見辛い点がたくさんあると思いますが、指摘等していただけると幸いです。

ありがとうございました。

また、わからないこと等があれば、コメントやTwitterで質問してくれると、時間があるときに返せるとおもいます。

コメントする