Java
Android
OAuth
Retrofit

RetrofitのAPI定義interfaceから通信せずにURLだけ取り出す

More than 1 year has passed since last update.

はじめに

メソッドの返り値をCall<Void>にすればリクエスト取り出せますね。
CallAdapterFactoryを追加したら普通のCall<T>で返せないかと思ってました。

まちがい

例えばこんなinterfaceがあります。

SuzuriAPI.java
public interface SuzuriAPI {

    @GET("/api/v1/activities")
    Single<Activities> activities();

    @GET("/api/v1/activities/unreads")
    Single<Unreads> activitiesUnreads();
}

これはGMOペパボが運営しているSUZURIというサービスのAPIを利用するために書いたものです。
「自分だけのオリジナルグッズが作れる。売れる。買える。」というやつです。
https://suzuri.jp/
https://suzuri.jp/developer
https://twitter.com/suzurijp

こんな風にして使います。

        OkHttpClient client;
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://suzuri.jp/")
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();
        api = retrofit.create(SuzuriAPI.class);
        api.activities().subscribe(new Consumer<Activities>() {
            @Override
            public void accept(Activities activities) throws Exception {
                // レスポンスのjsonがGsonでPOJOに変換されたものをRxのSingle経由で受け取ります。
                // なお、このActivitiesはAndroidのActivityとは無関係です。
            }
        });

実際には、APIを利用する前に、ユーザに認可してもらって、アクセストークンを取得する必要があります。
一部だけ自分でURLを組み立てるのも微妙ですし、Retrofit用のinterfaceは視認性が良いので、
このOAuth部分(認可とアクセストークン取得)についてもAPIの定義と同じように書いておきたいですね。
(APIとは別ファイルに書くべきかもしれませんが、ここではまとめます。)

アクセストークンの取得はAPI部分と同じように書いて、Retrofit経由で通信することができます。

SuzuriAPI.java
public interface SuzuriAPI {

    // アクセストークンの取得もRetrofitで定義
    @FormUrlEncoded
    @POST("/oauth/token")
    Single<AccessToken> accessToken(@Field("grant_type") String grant_type,
                                    @Field("code") String code,
                                    @Field("redirect_uri") String redirect_uri,
                                    @Field("client_id") String client_id,
                                    @Field("client_secret") String client_secret);
}

ところが、OAuthでの認可画面はAndroidだとChromeなどの外部ブラウザで表示します。
つまり、Retrofit経由では通信しません。
そして、Retrofitでは実際のリクエストが隠蔽されています。
そのため、interfaceに定義しておいても組み立てられたURLを取り出すことができません。

SuzuriAPI.java
public interface SuzuriAPI {

    // 認可画面はブラウザで行うため、Retrofitでは通信しない
    @GET("/oauth/authorize")
    Single<?> authorize(@Query("client_id") String client_id,
                        @Query("scope") String scope,
                        @Query("redirect_uri") String redirect_uri,
                        @Query("response_type") String response_type);
}

そこで、Retrofitが実際の通信に使っているOkHttpClientに、Interceptorを追加します。
インターセプトの条件については、ちょっと邪悪ですが、ヘッダーに目印を付けます。

SuzuriAPI.java
    @Headers("X-Dummy: Dummy")
    @GET("/oauth/authorize")
    Single<Dummy> authorize(

Interceptorでそのヘッダーを見つけたら、ダミーのレスポンスを作って返します。

        OkHttpClient client = defaultClient.newBuilder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(@NonNull Chain chain) throws IOException {
                        final Request original = chain.request();
                        if (original.header("X-Dummy") != null) {
                            Dummy dummy = new Dummy(original.url().toString());
                            ResponseBody body = ResponseBody.create(null, gson.toJson(dummy));
                            return new Response.Builder()
                                    .request(original)
                                    .protocol(Protocol.HTTP_1_0)
                                    .code(200)
                                    .message("OK")
                                    .body(body)
                                    .build();
                        }
                        return chain.proceed(original);
                    }
                })
                .build();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://suzuri.jp/")
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();
Dummy.java
public class Dummy {
    public final String value;

    public Dummy(String value) {
        this.value = value;
    }
}

Responseのrequestprotocolcodemessageは必須なので、適当な値を設定しています。
以下のようにして取り出します。

        api.authorize(CLIENT_ID, SCOPE, REDIRECT_URI, "code")
                .subscribe(new Consumer<Dummy>() {
                    @Override
                    public void accept(Dummy dummy) throws Exception {
                        // dummy.valueが組み立てられたURL
                    }
                });

(Retrofitにまともに触る初日なので単に見落としてるだけかもしれません……)

おわりに

冒頭で述べたようにCall<Void>を返すメソッドを定義しましょう