Android Studioに追加されたGoogle App Engineテンプレートを試そう 実装編

モバイルファースト室の@sys1yagiです。

Android Studioに追加されたGoogle App Engineテンプレートを試そう 導入編の続きです。今回はCloud Endpointsのテンプレートを使ってAndroid Studio上でTodoアプリを作る例を解説します。

Google App Engineテンプレートの利点

Androidアプリケーションの開発においてGoogle App Engine(以下、GAE)テンプレートの利用には以下の利点があると考えられます。

  • Android Studioでバックエンドも同時に開発できる
  • GAE関連の依存性をGradleで管理できる
  • バックエンドとフロントエンド間のインタフェースの実装が省略できる
  • バックエンドのモデル変更がすぐにフロントエンドに反映されるので迅速なプロトタイピングができる
  • バックエンドはGAEなのでそのまま運用が可能
  • クライアントライブラリでエンドポイントのURLやパスを変更したり、HttpRequestInitializerをセットしてカスタムした通信処理が可能になる。これによりバックエンドの切り替えも可能となる

Google App Engineテンプレートは「バックエンドに何を使うか決まっていない」、「MBaaSでは要件を満たせない」といったケースや「バックエンドを含めてプロトタイピングしたい」といったケースなどに非常に有効な選択肢なのではないかと思います。

Todoアプリケーションを作る

Cloud EndpointsはGAEで動作するので、バックエンドの詳細な作り込みについてはGAEアプリケーションと差がありません。ですのでGAEに関する部分の解説等は省略します。またAndroid側についてもCloud Endpointsのクライアントライブラリを利用する部分以外については省略します。

プロジェクト構成

プロジェクトは以下の構成とします。"app"がAndroidアプリケーションのモジュール、"api"がCloud Endpointsのモジュールです。

f:id:sys1yagi:20140912105217p:plain

TODOのモデル

Todoは以下の構造を用います(と言っても直接JSONを触ることはありません)。

{
  "id": long,
  "text": string,
  "created": long,
  "updated": long,
  "checked": boolean,
}

Objectifyを使ってモデルを定義する

まずはapi側にモデルを定義し、永続化の準備をしましょう。モデルの永続化についてはGAE向けに既に色々なライブラリが存在します。今回はObjectifyを利用します。

Objectifyはbuild.gradleのdependenciesに以下の定義を追加すればOKです。

compile 'com.googlecode.objectify:objectify:5.0.3'

Todoクラスを作成し、Objectifyのアノテーションを使ってモデルを定義します。少し長いように見えますが、フィールドを定義してgetter/setterを生成すればすぐに出来上がります。最下部のcreateTodo()はヘルパーメソッドなのであっても無くても良いです。Objectifyによるモデル定義の詳細についてはEntities - objectify-appengineを参照してください。

package com.sys1yagi.hellocloudendpoints.api;

import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;

import java.util.Date;

@Entity
@Cache
public class Todo {

  @Id
  Long id;
  private String text;
  private long created;
  @Index
  private long updated;
  @Index
  private boolean isChecked;

  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getText() {
    return text;
  }
  public void setText(String text) {
    this.text = text;
  }
  public long getCreated() {
    return created;
  }
  public void setCreated(long created) {
    this.created = created;
  }
  public long getUpdated() {
    return updated;
  }
  public void setUpdated(long updated) {
    this.updated = updated;
  }
  public boolean isChecked() {
    return isChecked;
  }
  public void setChecked(boolean isChecked) {
    this.isChecked = isChecked;
  }
  public static Todo createTodo() {
    long date = new Date().getTime();
    Todo todo = new Todo();
    todo.setCreated(date);
    todo.setUpdated(date);
    todo.setChecked(false);

    return todo;
  }
}

エンドポイントを実装する

あとはエンドポイントの実装です。Objectify用の実装も入っているので少しややこしいですが、それぞれ解説します。

package com.sys1yagi.hellocloudendpoints.api;

import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;

