Using retrofit2 in Android Studio - 51bootcamp/common GitHub Wiki

1. Why retrofit2?

REST API์™€ ์ƒ๋‹นํžˆ ์œ ์‚ฌํ•˜๊ฒŒ interface๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ž๋™์œผ๋กœ Json์„ parsing์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

2. retrofit2 official tutorial

add this line in build.gradle

    implementation 'com.squareup.retrofit2:retrofit:2.5.0'

make interface

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

make retrofit object which implements of GitHubService interface

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.

Call<List<Repo>> repos = service.listRepos("Ricci0708");

3. ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ

์œ„์— tutorial๋งŒ์œผ๋กœ๋Š” REST API๋ฅผ ์ด์šฉํ•œ JSONFILE์„ ํŒŒ์‹ฑํ•˜๊ฑฐ๋‚˜ ์ž๋™์œผ๋กœ ํด๋ž˜์Šค object๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ์–ด๋ ต๋‹ค. ์ด๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” build.gradle์—

    //for manage json file
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
    //okhttp
    implementation "com.squareup.okhttp3:okhttp:3.12.1"
    implementation 'com.squareup.okhttp3:logging-interceptor:3.12.1'

gson์€ google์—์„œ ๋งŒ๋“  Java serialization/deserialization library์ด๊ณ  okhttp๋Š” retrofit์ด http๋ฅผ ์ด์šฉํ•ด์„œ ํ†ต์‹ ์„ ํ•˜๋Š”๋ฐ ๊ทธ๋ƒฅ retrofit2๋กœ ๋งŒ์œผ๋กœ๋Š” ๋กœ๊น…์„ ํ•˜๋Š” ๊ฒƒ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์„œ logging์„ ์œ„ํ•ด์„œ implement๋ฅผ ํ•œ๋‹ค.

RetrofitInstance๋ผ๋Š” ์ž๋ฐ” ํŒŒ์ผ์„ ๋งŒ๋“ ํ›„ ์•„๋ž˜์˜ ๋‚ด์šฉ์„ ๋ถ™์—ฌ๋„ฃ๋Š”๋‹ค.

public class RetrofitInstance {
    private static Retrofit retrofit;
    private static OkHttpClient client;
    //URL can be easily changed
    private static final String BASE_URL = "http://10.0.2.2:8000/api/v1/";

    public static Retrofit getRetrofitInstance() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(getOkhttpClient())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
    private static OkHttpClient getOkhttpClient(){
        if (client == null){
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
        }
        return client;
    }
}

์ด ์ž๋ฐ” ํŒŒ์ผ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” retrofit object๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š” ์ž๋ฐ” ํŒŒ์ผ์ด๋‹ค. getRetrofitInstance๋Š” retrofit object๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ๊ทธ object๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์—†๋‹ค๋ฉด ์ƒ์„ฑ์„ ํ•œํ›„์— returnํ•œ๋‹ค. ์ด์™€ ๊ฐ™์ด ํ•˜๋Š” ์ด์œ ๋Š” retrofit object๋ฅผ ํ•„์š”์—†์ด ๋งŽ์ด ์ƒ์„ฑํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ์ด๋‹ค. ๋˜ํ•œ BASE_URL์ด๋ผ๋Š” String๋งŒ์„ ๋ฐ”๊ฟˆ์œผ๋กœ์จ ์š”์ฒญํ•˜๋Š” ์„œ๋ฒ„์˜ ์ฃผ์†Œ๋ฅผ ์‰ฝ๊ฒŒ ๋ฐ˜ํ™˜๊ฐ€๋Šฅํ•˜๋‹ค. ์ฃผ์˜!BASE_URL๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ๋ฐ˜๋“œ์‹œ 'http://'๋กœ ์‹œ์ž‘ํ•˜๊ณ  '/'๋กœ ๋์ด ๋‚˜์•ผํ•œ๋‹ค. ๋˜ํ•œ ์•ˆ๋“œ๋กœ์ด๋“œ ์ŠคํŠœ๋””์˜ค ์—๋ฎฌ๋ ˆ์ดํ„ฐ์˜ loopback ipaddress๋Š” 10.0.2.2์ด๋‹ค. localhost๋ฅผ ์„œ๋ฒ„์™€ ํ†ต์‹ ์„ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” 10.0.2.2๋กœ ์„ค์ •์„ ํ•ด์•ผ ๊ฐ€๋Šฅํ•˜๋‹ค.

