Retrofit อัพเดตครั้งใหญ่ สู่เวอร์ชั่น 2.0 อย่างเป็นทางการ พร้อมฟีเจอร์และการเปลี่ยนแปลงเพียบ

Posted on 6 Sep 2015 12:33 | 38657 reads | 0 shares
 

Retrofit เป็น HTTP Client Library ที่ใช้ติดต่อกับฝั่ง Server ที่ได้รับความนิยมอันดับต้นๆในแอนดรอยด์จากความเรียบง่ายในการใช้และประสิทธิภาพที่เหนือกว่าคนอื่นเขานั่นเอง

อย่างไรก็ตาม จุดอ่อนใหญ่สุดของ Retrofit ก่อนหน้านี้ในเวอร์ชั่น 1.x คือมันไม่สามารถยกเลิก Transaction ที่เรียกออกไปแล้วได้ ต้องรอให้มันทำจนเสร็จอย่างเดียว หรือถ้าจะทำอาจจะต้องใช้ท่ายากคือให้มันไปทำงานบน Thread แล้วแอบฆ่า Thread ทิ้งด้วยมือนั่นเอง

ทางทีมพัฒนาอย่าง Square สัญญาว่าตั้งแต่หลายปีก่อนว่าฟีเจอร์นี้จะทำได้ในเวอร์ชั่น 2.0 แต่จนแล้วจนรอดก็ไม่ออกมาสักที่

ล่าสุดในสัปดาห์ที่แล้ว จู่ๆ Square ก็ทำเซอร์ไพรส์ด้วยการปล่อย Retrofit 2.0 beta 1 ออกมาให้ใช้กันเรียบร้อยแล้ว ! หลังจากลองทดสอบโน่นนี่ก็พบว่าสามารถยกเลิกได้แล้วจริงๆ แต่ท่าการใช้งานก็เปลี่ยนไปด้วยเหมือนกัน รวมถึงมีอะไรที่เปลี่ยนไปอีกในหลายๆเรื่องเลยแหละ ก็เลยเอามาเขียนสรุปให้ฟังสำหรับคนที่กำลังสนใจเจ้าตัวนี้อยู่ครับ

แพคเกจเดิม เวอร์ชั่นใหม่

ตอนนี้ Retrofit 2.0 ยังอยู่ในสถานะ beta ถ้าจะใส่เข้าในโปรเจค ต้องใส่ตามนี้ครับ

compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'

แล้วก็จะได้ Retrofit 2.0 มาใช้ละ

การประกาศ Service แบบใหม่ ไม่มีแบ่งแยก Synchronous หรือ Asynchronous แล้ว

ในการประกาศ interface สำหรับ Service ในเวอร์ชั่น 1.9 หากต้องการประกาศให้เป็น Synchronous จะต้องประกาศแบบนี้

/* Synchronous in Retrofit 1.9 */

public interface APIService {

    @POST("/list")
    Repo loadRepo();

}

หรือถ้าจะสั่งแบบ Asynchronous จะประกาศแบบนี้

/* Asynchronous in Retrofit 1.9 */

public interface APIService {

    @POST("/list")
    void loadRepo(Callback<Repo> cb);

}

แต่ใน Retrofit 2.0 จะมีการประกาศแค่แบบเดียวเท่านั้นคือ

import retrofit.Call;

/* Retrofit 2.0 */

public interface APIService {

    @POST("/list")
    Call<Repo> loadRepo();

}

และการเรียกใช้งานก็จะเปลี่ยนไปด้วยเช่นกันกลายเป็นรูปแบบเดียวกับ OkHttp คือแบ่งเป็นคำสั่ง execute สำหรับเรียกแบบ Synchronous และ enqueue สำหรับการเรียกแบบ Asynchronous ดังนี้

การเรียกแบบ Synchronous

// Synchronous Call in Retrofit 2.0

