Charakterystyka
Retrofit
jest klientem HTTP
zorientowanym na typowanie obiektów zapytań, używającym do żądań bibliotekę OkHttp
. Umożliwia w łatwy sposób pobieranie i przesyłanie danych w formie obiektów lub schemacie JSON
za pośrednictwem usługi internetowej opartej na REST
. Ponadto pozwala na wykonywanie zapytań w sposób synchroniczny i asynchroniczny z uwzględnieniem uwierzytelniania i rejestrowania stanu operacji. Aby rozpocząć pracę z Retrofit
należy dokonać definicji klasy modelu danych, interfejsu deklarującego możliwe operacje HTTP oraz instancji klasy Retrofit.Builder
, której zadaniem jest zbudowanie usługi w oparciu o wskazane zależności.
Model
Konwerter podejmuje próbę konwersji otrzymanego wyniku do zadaklerowanego typu klasy modelu o strukturze danych reprezentującej oczekiwany rezultat. Wartości zostają przypisane tylko do właściwości zachowujących zgodność z formatem odpowiedzi, tzn. brakujące lub nadmiarowe pola są ignorowane. Poniższy listing przedstawia przykładową strukturę modelu Product
i Producer
dla zadanej odpowiedzi w formacie JSON
.
{
"id": 100,
"name": "Coca-Cola Lime",
"producer": {
"id": "200"
"name": "Coca-Cola Company",
"country": "USA"
},
"price": 2.5,
"ingredients": ["water", "sugar", "e150d", "lime syrup"]
}
//some classes with properties and getters, setters
data class Product(val id: Int, val name: String, val producer: Producer, val price: Double)
data class Producer(val id: Int, val name: String, val country String)
//converter will just ignore missing ingredients field in model
Interfejs
Rolą interfejsu jest zadeklarowanie metod odwołujących się do zasobów sieciowego API
. Za pomocą adnotacji @GET
, @POST
, @PUT
, @DELETE
możliwe jest określenie rodzaju zapytania dla architektury REST
. Zapytania mogą być parametryzowane przy użyciu argumentów metody oznaczonych jako @Path
i @Query
. Poza wysyłaniem danych w formie text/plain
wspierana jest także obsługa zapytań typu application/x-www-form-urlencoded
(adnotacja @FormUrlEncoded
) oraz multipart/form-data
(adnotacja @Multipart
). Dodatkowo możliwe jest ustawienie metadanych nagłówka w adnotacji @Headers
.
interface ProductService {
@GET("products")
fun getProducts() : Call<List<Product>>
@GET("products") //add some optional query to request
fun getProducts(@Query("sort") sort: String) : Call<List<Product>>
@GET("product/{id}") //parametrize request
fun getProduct(@Path("id") id: Int) : Call<Product>
@Headers("Cache-Control: max-age=1000000") //set additional static header
@GET("producer/{id}")
fun getProducer(@Path("id") id: Int) : Call<Producer>
@POST("products/new")
fun createProduct(@Body product: Product) : Call<ResponseBody>
@FormUrlEncoded //send as form url encoded
@POST("producers/new")
fun createProducer(@Field("name") name: String, @Field("country") country: String) : Call<ResponseBody>
@Multipart //send as multipart - mainly for files
@PUT("producer/{id}")
fun updateProducer(@Path("id") id: Int, @Part("image") image: RequestBody) : Call<ResponseBody>
@DELETE("product/{id}")
fun deleteProduct(@Path("id") id: Int) : Call<ResponseBody>
//some more REST API methods
}
Budowniczy
Aby wykorzystać stworzone API w interfejsie należy zbudować instancje typu Retrofit
przy pomocy budowniczego Retrofit.Builder
podając przynajmniej bazowe URL
oraz opcjonalnie m.in. konwerter (np. Gson
, Protobuf
, Simple XML
), adapter i klienta HTTP. Następnie wykorzystując obiekt Retrofit
stworzyć instancję wybranego interfejsu API.
private fun buildService() {
val retrofit = Retrofit.Builder()
.baseUrl("http://api.androidcode.pl/") //address not exists, only for example purpose
.addConverterFactory(GsonConverterFactory.create()) //Gson is default
.client(OkHttpClient.Builder().build()) //OkHttpClient is default
.build()
service = retrofit.create(ProductService::class.java) //instance of ProductService
//do calls on service object
}
Zapytanie
Żądanie sieciowe dla metody zapytania zwracającego instancje typu Call
może zostać wykonane synchronicznie przy użyciu metody execute
lub asynchronicznie metodą enqueue
wraz z przekazaniem obiektu zwrotnego typu Callback
.
private fun getProductsSync() {
val call : Call<List<Product>> = service.getProducts()
val response : Response<List<Product>> = call.execute()
//wait for response in this place
if(response.isSuccessful) {
//do something with data
val data : List<Product>? = response.body()
}
else {
//do some fail action
val error = response.errorBody()
val message = response.message()
}
}
private fun getProductsAsync() {
val call : Call<List<Product>> = service.getProducts()
//do request at this point and do something else during waiting for response
call.enqueue(object: Callback<List<Product>> {
override fun onResponse(call: Call<List<Product>>, response: Response<List<Product>>) {
//response returned at some moment
val products = response.body()
//do something with data
}
override fun onFailure(call: Call<List<Product>>, t: Throwable) {
//fail returned at some moment
}
})
}
Autoryzacja
W sytuacji, gdy zapytania wymagają autoryzacji możliwe jest dodanie tokenu autoryzacyjnego do zapytania przy użyciu adnotacji @Header("Authorization")
, jednakże w takim przypadku autoryzacja dotyczy tylko tego żądania. Aby dodać autoryzację do wszystkich zapytań należy dodać obiekt typu Interceptor
do konfiguracji klienta.
@GET("products")
fun getProducts(@Header("Authorization") String credentials) : Call<List<Product>>
//instead of adding authorization to single request like above
//just add authorization to every request by Interceptor config as below
private fun buildServiceWithAuthorization() {
//create interceptor which add credentials to request
val interceptor = object: Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val originalRequest = chain.request()
val builder = originalRequest.newBuilder()
.header("Authorization", Credentials.basic("username", "password"))
return chain.proceed(builder.build())
}
}
//pass interceptor to http client
val okHttpClient = OkHttpClient().newBuilder()
.addInterceptor(interceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("http://api.androidcode.pl/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
service = retrofit.create(ProductService::class.java) //instance of ProductService
}
RxJava
Retrofit
umożliwia współpracę z RxJava
(metody mogą zwracać Observable
) poprzez dodanie adaptera RxJava2CallAdapterFactory
do konfiguracji budowniczego co sprawia, że tworzenie aplikacji z wykorzystaniem obu bibliotek staje się prostsze. Dzięki temu Retrofit jest nierzadko wybierany jako podstawowy klient sieciowy w aplikacji używających RxJava.
interface ProductServiceRxJava {
@GET("products")
fun getProducts() : Observable<List<Product>> //or other type of RxJava observables
companion object {
fun create(): ProductServiceRxJava {
val retrofit = Retrofit.Builder()
.baseUrl("http://api.androidcode.pl/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create<ProductServiceRxJava>(ProductServiceRxJava::class.java)
}
}
}
class RxJavaActivity : AppCompatActivity() {
private val service by lazy { ProductServiceRxJava.create() }
private var disposable: Disposable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rxjava)
getProductsRxJava()
}
override fun onDestroy() {
super.onDestroy()
disposable?.dispose()
}
private fun getProductsRxJava() {
disposable = service.getProducts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
val products = result
//do something with data
},
{ error ->
val message = error.message
//some error action
}
)
}
}