|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Retrofit" |
| 4 | +date: 2019-06-10 |
| 5 | +categories: ["Biblioteki"] |
| 6 | +image: libraries/retrofit |
| 7 | +github: libraries/tree/master/retrofit |
| 8 | +description: "Biblioteki" |
| 9 | +version: Retrofit 2.5 |
| 10 | +keywords: "retrofit, network, http, request, response, sync, async, call, callback, rest, api, get, post, put, delete, path, query, header, multipart, formurlencoded, json, rxjava, android, programowanie, programming" |
| 11 | +--- |
| 12 | + |
| 13 | +## Charakterystyka |
| 14 | +`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. |
| 15 | + |
| 16 | +## Model |
| 17 | +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`. |
| 18 | + |
| 19 | +{% highlight json %} |
| 20 | +{ |
| 21 | + "id": 100, |
| 22 | + "name": "Coca-Cola Lime", |
| 23 | + "producer": { |
| 24 | + "id": "200" |
| 25 | + "name": "Coca-Cola Company", |
| 26 | + "country": "USA" |
| 27 | + }, |
| 28 | + "price": 2.5, |
| 29 | + "ingredients": ["water", "sugar", "e150d", "lime syrup"] |
| 30 | +} |
| 31 | +{% endhighlight %} |
| 32 | + |
| 33 | +{% highlight kotlin %} |
| 34 | +//some classes with properties and getters, setters |
| 35 | +data class Product(val id: Int, val name: String, val producer: Producer, val price: Double) |
| 36 | +data class Producer(val id: Int, val name: String, val country String) |
| 37 | +//converter will just ignore missing ingredients field in model |
| 38 | +{% endhighlight %} |
| 39 | + |
| 40 | +## Interfejs |
| 41 | +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`. |
| 42 | + |
| 43 | +{% highlight kotlin %} |
| 44 | +interface ProductService { |
| 45 | + |
| 46 | + @GET("products") |
| 47 | + fun getProducts() : Call<List<Product>> |
| 48 | + |
| 49 | + @GET("products") //add some optional query to request |
| 50 | + fun getProducts(@Query("sort") sort: String) : Call<List<Product>> |
| 51 | + |
| 52 | + @GET("product/{id}") //parametrize request |
| 53 | + fun getProduct(@Path("id") id: Int) : Call<Product> |
| 54 | + |
| 55 | + @Headers("Cache-Control: max-age=1000000") //set additional static header |
| 56 | + @GET("producer/{id}") |
| 57 | + fun getProducer(@Path("id") id: Int) : Call<Producer> |
| 58 | + |
| 59 | + @POST("products/new") |
| 60 | + fun createProduct(@Body product: Product) : Call<ResponseBody> |
| 61 | + |
| 62 | + @FormUrlEncoded //send as form url encoded |
| 63 | + @POST("producers/new") |
| 64 | + fun createProducer(@Field("name") name: String, @Field("country") country: String) : Call<ResponseBody> |
| 65 | + |
| 66 | + @Multipart //send as multipart - mainly for files |
| 67 | + @PUT("producer/{id}") |
| 68 | + fun updateProducer(@Path("id") id: Int, @Part("image") image: RequestBody) : Call<ResponseBody> |
| 69 | + |
| 70 | + @DELETE("product/{id}") |
| 71 | + fun deleteProduct(@Path("id") id: Int) : Call<ResponseBody> |
| 72 | + |
| 73 | + //some more REST API methods |
| 74 | +} |
| 75 | +{% endhighlight %} |
| 76 | + |
| 77 | +## Budowniczy |
| 78 | +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. |
| 79 | + |
| 80 | +{% highlight kotlin %} |
| 81 | +private fun buildService() { |
| 82 | + val retrofit = Retrofit.Builder() |
| 83 | + .baseUrl("http://api.androidcode.pl/") //address not exists, only for example purpose |
| 84 | + .addConverterFactory(GsonConverterFactory.create()) //Gson is default |
| 85 | + .client(OkHttpClient.Builder().build()) //OkHttpClient is default |
| 86 | + .build() |
| 87 | + |
| 88 | + service = retrofit.create(ProductService::class.java) //instance of ProductService |
| 89 | + //do calls on service object |
| 90 | +} |
| 91 | +{% endhighlight %} |
| 92 | + |
| 93 | +## Zapytanie |
| 94 | +Żą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`. |
| 95 | + |
| 96 | +{% highlight kotlin %} |
| 97 | +private fun getProductsSync() { |
| 98 | + val call : Call<List<Product>> = service.getProducts() |
| 99 | + val response : Response<List<Product>> = call.execute() |
| 100 | + //wait for response in this place |
| 101 | + if(response.isSuccessful) { |
| 102 | + //do something with data |
| 103 | + val data : List<Product>? = response.body() |
| 104 | + } |
| 105 | + else { |
| 106 | + //do some fail action |
| 107 | + val error = response.errorBody() |
| 108 | + val message = response.message() |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +private fun getProductsAsync() { |
| 113 | + val call : Call<List<Product>> = service.getProducts() |
| 114 | + //do request at this point and do something else during waiting for response |
| 115 | + call.enqueue(object: Callback<List<Product>> { |
| 116 | + override fun onResponse(call: Call<List<Product>>, response: Response<List<Product>>) { |
| 117 | + //response returned at some moment |
| 118 | + val products = response.body() |
| 119 | + //do something with data |
| 120 | + } |
| 121 | + override fun onFailure(call: Call<List<Product>>, t: Throwable) { |
| 122 | + //fail returned at some moment |
| 123 | + } |
| 124 | + }) |
| 125 | +} |
| 126 | +{% endhighlight %} |
| 127 | + |
| 128 | +## Autoryzacja |
| 129 | +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. |
| 130 | + |
| 131 | +{% highlight kotlin %} |
| 132 | +@GET("products") |
| 133 | +fun getProducts(@Header("Authorization") String credentials) : Call<List<Product>> |
| 134 | +//instead of adding authorization to single request like above |
| 135 | +//just add authorization to every request by Interceptor config as below |
| 136 | + |
| 137 | +private fun buildServiceWithAuthorization() { |
| 138 | + //create interceptor which add credentials to request |
| 139 | + val interceptor = object: Interceptor { |
| 140 | + override fun intercept(chain: Interceptor.Chain): okhttp3.Response { |
| 141 | + val originalRequest = chain.request() |
| 142 | + val builder = originalRequest.newBuilder() |
| 143 | + .header("Authorization", Credentials.basic("username", "password")) |
| 144 | + return chain.proceed(builder.build()) |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + //pass interceptor to http client |
| 149 | + val okHttpClient = OkHttpClient().newBuilder() |
| 150 | + .addInterceptor(interceptor) |
| 151 | + .build() |
| 152 | + |
| 153 | + val retrofit = Retrofit.Builder() |
| 154 | + .baseUrl("http://api.androidcode.pl/") |
| 155 | + .addConverterFactory(GsonConverterFactory.create()) |
| 156 | + .client(okHttpClient) |
| 157 | + .build() |
| 158 | + |
| 159 | + service = retrofit.create(ProductService::class.java) //instance of ProductService |
| 160 | +} |
| 161 | +{% endhighlight %} |
| 162 | + |
| 163 | +## RxJava |
| 164 | +`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. |
| 165 | + |
| 166 | +{% highlight kotlin %} |
| 167 | +interface ProductServiceRxJava { |
| 168 | + |
| 169 | + @GET("products") |
| 170 | + fun getProducts() : Observable<List<Product>> //or other type of RxJava observables |
| 171 | + |
| 172 | + companion object { |
| 173 | + fun create(): ProductServiceRxJava { |
| 174 | + val retrofit = Retrofit.Builder() |
| 175 | + .baseUrl("http://api.androidcode.pl/") |
| 176 | + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) |
| 177 | + .addConverterFactory(GsonConverterFactory.create()) |
| 178 | + .build() |
| 179 | + return retrofit.create<ProductServiceRxJava>(ProductServiceRxJava::class.java) |
| 180 | + } |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +class RxJavaActivity : AppCompatActivity() { |
| 185 | + |
| 186 | + private val service by lazy { ProductServiceRxJava.create() } |
| 187 | + private var disposable: Disposable? = null |
| 188 | + |
| 189 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 190 | + super.onCreate(savedInstanceState) |
| 191 | + setContentView(R.layout.activity_rxjava) |
| 192 | + getProductsRxJava() |
| 193 | + } |
| 194 | + |
| 195 | + override fun onDestroy() { |
| 196 | + super.onDestroy() |
| 197 | + disposable?.dispose() |
| 198 | + } |
| 199 | + |
| 200 | + private fun getProductsRxJava() { |
| 201 | + disposable = service.getProducts() |
| 202 | + .subscribeOn(Schedulers.io()) |
| 203 | + .observeOn(AndroidSchedulers.mainThread()) |
| 204 | + .subscribe( |
| 205 | + { result -> |
| 206 | + val products = result |
| 207 | + //do something with data |
| 208 | + }, |
| 209 | + { error -> |
| 210 | + val message = error.message |
| 211 | + //some error action |
| 212 | + } |
| 213 | + ) |
| 214 | + } |
| 215 | +} |
| 216 | +{% endhighlight %} |
0 commit comments