Call<Repo> call = service.loadRepo();
Repo repo = call.execute();

ซึ่งโค้ดด้านบนจะทำงานแบบ Blocking และไม่สามารถเรียกบน Main Thread ได้ในแอนดรอยด์ เพราะจะเจอ NetworkOnMainThreadException ถ้าจะเรียกคำสั่งด้านบน จะต้องแตก Thread ออกมาเองครับ

การเรียกแบบ Asynchronous

// Asynchronous Call in Retrofit 2.0

Call<Repo> call = service.loadRepo();
call.enqueue(new Callback<Repo>() {
    @Override
    public void onResponse(Response<Repo> response) {
        // Get result Repo from response.body()
    }

    @Override
    public void onFailure(Throwable t) {

    }
});

โค้ดด้านบนจะทำงานใน Background Thread โดยอัตโนมัติ และสามารถรับ Object ผลลัพธ์กลับมาจากคำสั่ง response.body() โดย onResponse และ onFailure จะถูกเรียกใน Main Thread ครับ

เราแนะนำให้ใช้ท่า enqueue จะเหมาะสมกับการทำงานบนแอนดรอยด์ที่สุดครับผม

การยกเลิก Transaction

สาเหตุที่เปลี่ยนวิธีการเรียกเป็น Call ก็เพื่อให้มันยกเลิกการติดต่อได้นั่นเอง โดยสามารถเรียกได้จากคำสั่ง call.cancel() ได้โดยตรงเลย

call.cancel();

เพียงเท่านี้ การติดต่อก็จะถูกยกเลิกแล้วครับ ง่ายเนอะ !

รูปแบบการสร้าง Retrofit Service แบบใหม่ แยก Converter ออกจาก Retrofit แล้ว

ใน Retrofit 1.9 เราสามารถสร้าง Retrofit และได้ GsonConverter มาโดยอัตโนมัติ ก็คือสามารถโหลดข้อมูลเป็น json และแปลงเป็น Data Access Object (DAO) ได้โดยทันที

แต่ใน Retrofit 2.0 ตัว Converter ถูกแยกออกมาจาก Retrofit เรียบร้อย หากประกาศโดยไม่ใส่ Converter ก็จะรับได้แค่ผลลัพธ์ที่เป็น String เท่านั้น และด้วยเหตุผลนี้ Retrofit 2.0 จะไม่มี Gson เป็น Dependency แล้ว จะไม่สามารถเรียกใช้ฟังก์ชั่นใดๆจาก Gson ได้

หากต้องการรับผลมาเป็น json แล้ว Parse อัตโนมัติ ก็ต้องอัญเชิญ Gson Converter เข้ามาในโปรเจคด้วยดังนี้

compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'

และต้องเสียบ Converter เข้ามาผ่านคำสั่ง addConverterFactory ดังนี้

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.nuuneoi.com/base/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        service = retrofit.create(APIService.class);

โดย Converter มีอยู่หลายตัวด้วยกัน เลือกใช้ได้ตามอัธยาศัย

Gson: com.squareup.retrofit:converter-gson
Jackson: com.squareup.retrofit:converter-jackson
Moshi: com.squareup.retrofit:converter-moshi
Protobuf: com.squareup.retrofit:converter-protobuf
Wire: com.squareup.retrofit:converter-wire
Simple XML: com.squareup.retrofit:converter-simplexml

ส่วนตัวชอบรูปแบบนี้มากกว่าครับเพราะมันชัดเจนดีว่า Retrofit เป็นแค่ตัวกลางในการติดต่อและทำงานแยกกันกับ Converter นั่นเอง

การทำ Gson ของตัวเอง

ในกรณีที่ต้องการปรับ Format อะไรใน json เช่นรูปแบบของวันที่ เราสามารถโยน Gson ที่สร้างขึ้นมาเองเข้าไปใน GsonConverterFactory.create() ได้ทันที

        Gson gson = new GsonBuilder()
                .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
                .create();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.nuuneoi.com/base/")
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

        service = retrofit.create(APIService.class);

