ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

OkHttp3 + Retrofit2 + gson + RxJava + RxLifecycle + retrolambda を使って簡単にAPIを叩く

こんにちは。スマートフォンアプリエンジニアのまさです。
最近Androidの開発をすることになったために、同僚に助けて頂きながら勉強しました。 非同期、安全に、簡単にAPIを叩くために、ライブラリを使って
https://httpbin.org/get
を叩き、jsonデータを取得する方法をご紹介します。

まずは各種ライブラリを使えるようにするために

build.gradle

buildscript {
    dependencies {
        classpath 'me.tatarka:gradle-retrolambda:3.3.1'  // 追加
    }
}

app/build.gradle

apply plugin: 'me.tatarka.retrolambda'  // 追加

android {
  ...
  
   // 追加
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'

    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'io.reactivex:rxjava:1.2.1'
    compile 'com.trello:rxlifecycle:1.0'
    compile 'com.trello:rxlifecycle-components:1.0'

    compile 'com.squareup.okhttp3:okhttp:3.5.0'

    compile 'com.google.code.gson:gson:2.8.0'

    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'

    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'io.reactivex:rxjava:1.2.1'

    retrolambdaConfig 'net.orfjackal.retrolambda:retrolambda:2.3.0'
}

のように編集して、Sync Now します。 これで、今回使うライブラリを使えるようになります。

それでは適当にActivityを作って実装してみます。

package com.unifa_e.studyrx;

import android.os.Bundle;
import android.util.Log;

import com.google.gson.annotations.SerializedName;
import com.trello.rxlifecycle.components.support.RxAppCompatActivity;

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import rx.Observable;
import rx.Observer;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;


public class MainActivity extends RxAppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private static final String accessServer = "https://httpbin.org/";
    private OkHttpClient mClient = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        access();
    }

    private void access() {
        requestGet()
                .compose(bindToLifecycle())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Res>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted");
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        Log.e(TAG, "Error : " + throwable.toString());
                    }

                    @Override
                    public void onNext(Res res) {
                        Log.d(TAG, res.toString());
                    }
                });
    }

    private Observable<Res> requestGet() {
        Retrofit retrofit =
                new Retrofit.Builder()
                        .baseUrl(accessServer)
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                        .client(mClient)
                        .build();

        return retrofit.create(SampleApi.class).get();
    }
}

class Res {
    String url;
    String origin;
    Headers headers;

    @Override
    public String toString() {
        return "Res{" +
                "url='" + url + '\'' +
                ", origin='" + origin + '\'' +
                ", headers=" + headers +
                '}';
    }
}

class Headers {
    @SerializedName("Host")
    String host;
    @SerializedName("Connection")
    String connection;

    @Override
    public String toString() {
        return "Headers{" +
                "host='" + host + '\'' +
                ", connection='" + connection + '\'' +
                '}';
    }
}

interface SampleApi {
    @GET("/get")
    Observable<Res> get();
}

※今回は備忘録的にご紹介のため、1ファイル内にシンプルに実装しており、アクセッサやファイルを分けるなどの処理を行なっておりません。

これで実行いただくと、https://httpbin.org/get APIをrequestし、responseのjsonデータをパースしてResクラスへデータを入れ込み、ログを表示します。

簡単な説明

まずは responseに帰ってくるjsonをクラスへデータを入れ込むためのクラスである、 Resクラスと、Headersクラスを作成します。
@SerializedName をつけることで、サーバサイドとメンバ変数名が違う場合には、紐付けを行うことができます。
※今回はサンプルなので、すべてのプロパティを実装していません。

getResメソッドの.addConverterFactory(GsonConverterFactory.create()) をつけることでJSONパーサを行ないます。
Rxをつかい、Observableを返すよう実装しています。

onCreateからAPIを叩くObservableSubScribeして実行します。

AppCompatActivity ではなく、RxAppCompatActivity を継承することで、 RxLifecycle.compose(bindToLifecycle()) を処理チェーンに入れるだけで、利用できます。
いちいちライフサイクルを気にせずにRxを使えるので、すでに存在しないActivityのUIにアクセスするというようなことがなくなって、安全で簡単ですね。

最後に onNextに渡ってきたResクラスを表示させています。