๊ทธ ํ›„ project์— interface๋ฅผ ํ•˜๋‚˜ ์ƒ์„ ํ•œ๋‹ค. common๊ณผ ๊ฐ™์€๊ฒฝ์šฐ์—๋Š”

public interface ApiInterface {
    @GET("class/{date}/")
    Call<ClassList> getClassList(@Path("date") String date);
    @GET("class/{date}/{classID}")
    Call<Class> getClassInfo(@Path("date") String date,
                             @Path("classID") Integer classID);
    @POST("login/")
    Call<User>login(@Body User user);
}

์ด interface๋ฅผ ๋ณด๋ฉด REST API์™€ ์ƒ๋‹นํžˆ ์œ ์‚ฌํ•œ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ REST API๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด์„œ ์‰ฝ๊ฒŒ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

@GET("class/{date}/")
Call<ClassList> getClassList(@Path("date") String date);

์œ„์˜ API๋Š” get method๋กœ class/{date}์— request๋ฅผ ๋ณด๋‚ธ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค. {date}์˜ ๊ฒฝ์šฐ์—๋Š” @Path("date") String date์— ์˜ํ•ด์„œ ์ฃผ์–ด์ง€๋Š” ์ธ์ž์— date๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.Call๋Š” ์ด Call์˜ return type์€ ClassList๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค. ๋งŒ์•ฝ ๋‹จ์ˆœํžˆ ์ธ์ž๋ฅผ ํ•˜๋‚˜๋งŒ์„ ๋ฐ›์ง€ ์•Š๊ณ  ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ๋ฐ›๋Š”๋‹ค๋ฉด

@GET("class/{date}/{classID}")
Call<Class> getClassInfo(@Path("date") String date,
                         @Path("classID") Integer classID);

์œ„์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ๋œ๋‹ค.

@POST("login/")
Call<User>login(@Body User user);

์œ„์˜ API๋Š” POST method๋กœ login/์— request๋ณด๋‚ธ ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๋Š” ๊ฒƒ์ด๊ณ , @Body์—๋Š” User user๋ฅผ ๋„ฃ์–ด์„œ ์š”์ฒญ์„ ํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค. retrun type๋˜ํ•œ User๋ผ๋Š” ์˜๋ฏธ์ด๋‹ค.

์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ๊ฐ๊ฐ API๋ฅผ ํ†ตํ•ด์„œ ๋ฐ›๋Š” JSON Object๋ฅผ ํŒŒ์‹ฑํ•ด์„œ ๋‹ด์„ ํด๋ž˜์Šค๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์˜ˆ๋ฅผ ๋“ค์ž๋ฉด common api์ค‘ ํ•˜๋‚˜์ธ @GET api/v1/class/$date/$classID/ ์˜ ๊ฒฝ์šฐ์—๋Š” return ๋˜๋Š” Json object๋Š”

{
    "classID": 1,
    "className": "Booktalking with Author",
    "expertName": "lkh",
    "minGuestCount": 4,
    "maxGuestCount": 8,
    "availableTimeTable": [
        {
            "timeTableIdx": 11,
            "startTime": "03 : 00 PM",
            "endTime": "05 : 00 PM",
            "isBooked": false
        },
        {
            "timeTableIdx": 12,
            "startTime": "05 : 00 PM",
            "endTime": "07 : 00 PM",
            "isBooked": false
        }
    ]
}

์˜ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ์ด๋ฅผ ๋‹ด์„ Class๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ๋งค์šฐ ๊ท€์ฐฎ๊ณ  ์งœ์ฆ๋‚˜๋Š” ์ผ์ด๋‹ค. ์ด๋ฅผ ์ž๋™์œผ๋กœ ํ•ด์ฃผ๋Š” site๊ฐ€ ์žˆ๋‹ค! http://www.jsonschema2pojo.org/ ์— ๋“ค์–ด๊ฐ€์„œ ๋ฐ˜ํ™˜๋ฐ›๋Š” json response๋ฅผ ๋„ฃ์œผ๋ฉด java class๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ ์ƒ์„ฑ๋œ Class ํด๋ž˜์Šค๋Š”