เรียบร้อยครับ

รูปแบบ URL แบบใหม่ แบบเดียวกับ <a href>

ใน Retrofit 2.0 มีการเปลี่ยนแนวคิดเรื่อง Base URL และ @Url ใหม่ โดยทั้งสองตัวแปรตัวจะถูก Resolve ในวิธีเดียวกับ <a href="..."> ก็คือตามภาพด้านล่างนี้

ดังนั้นใน Retrofit 2.0 เราจะประกาศกันแบบนี้

- Base URL: ลงท้ายด้วย /

- @Url: ไม่ต้องขึ้นต้นด้วย /

เช่น

public interface APIService {

    @POST("user/list")
    Call<Users> loadUsers();

}

public void doSomething() {
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("http://api.nuuneoi.com/base/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    APIService service = retrofit.create(APIService.class);
}

ทีนี้คำสั่ง loadUsers จะไปโหลดจาก http://api.nuuneoi.com/base/user/list ให้ครับ

นอกจากนี้ @Url ใน Retrofit 2.0 ยังสามารถใส่ Full URL ได้ด้วย เช่น

public interface APIService {

    @POST("http://api.nuuneoi.com/special/user/list")
    Call<Users> loadSpecialUsers();

}

คำสั่งด้านบนนี้ก็จะยิงตรงไปที่ URL ที่กำหนดโดยไม่สนใจ Base URL นั่นเอง

สังเกตดูว่ารูปแบบการประกาศ URL จะต่างจากเดิมไปเยอะมาก หากย้ายมาใช้ Retrofit 2.0 ต้องไล่แก้ให้เรียบร้อยด้วยครับ

ใช้ OkHttp เป็นตัวติดต่อหลักอย่างเป็นทางการ

ใน Retrofit 1.9 ตัว OkHttp ถูกกำหนดไว้เป็น Optional หากจะใช้ร่วมกับ OkHttp เราต้องเพิ่ม okhttp เข้ามาใน Dependency เอง

แต่ใน Retrofit 2.0 จะใส่ OkHttp มาเป็น Dependency โดยอัตโนมัติครับ

  <dependencies>
    <dependency>
      <groupId>com.squareup.okhttp</groupId>
      <artifactId>okhttp</artifactId>
    </dependency>

    ...
  </dependencies>

ตัว Retrofit 2.0 ก็เลยจะใช้ OkHttp เป็นตัวติดต่อหลักโดยทันทีไม่ต้องทำอะไรเพิ่มเติม ทั้งนี้ก็เพื่อปรับรูปแบบการเรียกให้เป็น Call ดังที่กล่าวไว้ข้างต้นนั่นเอง

หากผลลัพธ์ Parse ไม่ผ่าน ก็จะยังเข้า onResponse อยู่

ใน Retrofit 1.9 หากผลลัพธ์ที่ได้มาไม่สามารถ parse ให้เป็น Object ประเภทที่กำหนดเป็นผลลัพธ์ได้ มันจะเข้ากรณีของ failure แต่สำหรับ Retrofit 2.0 หากได้ผลลัพธ์มาจาก Server ถึงแม้จะ parse ให้เป็น Object ที่กำหนดไม่ได้ ก็จะเข้า onResponse อยู่ดี เพียงแต่ response.body() จะเป็น null ดังนั้นต้องเช็คให้ดีครับ

ทั้งนี้กรณีที่เกิดปัญหา เช่น 404 Not Found ก็จะเข้า onResponse เช่นกัน เราสามารถดึงข้อความของปัญหาได้จากคำสั่ง response.errorBody().string() ครับ

ตรงนี้ Logic ค่อนข้างต่างจากตอน Retrofit 1.9 ดังนั้นหากย้ายมา 2.0 จะต้องปรับ Logic ให้ตรงด้วยครับ

การไม่ใส่ INTERNET Permission จะ Throw SecurityException

ใน Retrofit 1.9 หากลืมใส่ INTERNET Permission เข้าไปใน AndroidManifest.xml การเรียกแบบ Asynchronous จะเข้าที่คำสั่ง failure ใน Callback พร้อมกับ Error Message ว่า PERMISSION DENIED ไม่ได้เกิด Exception แต่อย่างใด

แต่ใน Retrofit 2.0 หากลืมใส่ เมื่อสั่ง enqueue หรือ execute แอพฯจะ Throw SecurityException ออกมาทันที ส่งผลให้แอพฯแครชหากไม่ได้ Handle ไว้

ก็คือจะกลับเข้าสู่อาการเดียวกับตอนสั่ง HttpURLConnection ด้วยตัวเองนั่นเอง เป็นความเปลี่ยนแปลงเล็กๆน้อยๆที่ต้องทราบไว้ครับ

เปลี่ยนไปใช้ Interceptor ของ OkHttp

ใน Retrofit 1.9 คุณสามารถใช้ RequestInterceptor มาดัก Request ได้ แต่บน Retrofit 2.0 เจ้า Interceptor นี้ถูกเอาออกไปเรียบร้อย เนื่องจากการติดต่อหลักตอนนี้ย้ายจาก Retrofit ไปอยู่บน OkHttp เต็มตัวแล้วนั่นเอง

และจากการที่มันไปอยู่บน OkHttp เราเลยสามารถใช้ท่า Interceptor มาตรฐานของ OkHttp ได้ดังนี้

        OkHttpClient client = new OkHttpClient();
        client.interceptors().add(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response response = chain.proceed(chain.request());

                // Do anything with response here

                return response;
            }
        });