import java.util.Date;
import java.util.List;

import static com.googlecode.objectify.ObjectifyService.ofy;

@Api(name = "todoApi", version = "v1",
    namespace = @ApiNamespace(ownerDomain = "api.hellocloudendpoints.sys1yagi.com",
        ownerName = "api.hellocloudendpoints.sys1yagi.com", packagePath = ""))
public class TodoEndpoint {

  static {
    ObjectifyService.register(Todo.class);
  }

  @ApiMethod(name = "list", httpMethod = "get")
  public List<Todo> list() {
    return ofy().load().type(Todo.class).list();
  }

  @ApiMethod(name = "add", httpMethod = "post")
  public Todo add(Todo todo) {
    todo.setCreated(new Date().getTime());
    return update(todo);
  }

  @ApiMethod(name = "update", httpMethod = "post")
  public Todo update(Todo todo) {
    todo.setUpdated(new Date().getTime());
    Key<Todo> key = ofy().save().entity(todo).now();
    return ofy().load().type(Todo.class).id(key.getId()).now();
  }

  @ApiMethod(name = "delete", httpMethod = "post")
  public void delete(Todo todo) {
    ofy().delete().entity(todo).now();
  }
}

Objectifyのコード

Objectifyを利用するには、ObjectifyServiceクラスのregisterメソッドでモデルをセットする必要があります。今回はTodoEndpointクラスのstaticイニシャライザで以下の様にTodoクラスをセットしています。

static {
  ObjectifyService.register(Todo.class);
}

Objectifyでのモデルの操作はObjectifyクラスを用います。ObjectifyクラスはObjectifyServiceクラスのofyメソッドで取り出せます。ofyメソッドをstatic importしておくと楽です。

import static com.googlecode.objectify.ObjectifyService.ofy;

ofy().load().type(Todo.class).list();
ofy().save().entity(todo).now();
ofy().delete().entity(todo).now();

エンドポイントのコード

エンドポイント側のコードについては前回解説した以上のことはあまりありません。

READ系のエンドポイントは単純にモデルを読み込んで返却すれば、自動的にJSON形式のレスポンスが構築されます。

@ApiMethod(name = "list", httpMethod = "get")
public List<Todo> list() {
  return ofy().load().type(Todo.class).list();
}

上記コードによって以下の様なJSONを返します。

{
 "items": [
  {
   "id": "5639445604728832",
   "text": "出張日程の調整",
   "created": "0",
   "updated": "1411609163221",
   "checked": false,
   "kind": "todoApi#resourcesItem"
  },
  ...
 ]
}

エンドポイントのhttpMethodをpostやputにすると、モデルをそのまま引数に受け取るスタイルで記述できます。これによりクライアント側で、「モデルのオブジェクトを渡してリクエストする」という操作が可能になります。

@ApiMethod(name = "update", httpMethod = "put")
public Todo update(Todo todo) {
  todo.setUpdated(new Date().getTime());
  Key<Todo> key = ofy().save().entity(todo).now();
  return ofy().load().type(Todo.class).id(key.getId()).now();
}

また、エンドポイントとして実装したメソッドを通常のメソッドと同じように使うこともできます。以下の実装ではaddメソッド内でupdateメソッドを呼び出して永続化の処理を共通化しています。

@ApiMethod(name = "add", httpMethod = "post")
public Todo add(Todo todo) {
  todo.setCreated(new Date().getTime());
  return update(todo);
}

クライアント側を実装する

次はクライアントの実装です。クライアントライブラリが自動的に生成されるので、そのライブラリを使ってエンドポイントにアクセスするだけです。

TodoApiクラスを管理するクラスを作る

ライブラリを使ってエンドポイントにアクセスするだけと言ってもいくらかの準備が必要です。エンドポイントにアクセスする為のTodoApiクラスを毎回生成するのは無駄なので、Singletonにしておきます。

import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.extensions.android.json.AndroidJsonFactory;

