Retrofit

Biblioteki

  |   10 min czytania
(Retrofit 2.5)

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
                }
            )
    }
}