และโยน client ที่สร้างได้ เข้าไปใน Builder ของ Retrofit

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.nuuneoi.com/base/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(client)
                .build();

เป็นอันเรียบร้อยครับ

สำหรับข้อมูลว่า OkHttp Interceptor ทำอะไรได้บ้าง สามารถหาได้จาก OkHttp Interceptors ครับ

Certificate Pinning

เหมือนกับ Interceptor หากเราต้องการใช้ Certificate Pinning เราต้องสร้างตัวแปร OkHttp ขึ้นมาก่อนพร้อมกับผูกข้อมูลเหล่านั้นไว้ใน OkHttp Instance ตัวนี้ ยกตัวอย่างเช่น

OkHttpClient client = new OkHttpClient.Builder()
        .certificatePinner(new CertificatePinner.Builder()
                .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
                .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
                .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
                .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
                .build())
        .build();

จากนั้นค่อยเอาตัวแปร client ที่สร้างขึ้นมาโยนต่อให้ retrofit เอาไปใช้งาน ก็เป็นอันเสร็จพิธี

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://api.nuuneoi.com/base/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(client)
        .build();

สำหรับข้อมูลว่าเราจะเอา sha1 hash มาจากไหน ... Search กูเกิลเอาครับ ไม่ยาก มีวิธีทำง่ายๆหลายวิธีเลย ลองดูครับ

การทำงานร่วมกับ RxJava ด้วย CallAdapter

นอกจากรูปแบบการประกาศ interface ที่คืนมาเป็น Call<T> แล้ว เรายังสามารถสร้างสิ่งที่ได้รับคืนมาเป็น Type ของตัวเองได้ด้วย เช่น MyCall<T> เป็นต้น ระบบตรงนี้บน Retrofit เราเรียกว่า CallAdapter