如何でしたでしょうか。最初はとっつきにくかったですが、Rxを使うと見た目もよくなりますし、非同期処理の敷居が下がってとてもいいと思いました。

net/httpでHTTP API Clientを作ってみました

こんにちは、チョウです。

最近HTTP APIを利用できるようにクライアントを作ってみました。そこで使ってるHTTP Clientのライブラリはnet/httpです。ほかにいろいろライブラリがありますが、依存を増やしたくないため、見送りました。

HTTP API Clientで対応したいケース

  • GET with query
  • POST application/json
  • PUT/PATCH application/json
  • DELETE
  • timeout

つまりRESTful HTTP APIでよく見られるケースです。最後のtimeoutはリクエストの内容ではなくて、クライアントは一定の時間内で返せる機能です。

net/httpのドキュメントを見ると、すぐできそうなのはGETのケースです。

uri = URI('http://example.com/index.html')
params = { :limit => 10, :page => 3 }
uri.query = URI.encode_www_form(params)

res = Net::HTTP.get_response(uri)
puts res.body if res.is_a?(Net::HTTPSuccess)

そしてPOSTの方はformが対応してるっぽいです。

uri = URI('http://www.example.com/search.cgi')
res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
puts res.body

application/jsonの場合は基本リクエストのbodyに内容を書き込むので… やはりnet/httpのソースコードを読むしかないのか。 いろいろ調べたところ、このコードでいけそうです。

uri = URI('http://www.example.com/search.cgi')
request = Net::HTTP::Post.new(uri.path, {'Content-Type' => 'application/json'})
request.body = {foo: 1, bar: 'a'}.to_json
response = Net::HTTP.start(uri.host, uri.port) do |http|
  http.request(request)
end

PUTとPATCHは同じ方法で出来ます。DELETEのリクエストにはbodyがないため、GETと似たようなやり方です。

最後のtimeoutはNet::HTTP.startのblockで、httpで設定を変えるらしいです。でもopen_timeoutとread_timeout2つのtimeout設定に分けてます。open_timeoutはsocket接続する時使われるもので、read_timeoutはデータを送ったあとサーバーのレスポンスを待つtimeoutです。

response = Net::HTTP.start(uri.host, uri.port) do |http|
  http.open_timeout = 1
  http.read_timeout = 2
  http.request(request)
end

これでHTTP APIのクライアントに必要なものが揃いましたので、クライアントを作ってみます。

実際のコードではnamespaceがありますが、ここでは省略しました。

まずRequestです。

class Request
  attr_accessor :method # Symbol
  attr_accessor :path # String e.g /bar
  attr_accessor :headers # Hash<String, String>
  attr_accessor :query_params # Hash<String, String/Array<String>>
  attr_accessor :form_params # Hash<String, String/Array<String>>
  attr_accessor :body # String

  def initialize(hash = {})
    @method = hash[:method]
    @path = hash[:path]
    @headers = hash[:headers] || {}
    @query_params = hash[:query_params] || {}
    @form_params = hash[:form_params] || {}
    @body = hash[:body]
  end

  # @return [String]
  def content_type
    @headers['Content-Type']
  end

  # @return [Boolean]
  def has_query_param?
    @query_params.any?
  end

  # @return [Boolean]
  def has_form_param?
    @form_params.any?
  end
end

ここで、query_params, form_paramsとbodyで別々のケースを対応する想定です。

  • query_params => GET
  • form_params => POST form
  • body => POST/PUT/PATCH json

つぎはClientです。

class Client
  def initialize(client_config)
    @client_config = client_config
  end

  def execute(request)
    net_http_request = build_net_http_request(request)
    net_http_response = ::Net::HTTP.start(
        @client_config.host, @client_config.port,
        use_ssl: (@client_config.schema == 'https')) do |http|
      http.open_timeout = @client_config.open_timeout
      http.read_timeout = @client_config.read_timeout
      http.request(net_http_request)
    end
    Response.new(net_http_response)
  end

  private

  def build_net_http_request(request)
    klass = determine_net_http_request_class(request.method)
    r = klass.new(build_path_with_query(request), @client_config.default_headers.merge(request.headers))
    if r.request_body_permitted?
      if request.content_type == 'application/x-www-form-urlencoded' || request.content_type == 'multipart/form-data'
        r.set_form(request.form_params, request.content_type)
      else
        r.body = request.body
      end
    end
    r
  end

  def build_path_with_query(request)
    if request.has_query_param?
      query_params = request.query_params.reject {|k, v| v.nil?}
      "#{request.path}?#{::URI.encode_www_form(query_params)}"
    else
      request.path
    end
  end

  def determine_net_http_request_class(method)
    case method
      when :get
        ::Net::HTTP::Get
      when :post
        ::Net::HTTP::Post
      when :put
        ::Net::HTTP::Put
      when :patch
        ::Net::HTTP::Patch
      when :delete
        ::Net::HTTP::Delete
      else
        raise ::ArgumentError, "unsupported http method #{method}"
    end
  end
