1. Đặt vấn đề
Trong các dự án Android cần làm việc với API, chúng ta thường sử dụng thư viện Retrofit. Ngoài việc cung cấp các phương thức request API, Retrofit còn cung cấp một giao diện làm việc với các thư viện serialization/deserialization (như Gson, Jackson, Moshi…) hỗ trợ việc chuyển đổi response của API từ dạng text về dạng object model trong Java/Kotlin và ngược lại để truyền trong request body.
Tuy nhiên, để Retrofit có thể làm được việc này ta cần cung cấp cho nó một đối tượng Converter Factory. Retrofit cung cấp sẵn một số Converter Factory để làm việc được với JSON và XML. Nhưng, trong một số trường hợp ta cần làm việc với response ở cả 2 dạng XML và JSON thì sao ? Trong bài viết này mình sẽ đưa ra một cách để giúp Retrofit có thể làm việc được với cả 2 dạng này nhé.
2. Converter Factory trong Retrofit
Để khởi tạo một đối tượng Retrofit, ta cần dùng cú pháp như sau.
1 2 3 4 5 6 | Retrofit<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span>HOST_NAME<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">client</span><span class="token punctuation">(</span>okHttpClient<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">addConverterFactory</span><span class="token punctuation">(</span>converterFactory<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Bỏ qua các phần râu ria, hãy để ý đến phương thức addConverterFactory
. Phương thức này cho phép thêm một converter factory mới vào danh sách converter factory của Retrofit. Vậy thì tại sao lại không add 2 converter factory vào như thế này ?
1 2 3 4 5 6 7 | Retrofit<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span>HOST_NAME<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">client</span><span class="token punctuation">(</span>okHttpClient<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">addConverterFactory</span><span class="token punctuation">(</span>jsonConverterFactory<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">addConverterFactory</span><span class="token punctuation">(</span>xmlConverterFactory<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Nhìn thì rất hợp lý nhưng vấn đề là khi Retrofit tạo Converter nó chỉ được sử dụng 1 converter factory mà thôi. Để ý lớp Converter.Factory
của Retrofit ta sẽ thấy 2 phương thức dưới đây.
1 2 3 4 5 6 7 8 9 | <span class="token keyword">public</span> <span class="token annotation punctuation">@Nullable</span> <span class="token class-name">Converter</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">ResponseBody</span><span class="token punctuation">,</span> <span class="token operator">?</span><span class="token punctuation">></span></span> <span class="token function">responseBodyConverter</span><span class="token punctuation">(</span> <span class="token class-name">Type</span> type<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> annotations<span class="token punctuation">,</span> <span class="token class-name">Retrofit</span> retrofit<span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token annotation punctuation">@Nullable</span> <span class="token class-name">Converter</span><span class="token generics"><span class="token punctuation"><</span><span class="token operator">?</span><span class="token punctuation">,</span> <span class="token class-name">RequestBody</span><span class="token punctuation">></span></span> <span class="token function">requestBodyConverter</span><span class="token punctuation">(</span> <span class="token class-name">Type</span> type<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> parameterAnnotations<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> methodAnnotations<span class="token punctuation">,</span> <span class="token class-name">Retrofit</span> retrofit<span class="token punctuation">)</span> |
Trong trường hợp jsonConverterFactory.responseBodyConverter()
trả về null thì nó mới dùng đến xmlConverterFactory.responseBodyConverter()
. Như vậy nếu bình thường thì Retrofit chỉ dùng duy nhất Converter Factory đầu tiên được add.
Do vậy, ta sẽ cần phải sử dụng 1 Converter Factory duy nhất cho Retrofit !
3. Ý tưởng về một AutoTypeConverterFactory
Vấn đề mấu chốt là phải làm sao để Converter Factory nhận ra được API nào sử dụng JSON, API nào thì trả về XML để trả về Converter tương ứng. Nó không thể đợi response trả về rồi mới kiểm tra được, cũng không thể kiểm tra kiểu của model response/request body vì số lượng model là rất lớn. Cách tốt hơn cả là sử dụng Annotation cho các method request của interface API Service.
Với các request/response body kiểu JSON ta sẽ sử dụng annotation @JsonType
còn với kiểu XML thì là @XmlType
. Có lẽ đó là cách hoàn hảo nhất. Ta luôn biết với từng API thì kiểu truyền đi và trả về là dạng nào.
Vấn đề xác định kiểu trả về/truyền đi của API đã xong. Giờ đến phần làm sao để Converter Factory tự động nhận diện từng kiểu để trả về converter phù hợp.
Quay trở lại nội dung class Converter.Factory với 2 method quan trọng nhất:
1 2 3 4 5 6 7 8 9 | <span class="token keyword">public</span> <span class="token annotation punctuation">@Nullable</span> <span class="token class-name">Converter</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">ResponseBody</span><span class="token punctuation">,</span> <span class="token operator">?</span><span class="token punctuation">></span></span> <span class="token function">responseBodyConverter</span><span class="token punctuation">(</span> <span class="token class-name">Type</span> type<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> annotations<span class="token punctuation">,</span> <span class="token class-name">Retrofit</span> retrofit<span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token annotation punctuation">@Nullable</span> <span class="token class-name">Converter</span><span class="token generics"><span class="token punctuation"><</span><span class="token operator">?</span><span class="token punctuation">,</span> <span class="token class-name">RequestBody</span><span class="token punctuation">></span></span> <span class="token function">requestBodyConverter</span><span class="token punctuation">(</span> <span class="token class-name">Type</span> type<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> parameterAnnotations<span class="token punctuation">,</span> <span class="token class-name">Annotation</span><span class="token punctuation">[</span><span class="token punctuation">]</span> methodAnnotations<span class="token punctuation">,</span> <span class="token class-name">Retrofit</span> retrofit<span class="token punctuation">)</span> |
Trong phương thức responseBodyConverter
ta thấy có tham số annnotations
. Tham số annotations
sẽ trả về một mảng các annotation được sử dụng với từng phương thức trong API Service. Bây giờ do ta thêm annotation là @JsonType
hoặc @XmlType
nên chúng cũng nằm trong mảng này luôn. Bằng việc kiểm tra tham số annotations
chứa annotation @JsonType
hay @XmlType
ta sẽ xác định được converter nào cần dùng.
Với phương thức requestBodyConverter
thì cũng tương tự như vậy nhưng tham số cần kiểm tra sẽ là parameterAnnotations
vì đây là tham số cho params được truyền trong request body.
Bằng việc dùng 1 Converter Factory và kiểm tra annotation, ta sẽ biết được lúc nào cần sử dụng converter nào và trả về converter tương ứng.
4. Bắt tay vào code
Mình sẽ ví dụ bằng Kotlin nhé. Đầu tiên cần định nghĩa 2 annotation class:
1 2 3 4 5 6 7 8 | <span class="token annotation builtin">@Target</span><span class="token punctuation">(</span>AnnotationTarget<span class="token punctuation">.</span>FUNCTION<span class="token punctuation">)</span> <span class="token annotation builtin">@Retention</span><span class="token punctuation">(</span>AnnotationRetention<span class="token punctuation">.</span>RUNTIME<span class="token punctuation">)</span> <span class="token keyword">annotation</span> <span class="token keyword">class</span> JsonType <span class="token annotation builtin">@Target</span><span class="token punctuation">(</span>AnnotationTarget<span class="token punctuation">.</span>FUNCTION<span class="token punctuation">)</span> <span class="token annotation builtin">@Retention</span><span class="token punctuation">(</span>AnnotationRetention<span class="token punctuation">.</span>RUNTIME<span class="token punctuation">)</span> <span class="token keyword">annotation</span> <span class="token keyword">class</span> XmlType |
Giải thích qua:
@Target(AnnotationTarget.FUNCTION)
: Annotation chỉ được gắn vào một hàm/phương thức.@Retention(AnnotationRetention.RUNTIME)
: Annotation có hiệu lực trong thời gian Runtime, không ảnh hưởng lúc Compile.
Trong API Service interface chỉ cần dùng thế này:
1 2 3 4 5 6 7 | <span class="token annotation builtin">@XmlType</span> <span class="token annotation builtin">@GET</span><span class="token punctuation">(</span><span class="token string">"..."</span><span class="token punctuation">)</span> <span class="token keyword">fun</span> <span class="token function">getUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> UserResponse <span class="token annotation builtin">@POST</span><span class="token punctuation">(</span><span class="token string">"..."</span><span class="token punctuation">)</span> <span class="token keyword">fun</span> <span class="token function">postComment</span><span class="token punctuation">(</span><span class="token annotation builtin">@JsonType</span> comment<span class="token operator">:</span> Comment<span class="token punctuation">)</span><span class="token operator">:</span> ResultResponse |
Giờ là nhân vật chính: AutoTypeConverterFactory mình sẽ áp dụng State Design Pattern cho lớp này để nó có thể chuyển qua lại giữa 2 trạng thái “JSON state” và “XML state” nhờ đó hành vi cũng thay đổi theo. Các bạn có thể áp dụng nhiều cách khác như dùng Delegate chẳng hạn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <span class="token keyword">class</span> <span class="token function">AutoTypeConverterFactory</span><span class="token punctuation">(</span> <span class="token keyword">private</span> <span class="token keyword">val</span> jsonFactory<span class="token operator">:</span> Converter<span class="token punctuation">.</span>Factory<span class="token punctuation">,</span> <span class="token keyword">private</span> <span class="token keyword">val</span> xmlFactory<span class="token operator">:</span> Converter<span class="token punctuation">.</span>Factory <span class="token punctuation">)</span> <span class="token operator">:</span> Converter<span class="token punctuation">.</span><span class="token function">Factory</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token keyword">var</span> currentFactory <span class="token operator">=</span> jsonFactory <span class="token comment">// Mặc định là jsonFactory</span> <span class="token keyword">private</span> <span class="token keyword">fun</span> <span class="token function">updateCurrentFactory</span><span class="token punctuation">(</span>annotations<span class="token operator">:</span> Array<span class="token operator"><</span>Annotation<span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">when</span> <span class="token punctuation">(</span>annotations<span class="token punctuation">.</span><span class="token function">find</span> <span class="token punctuation">{</span> it <span class="token keyword">is</span> JsonType <span class="token operator">||</span> it <span class="token keyword">is</span> XmlType <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">is</span> JsonType<span class="token punctuation">,</span> <span class="token keyword">null</span> <span class="token operator">-></span> currentFactory <span class="token operator">=</span> jsonFactory <span class="token comment">// Trong trường hợp không thêm annotation sẽ dùng jsonFactory</span> <span class="token keyword">is</span> XmlType <span class="token operator">-></span> currentFactory <span class="token operator">=</span> xmlFactory <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token annotation builtin">@Nullable</span> <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">responseBodyConverter</span><span class="token punctuation">(</span> type<span class="token operator">:</span> Type<span class="token punctuation">,</span> annotations<span class="token operator">:</span> Array<span class="token operator"><</span>Annotation<span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> retrofit<span class="token operator">:</span> Retrofit <span class="token punctuation">)</span><span class="token operator">:</span> Converter<span class="token operator"><</span>ResponseBody<span class="token punctuation">,</span> <span class="token operator">*</span><span class="token operator">></span><span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token function">updateCurrentFactory</span><span class="token punctuation">(</span>annotations<span class="token punctuation">)</span> <span class="token keyword">return</span> currentFactory<span class="token punctuation">.</span><span class="token function">responseBodyConverter</span><span class="token punctuation">(</span>type<span class="token punctuation">,</span> annotations<span class="token punctuation">,</span> retrofit<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token annotation builtin">@Nullable</span> <span class="token keyword">override</span> <span class="token keyword">fun</span> <span class="token function">requestBodyConverter</span><span class="token punctuation">(</span> type<span class="token operator">:</span> Type<span class="token punctuation">,</span> parameterAnnotations<span class="token operator">:</span> Array<span class="token operator"><</span>Annotation<span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> methodAnnotations<span class="token operator">:</span> Array<span class="token operator"><</span>Annotation<span class="token operator">?</span><span class="token operator">></span><span class="token punctuation">,</span> retrofit<span class="token operator">:</span> Retrofit <span class="token punctuation">)</span><span class="token operator">:</span> Converter<span class="token operator"><</span><span class="token operator">*</span><span class="token punctuation">,</span> RequestBody<span class="token operator">></span><span class="token operator">?</span> <span class="token punctuation">{</span> <span class="token function">updateCurrentFactory</span><span class="token punctuation">(</span>parameterAnnotations<span class="token punctuation">)</span> <span class="token keyword">return</span> currentFactory<span class="token punctuation">.</span><span class="token function">requestBodyConverter</span><span class="token punctuation">(</span> type<span class="token punctuation">,</span> parameterAnnotations<span class="token punctuation">,</span> methodAnnotations<span class="token punctuation">,</span> retrofit <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Trong mỗi phương thức, AutoTypeConverterFactory
sẽ luôn kiểm tra annotions -> cập nhật trạng thái converter factory hiện tại -> gọi phương thức tương ứng của converter factory hiện tại. Ví dụ khi mình dùng JSON Converter Factory là Gson, XML Converter Factory là TikXml thì có thể khởi tạo AutoTypeConverterFactory
như sau:
1 2 3 4 5 | <span class="token keyword">val</span> converterFactory <span class="token operator">=</span> <span class="token function">AutoTypeConverterFactory</span><span class="token punctuation">(</span> jsonFactory <span class="token operator">=</span> GsonConverterFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> xmlFactory <span class="token operator">=</span> TikXmlConverterFactory<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> |
Và nhét nó vào trong Retrofit Builder như một converter factory thông thường:
1 2 3 4 5 6 | Retrofit<span class="token punctuation">.</span><span class="token function">Builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">baseUrl</span><span class="token punctuation">(</span>HOST_NAME<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">client</span><span class="token punctuation">(</span>okHttpClient<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">addConverterFactory</span><span class="token punctuation">(</span>converterFactory<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Như vậy là đã xong. Bây giờ Retrofit đã sẵn sàng để một lúc cân 2 kiểu API.
5. Kết luận
Qua bài viết này, mình đã chia sẻ cách để áp dụng song song nhiều kiểu response/request body khác nhau đồng thời cũng đã giải thích tương đối chi tiết về Converter Factory trong Retrofit. Nếu có bất kỳ góp ý hay thắc mắc gì hãy gửi vào phần bình luận nhé.
Happy coding !!