สำหรับ CallAdapter ก็มีเตรียมมาให้เป็น Module แยกอยู่จำนวนหนึ่ง ตัวนึงที่น่าจะใช้กันเยอะคือตัวที่เอาไว้ทำงานร่วมกับ RxJava ซึ่งจะคืนค่ามาเป็น Observable<T> ก่อนอื่นเลยก็ต้องอัญเชิญโมดูลของ RxJava CallAdapter และ RxAndroid มาให้เรียบร้อย สองตัวด้วยกัน

    compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'
    compile 'io.reactivex:rxandroid:1.0.1'

Sync Gradle ให้เรียบร้อย ในขั้นตอนของการสร้าง Retrofit ให้สั่งคำสั่ง addCallAdapterFactory เพิ่มเติมดังนี้

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.nuuneoi.com/base/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

คราวนี้ interface ของ Service คุณก็จะสามารถสร้างฟังก์ชั่นที่คืนค่ามาเป็น Observable<T> ได้แล้ว

public interface APIService {

    @POST("list")
    Call<DessertItemCollectionDao> loadDessertList();

    @POST("list")
    Observable<DessertItemCollectionDao> loadDessertListRx();

}

วิธีการใช้งานจากนี้ก็เป็นท่าของ RxJava ปกติเลยครับ โดยสิ่งที่อยากให้เน้นเพิ่มเติมคือการสั่ง observeOn(AndroidSchedulers.mainThread()) ที่ต้องใส่ไว้หากต้องการให้โค้ดในส่วนของ subscribe ทำงานบน Main Thread ครับ

        Observable<DessertItemCollectionDao> observable = service.loadDessertListRx();

        observable.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .unsubscribeOn(Schedulers.io())
            .subscribe(new Subscriber<DessertItemCollectionDao>() {
                @Override
                public void onCompleted() {
                    Toast.makeText(getApplicationContext(),
                            "Completed",
                            Toast.LENGTH_SHORT)
                        .show();
                }

                @Override
                public void onError(Throwable e) {
                    Toast.makeText(getApplicationContext(),
                            e.getMessage(),
                            Toast.LENGTH_SHORT)
                        .show();
                }

                @Override
                public void onNext(DessertItemCollectionDao dessertItemCollectionDao) {
                    Toast.makeText(getApplicationContext(),
                            dessertItemCollectionDao.getData().get(0).getName(),
                            Toast.LENGTH_SHORT)
                        .show();
                }
            });

ง่ายๆเท่านี้นี่แล น่าจะถูกใจคอ RxJava ได้เป็นอย่างดี =)

สรุป

การเปลี่ยนแปลงหลักๆที่เห็นก็เป็นไปตามที่ลิสต์ไว้ในบทความนี้ครับ

ถามว่าถึงเวลาเปลี่ยนมาเป็น 2.0 หรือยัง? เนื่องจากว่ามันยังเป็น Beta อยู่ หากไม่จำเป็นจริงๆแนะนำให้รอดูผลการทำงานจากคนอื่นๆสักพักก่อนก็ได้ครับ ส่วนตัวผมเป็น Early Adopter และแอพฯไม่ได้ซีเรียสอะไรมาก จะขอสลับมาใช้ตัว 2.0 เลย =)

อย่างไรก็ตาม เท่าที่ลองใช้ผมถือว่าโอเคกับตัวใหม่เลย ไม่มีปัญหาใดๆและรูปแบบน่าใช้ขึ้นมาก แถม Document ตอนนี้ก็เปลี่ยนเป็นของ 2.0.0 ทั้งหมดแล้ว ถ้าไม่คิดอะไรมากก็เปลี่ยนมาใช้เลยก็ได้ครับ มาลองกันๆ ^_^

ผู้เขียน: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)
นักพัฒนาแบบ Full-Stack ที่มีประสบการณ์ในการพัฒนาแอพฯแอนดรอยด์มากว่า 6 ปีและอยู่ในวงการพัฒนาแอพฯมือถือมากว่า 12 ปี มีความสนใจทางด้าน Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking และรักที่จะแชร์เรื่องราวให้ผู้คนได้อ่านได้ฟังกันผ่าน Blog