end

POSTのform、特にファイルが入ってるケースは直接bodyを入れることができないので、set_formを通してboundaryなどを付け加えてもらう必要があります。

最後はResponseとClientConfigです。

class Response
  # @param [Net::HTTPResponse] net_http_response
  def initialize(net_http_response)
    @net_http_response = net_http_response
  end

  # @return [Fixnum]
  def status_code
    Integer(@net_http_response.code)
  end

  # @return [Hash<String,String>]
  def headers
    headers = {}
    @net_http_response.each_header do |k, vs|
      headers[k] = vs
    end
    headers
  end

  def content_type
    @net_http_response.content_type
  end

  # @return [String]
  def body_as_string
    @net_http_response.body
  end
end

特に注意すべきなのは、net/httpのresponseで取ったstatus_codeのタイプは数値ではなく、文字列です。アプリケーションのコードは基本数値を想定してるので、予め数値に変換するほうをおすすめします。あとなぜかResponseから直接全部のHEADERを取れなくて、コピーの方法でなんとかなりました。

class ClientConfig
  attr_reader :schema # String
  attr_accessor :host # String e.g www.example.com
  attr_accessor :port # Fixnum e.g 80, 443

  attr_accessor :open_timeout # Fixnum
  attr_accessor :read_timeout # Fixnum
  attr_accessor :logger # Logger

  attr_accessor :default_headers # Hash

  def initialize
    @schema = 'http'
    @host = nil
    @port = 80

    @open_timeout = 0
    @read_timeout = 0
    @logger = ::Logger.new(STDOUT)

    @default_headers = {'User-Agent' => "MyApiClient 0.0.1"}
  end

  # @param [String] schema e.g http, https
  def schema=(schema)
    @schema = schema.downcase
  end
end

ClientConfigはとくに複雑ではありません。

実際の使い方

# どっかてclientを初期化
client_config = ClientConfig.new
client_config.host = 'www.example.com'
client = Client.new(client_config)

request = Request.new(method: :get, path: '/')
response = client.execute(request)
puts response.body_as_string

もっと使いやすいようにmoduleを用意しました。

module HttpClientSupport
  attr_writer :http_client

  def execute_http_request(hash)
    request = Request.new(hash)
    @http_client.execute(request)
  end

  def http_get(path, query_params = {})
    request = Request.new
    request.method = :get
    request.path = path
    request.query_params = query_params
    @http_client.execute(request)
  end

  def http_post(path, form_params)
    request = Request.new
    request.method = :post
    request.path = path
    request.form_params = form_params
    request.headers = {'Content-Type' => 'application/x-www-form-urlencoded'}
    @http_client.execute(request)
  end

  def http_post_json(path, json)
    request = Request.new
    request.method = :post
    request.path = path
    request.body = json
    request.headers = {'Content-Type' => 'application/json'}
    @http_client.execute(request)
  end

  def http_put_json(path, json)
    request = Request.new
    request.method = :put
    request.path = path
    request.body = json
    request.headers = {'Content-Type' => 'application/json'}
    @http_client.execute(request)
  end
end

これでincludeしたクラスで

response = http_get('/')

だけで使えます。

ご参考になれば幸いです。

Rubyのスレッドで並列化するのに向いている処理を調べてみる

こんにちは、夏に向けて腹筋強化中のWebエンジニア、本間です。

はじめに、この開発者ブログを開設してから、半年が経過しました🎉

気づけばエントリー数も20を超え、開発者ブログっぽくなってきなーと感じております。 この勢いを続けていけるよう、会社としても個人としてもこれからも頑張っていく所存です。