import com.sys1yagi.hellocloudendpoints.api.todoApi.TodoApi;

public class ApiClient {
  private static final TodoApi INSTANCE = getApiClient();

  public static TodoApi getInstance() {
    return INSTANCE;
  }

  private static TodoApi getApiClient() {
    return new TodoApi.Builder(AndroidHttp.newCompatibleTransport(),
        new AndroidJsonFactory(), null)
        //for genymotion.
        //if you use android emulator, you should replace to "10.0.2.2".
        //.setRootUrl("http://10.0.3.2:8080/_ah/api/")
        .build();
  }
}

エンドポイントへのアクセスをAsyncTaskで包む

エンドポイントへアクセスするメソッドはブロッキングされるので、AsyncTaskなどで包む必要があります。今回は操作毎にAsyncTaskを定義しています。

以下はTodo一覧の取得の例です。エンドポイントではList<Todo>を返していましたがクライアント側ではTodoCollectionクラスを受け取ります。TodoCollection.getItems()List<Todo>を取り出せます。

import com.sys1yagi.hellocloudendpoints.api.todoApi.model.TodoCollection;
import android.os.AsyncTask;
import java.io.IOException;

public class ListTask extends AsyncTask<Void, Void, TodoCollection> {

  @Override
  protected TodoCollection doInBackground(Void... params) {
    try {
      return ApiClient.getInstance().list().execute();
    } catch (IOException e) {
      return null;
    }
  }
}

Todoの追加はaddメソッドにTodoクラスのインスタンスを渡すだけです。エンドポイント側でモデルを受け取るスタイルにしておけばクライアント側で非常にシンプルに記述できます。

import com.sys1yagi.hellocloudendpoints.api.todoApi.model.Todo;
import android.os.AsyncTask;
import java.io.IOException;

public class AddTask extends AsyncTask<Todo, Void, Todo> {

  @Override
  protected Todo doInBackground(Todo... params) {
    Todo todo = params[0];
    try {
      return ApiClient.getInstance().add(todo).execute();
    } catch (IOException e) {
      return null;
    }
  }
}

デプロイする

バックエンド、クライアント共に準備が整いました。実装したバックエンドをデプロイしましょう。

デプロイするにはGoogle Developers ConsoleでプロジェクトIDを作成する必要があります。Google Cloud Platformのページの「今すぐ試す」などからプロジェクトIDを作成のフローに遷移できるのでお試し下さい。

プロジェクトIDを作成したらapiモジュールのsrc/main/webapp/WEB-INF/appengine-web.xmlのapplication要素にプロジェクトIDを記述します。

f:id:sys1yagi:20141016182002j:plain

あとはgradleで以下のタスクを実行すればデプロイできます(初回は認証処理が挟まります)。

./gradlew appengineUpdateAll

デプロイがうまくいけばアプリケーションが動作するようになるはずです。

f:id:sys1yagi:20141016181959j:plain

APIs Explorerでデプロイしたエンドポイントを確認する

デプロイ後はGoogle Developers ConsoleでGAEアプリケーションとして管理できます。また、APIs ExplorerでエンドポイントをWeb上で操作できるようになります。

以下のURLのyour-project-idをプロジェクトIDに置き換えると、定義したエンドポイントの一覧が見れます。

https://apis-explorer.appspot.com/apis-explorer/?base=https://your-project-id.appspot.com/_ah/api#p/

f:id:sys1yagi:20141016182007j:plain

特定のエンドポイントを選択し、リクエストを投げる事もできます。

f:id:sys1yagi:20141016182013j:plain

実際に返却されるJSONを見ることも出来るのでトラブルシューティングに役立ちます。

f:id:sys1yagi:20141016182016j:plain

まとめ

駆け足でしたがCloud Endpointsを使ってTodoアプリを作る方法について解説しました。簡単にバックエンドを含めて開発できる事がわかったかと思います。詳細な実装についてはhttps://github.com/sys1yagi/TodoCloudEndpointsを参照して下さい。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/