public class Class implements Parcelable {
    @SerializedName("classID")
    @Expose
    private Integer classID;
    @SerializedName("className")
    @Expose
    private String className;
    @SerializedName("expertName")
    @Expose
    private String expertName;
    @SerializedName("minGuestCount")
    @Expose
    private Integer minGuestCount;
    @SerializedName("maxGuestCount")
    @Expose
    private Integer maxGuestCount;
    @SerializedName("availableTimeTable")
    @Expose
    private List<TimeTable> availableTimeTable = new ArrayList<TimeTable>();
    @SerializedName("coverImage")
    @Expose
    private List<String> coverImage = new ArrayList<String>();

์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ API๋กœ ์š”์ฒญ์„ ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ œ๋Œ€๋กœ ์ฃผ๊ณ  ๋ฐ›์Œ์„ ํ™•์ธํ• ์ˆ˜์žˆ๋‹ค!

์‹ค์ œ Common์—์„œ retrofit์„ ํ†ตํ•ด์„œ ์ฃผ๊ณ  ๋ฐ›๋Š” code์˜ ์˜ˆ์‹œ์ด๋‹ค.

ApiInterface service = RetrofitInstance.getRetrofitInstance()
                        .create(ApiInterface.class);
                //TODO(woonjin): get the today's date and change the arguments
Call<Class> request = service.getClassInfo(selectedDate,
        classList.getClassList().get(position).getClassID());
request.enqueue(new Callback<Class>() {
    @Override
    public void onResponse(Call<Class> call, Response<Class> response) {
        Context context = container.getContext();
                selectedClass = response.body();
                Log.d("ClassName", selectedClass.getClassName());
    }
    @Override
    public void onFailure(Call<Class> call, Throwable t) {
    //TODO (woongjin) : how to deal with failure
    }
});
D/OkHttp: --> GET http://10.0.2.2:8000/api/v1/class/2019-01-17/4
D/OkHttp: --> END GET
D/OkHttp: <-- 200 OK http://10.0.2.2:8000/api/v1/class/2019-01-17/4 (10ms)
    Date: Thu, 17 Jan 2019 20:54:03 GMT
    Server: WSGIServer/0.2 CPython/3.7.0
    Content-Type: application/json
    X-Frame-Options: SAMEORIGIN
    Content-Length: 223
    {"classID": 4, "className": "A Wine Tasting", "expertName": "jmj", "minGuestCount": 6, "maxGuestCount": 10, "availableTimeTable": [{"timeTableIdx": 9, "startTime": "09 : 00 AM", "endTime": "11 : 00 AM", "isBooked": false}]}
    <-- END HTTP (223-byte body)
D/ClassName: A Wine Tasting

์ด๋Ÿฐ์‹์œผ๋กœ ๋กœ๊ทธ๊ฐ€ ์ฐํžŒ๋‹ค!

4. ๋” ์ƒ๊ฐํ•ด๋ณผ๊ฒƒ

๋ฌผ๋ก  ์ด๊ฒƒ์€ ๊ฑฐ์˜ ๊ฐ„๋‹จํ•˜๊ฒŒ๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ•œ๊ฒƒ์ด๋‹ค. ์›๋ž˜๋ผ๋ฉด on failure์—์„œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค. On failure์˜ ๊ฒฝ์šฐ์—๋Š” ์„œ๋ฒ„๊ฐ€ ์•„์˜ˆ ์‘๋‹ต์ด ์—†๊ฑฐ๋‚˜, jsonํŒŒ์ผ์„ ํŒŒ์‹ฑ์— ์‹คํŒจํ–ˆ์„๋•Œ, ๋“ค์–ด๊ฐ€๋Š” ๋ธ”๋Ÿญ์ด๋‹ค. ์„œ๋ฒ„์—์„œ 400๋ฒˆ๋Œ€์˜ response๋ฅผ ๋ณด๋‚ด๋”๋ผ๋„ on success๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— on success๋‚ด์—์„œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ๋˜ํ•œ enqueue๋Š” ๋น„๋™๊ธฐํ™” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•จ์œผ๋กœ ๋งŒ์•ฝ ๋™๊ธฐํ™” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋ฉด ์กฐ๊ธˆ๋” ์•Œ์•„๋ด์•ผํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ๊ฐ™์ด ๊ณต๋ถ€ํ•ด๋ด์š”~

โš ๏ธ **GitHub.com Fallback** โš ๏ธ