話は変わりまして、今回のブログでは「Rubyのスレッドで並列化するのに向いている処理」を簡単に調べたので、メモがわりに残しておこうと思います。

調査を実施した理由として、バックグラウンドジョブを動かすGemの1つに sidekiq という有名なGemがあります。 このsidekiqの特徴の1つに「マルチスレッドで動作する」というものがあります。 一方、MRI(CRuby)にはGVL(Global VM Lock)、またはGIL(Global Interpreter Lock)と呼ばれる排他制御の機構があり、「同時に実行できるスレッドは1スレッドのみ」という制限がかかっています。 このロックにより、sidekiqでは複数スレッド立ち上げても処理性能が向上しないのでは、と疑問に思ったことがきっかけです。

ただし、GVLはIO関連の処理が実行されている間は解放されて他スレッドの処理を実行できる、と聞いたこともあります。 どの処理だとマルチスレッドにしてもGVLの影響を受けずに並列化できるのか、調査してみました。

続きを読む

AWS Lambda から Google Cloud Pub/Sub へ投入してみる(Python)

おはようございます。
こんにちは。
こんばんは。

最近の日差しの強さで肌が焼けるなと感じる地黒のインフラ担当すずきです。

本当に暑くなってきましたね。でも冷房で寒くてつらい…。
皆様、温度差で体を壊さないようにしていただけたらと思います。

さて本題、ユニファのシステムは基本的にAWSを利用しているのですが、BigQueryとかも利用したいのでGCPを利用しようという流れが来てます。
S3に溜まってるログをGCS経由で送ればいいじゃんってのもあるのですが、どうせならCloud Pub/Subとか使ってみようって事でLambdaからCloud Pub/Subを触ってみました。

続きを読む

iOSのCoreBluetoothの実装をしてみる

iOSエンジニアのしだです。
最近、Bluetooth Low Energy(BLE)をつかう場面が多くなってきたので、iOSでBLEを利用する方法を勉強中であります。 特に目新しい話ではないのですが、iOSでCentral側とPeripheral側の実装を試してみたので共有したいと思います。

続きを読む

エンジニアから見た新保育指針

こんにちは。エンジニアの田渕です。 5月も中旬を過ぎたのに、東京はまだなんだか微妙に肌寒い日々。。。毎年こんなもんだったっけ?と思いながら、過ごしています。

さて、すっかり定着してきたユニファのエンジニアブログですが、本日はユニファらしい(?)記事を一つ書いてみようかと思います。

続きを読む

Cordova で React VR をスマホアプリ化する

こんにちは。田中剛です。

今回は先日リリースされたReact VR を Cordovaで スマホアプリ化 する話を書いてみます。

え? ユニファでVR ? (||゚Д゚)

と思った方もいらっしゃるかもしれません。

はい、VRは私の趣味100%で業務には1%も関係ありません。。

が、Cordova や ReactNative などのハイブリッドアプリのフレームワークは何かのプロジェクトで使いたいと密かに思っています。 ( ̄ー ̄)

今回の記事を書くにあたって以下の記事がとても参考になりました。m(__)m qiita.com

では、やり方を順に説明していきます。

インストール
  1. cordovaのインストール

     $ npm install -g cordova
    
  2. React VRのインストール

     $ npm install -g react-vr-cli
    
プロジェクトの作成
  1. cordovaプロジェクトの作成

    $ cordova create path/to/your/dir com.example.hello.reactvr HelloCordovaReactVr
    

    createのパラメータは corodva create ディレクトリ 識別子 アプリ名 です。

    識別子でハイフンやアンダースコアを使うとハマるので要注意です。(AppIDとしてiOSではアンダースコア、Androidではハイフンを使えないため)

  2. iOSやAndroidのプラットフォームを追加

     $ cordova platform add ios --save
     $ cordova platform add android --save
    
  3. React VRのプロジェクトを作成

    corodvaプロジェクトのルートディレクトリでReact VRのプロジェクトを作成します。

     $ react-vr init react_vr
    

    initのパラメータは react-vr init アプリ名です。

アプリのビルドとコードの修正
  1. React VRアプリのコード(js)の修正

    index.vr.js をエディタで編集します

    せっかくなので extract-streetviewというツールを使って弊社オフィス地点のストリートビューのパノラマ画像をダウンロードして使ってみます。

     <View>
       <Pano source={asset('unifa_office_sv_pano.jpg')}
         style={{
           transform: [
             {rotateY : -90}
           ] }}
       />
       <Text
         style={{
           backgroundColor: '#77ff79',
           fontSize: 0.5,
           layoutOrigin: [0.5, 0.5],
           paddingLeft: 0.2,
           paddingRight: 0.2,
           textAlign: 'center',
           textAlignVertical: 'center',
           transform: [{translate: [0, 0, -3]}],
         }}>
         Welcome to Unifa
       </Text>
     </View>
    

    ちなみに extract-streetview は次のような感じで使えます。

     $ extract-streetview 35.7004793,139.7768184 -f jpg -q 100 -z 2 -o unifa_office_sv_pano.jpg
    
  2. React VRアプリをビルド

     $ cd react_vr
     $ npm run bundle
    
  3. React VRアプリのコード(html)の修正

    vr/index.html をエディタで編集します

    1. index.htmlにcordova.jsを追加

      <body>
            .....
           <script type="text/javascript" src="cordova.js"></script>
      </body>
      
    2. ビルドした js を使うようにパスを修正

      2箇所修正します。.jsをつけないとNGのようです

      client.bundle.js <script src="./build/client.bundle.js?platform=vr"></script>

      index.bundle.js './build/index.bundle.js?platform=vr&dev=true'

    3. static_assets が index.html があるディレクトリから見えるようにする

       ln -s ../static_assets .
      
    4. assetsRootのパラメータを追加

       ReactVR.init(
           // When you're ready to deploy your app, update this line to point to
           // your compiled index.bundle.js
           './build/index.bundle.js?platform=vr&dev=true',
           // Attach it to the body tag
           document.body,
           { assetRoot: 'static_assets' }  // 追加
         );  
      
  4. cordovaのwwwディレクトリがReact VRのコンテンツを参照するようにする

    cordovaプロジェクトのルートディレクトリに移動して

    $ mv www www.orig
    $ ln -s react_vr/vr www
    
  5. cordovaアプリをビルド

     $ cordova build [ios/android]
    

    この状態で $ cordova serve ios をするとブラウザ上で動作確認できます。

    f:id:sanshonoki:20170512101655g:plain

エミュレーターで確認

続いてエミュレーターで動作確認します

  1. iOS

     $ cordova emulate ios 
    

    必要に応じて --target=iPhone-6sのようにターゲット端末をオプションで指定します。

    f:id:sanshonoki:20170512102602p:plain:w250

    おお、出ました! ただし、シミュレーターではジャイロが使えないため真下向きのままから動きません… (´Д`。)

  2. Android

    Android は 最初にエミュレーターを起動してから コマンドを打ってください

     $ cordova emulate android
    

    f:id:sanshonoki:20170506152630p:plain:w250

    orz.. AndroidのエミュレーターはWebGLをサポートしてないようです..

実機で確認

手持ちのiPad mini でやってみます。

そのまま、$ cordova run ios すると

 `Code Sign error: No code signing identities found: No valid signing identities (i.e. certificate and private key pair) were found.` 

のエラーが出るので Xcodeのプロジェクトを開きます

$ open platforms/ios/HelloCorodvaReactVr.xcodeproj

Fix issueしてXcode上でビルド、実行します。 f:id:sanshonoki:20170511151122j:plain:w300

アプリは起動できたもののテクスチャが表示されません..。(ノ゚ο゚)ノ
iPad miniが古いせいでしょうか…

Androidは手持ちのNexus5がいつの間にやらUSBデバッグ接続できなくなっていて(なぜ…?)実機で確認できませんでした..

iOS、Androidとも最新の実機では動いてくれると信じています。。

実機で確認(続)

iPad mini の OS を10.3 に アップデートしたら見れるようになりました! ヾ(´▽`)ノ f:id:sanshonoki:20170512141643j:plain

アップデート前は動いてなかったThree.jsのサンプルがOSアップデート後に動くようになったのでそれでWebGL対応を判定できそうです。 https://threejs.org/examples/#webgl_animation_cloth