Retrofit biblioteka na Android-u

Share

REST servisi

Veliki broj danas veoma popularnih aplikacija na Android-u i drugim mobilnim platformama uglavnom služi da korisnicima prikaže nekakav sadržaj. Većinu korisnika često uglavnom ni ne zanima izgled našeg kirisničkog interfejsa ili koje su dodatne opcije podržane već samo žele da vide nove slike, zanimljiv video ili da razmene par reči i emotikona sa ljudima koje poznaju ili ne poznaju. Ne treba zaboraviti i da ovaj sadržaj treba da bude svež i raznolik jer će u suprotnom naši korisnici veoma brzo preći na druge aplikacije.

Ovakavo ponašanje mobilnih aplikacija je poprilično teško ostvariti standardnim modelom desktop aplikacija gde bi smo uz kod zadužen za poslovnu logiku i nekakav interfejs možda spakovali i bazu sa podacima. Mnogo bolji pristup da se ovo ostvari je da deo sa podacima premestimo na nekakav server i onda omogućimo korisnicima da podatke skidaju po potrebi korišćenjem nekog servisa. Ukoliko uz ovo dodamo i želje korisnika da te podatke kreiraju i modifikuju dolazimo logično do zaključka da su ti podaci ustvari resursi a kao implementaciju ovog principa i do REST pristupa.
REST servisi su mnogo više od vraćanja nekih JSON fajlova preko HTTP-a. Ali čak i da usvojimo ovo uprošćenje i ne zalazimo mnogo u detalje REST-a možemo uraditi mnogo jer skoro svaka velika kompanija danas nam pruža nekakav REST API. Čak iako ne želimo integraciju sa servisima drugih provajdera i dalje možemo koristiti REST arhitekturu kao dobru praksu i način da naš sistem napravimo lakšim za testiranje i razvoj.

REST na Android-u po starinski

Šta nam je sve potrebno za ulaz u veliki svet REST servisa?

  • Prvo nam treba nekakva mrežna biblioteka za komunikaciju preko HTTP-a. Android frejmvork nudi nam URLConnection
  • Takođe nam je potrebna i JSON porška za serializaciju/deserializaciju. Jedan od dobrih izbora za ovo je GSON
  • Ne zaboravimo takođe da na Android-u ne smemo obavljati duge operacije (uključujući ovde i mrežnu komunikaaciju) na Main niti. Kako bismo ovo rešili možemo koristiti Android-ov AsyncTask

Ukoliko sada spakujemo ove komponente u neku klasu dobićemo ovo:

  private class GetExamplesOperation extends AsyncTask<Example, Void, String> {

    @Override
    protected Example doInBackground(String... params) {
        getExamples(params[0]);
    }

    @Override
    protected void onPostExecute(Example example) {
        //Do something with example here probably by calling a callback passed into AsyncTask constructor
    }

    public static Example getExamples(String examplesUrl) {
        HttpURLConnection urlConnection = null;
        try {
            // create connection
            URL urlToRequest = new URL(serviceUrl);
            urlConnection = (HttpURLConnection) urlToRequest.openConnection();
            urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
            urlConnection.setReadTimeout(DATARETRIEVAL_TIMEOUT);

            // handle errors
            int statusCode = urlConnection.getResponseCode();
            if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
                // handle authorization
            } else if (statusCode != HttpURLConnection.HTTP_OK) {
                // handle 404, 500, etc
            }

                // create JSON object from content
            InputStream in = new BufferedInputStream(urlConnection.getInputStream());
            Gson gson = new GsonBuilder().create();
            Example example = gson.fromJson(reader, Example.class);;
            return example;
        } catch (MalformedURLException e) {
            // URL is invalid
        } catch (SocketTimeoutException e) {
            // data retrieval or connection timed out
        } catch (IOException e) {
            // (could not create input stream)
        } catch (JSONException e) {
            // response body is no valid JSON string
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
        }
        }       

        return null;
    }
    }

Poprilično dugačak kod za nešto što bismo u Bash-u uradili u jednoj liniji koristeci curl.
Ovaj kod je poprilično pojednostavljen i verovatno ćemo imati poziva ka mnogo više URL-ova i shodno tome ovakav kod ponoviti više puta. Fino bi bilo i kad bismo na nekakav pametan način keširali odgovore sa servera i reciklirali konekcije kako bismo uštedeli resurse.
Takođe ne treba zaboraviti na mnoge potencijalne probleme sa AsyncTask-ovima (kao što su odvezanost od životnog veka naših Activity-ja i problemi otkazivanja istih i logike redosleda izvršavanja na različitim verzijama Android-a) isto ne treba zaboraviti ni da URLConnection ima dosta specifičnosti pa čak i bugova na nekim verzijama Android-a. Naravno sve se ovo može izbeći uz pisanje dosta koda i dobru organizaciju ali u najboljem slučaju izgubićemo dosta vremena na to a u najgorem uvesti gomilu grešaka u program i načiniti kod komplikovanijim i sve to samo da bismo postigli da skinemo ili pošaljemo neke podatke na servis.

Retrofit priskače u pomoć

Gore navedeni problemi deluju kao nešto sa čime se verovatno veliki broj Android programera susreće imajući na umu veliku popularnost REST servisa. Isti problem je mučio i dobre ljude iz kompanije Square te su oni odlučili da kreiraju biblioteku koja ga rešava i javno je objave (uz njihove mnoge druge korisne biblioteke) pod imenom Retrofit. Ova biblioteka je izuzetno popularna među Android programerima što zbog lakog korišćenja i odličnih performansi toliko i zbog ogromnog broja pitanja i odgovora na sajtu Stack Overflow. Takođe ista je softver otvorenog koda sa Apache licencom prijateljski nastrojenom čak i ka komercijalnim projektima.

Kako bismo Retrofit koristili u našem projektu i imajući u vidu da je jar dostupan na JCenter-u dovoljno je da dodamo ovu liniju u naš build.gradle

 compile 'com.squareup.retrofit2:retrofit:2.2.0'

Nakon ovoga biblioteka se drži jednog prostog pravila. Potrebno je definisati interfejse koji predstavljaju naše servise i definisati po jedan metod za svaki URL kojem želimo da pristupimo. Na primer ako imamo REST servis ExampleRest sa kojeg želimo da skinemo sve primere za specifičnog korisnika:

     public interface ExampleRest {
    @GET("{user}/examples")
    Call<List<Example>> getExamples(@Path("user") String username);
}

Ovde se već može primetiti poprilično dobra i logična primena anotacija za:

  • Tip zahteva koji želimo da izvršimo (@GET)
  • Deo putanje koji je promenjiv u zavisnosti on našeg identifikatora korisnika (@Path)

Kako bismo promenili HTTP method koji koristimo iz GET u POST (ostali podržani su PUT, DELETE i HEAD) dovoljno je samo promeniti anotaciju a Retrofit će obaviti ostalo.

Možda se pitate kako sada implementirati ovaj Interfejs? A pomalo čudan odgovor i činjenica kod ove biblioteke je da za time nema potrebe. Dovoljno je da pozovemo:

     Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://example.com/v2")
    .build();
ExampleRest exampleRest = retrofit.create(ExampleRest.class);

I koristeći „magiju“ (i refleksiju) Retrofit će napraviti implementaciju interfejsa za nas. Nadalje dovoljno je samo da pozivamo metode na našem objektu a mrežnu komunikaciju, keširanje i serializaciju/deserializaciju će biblioteka obaviti za nas.

 Call<List<Example>> callExamples = exampleRest.getExamples("test");
List<Example> examples = callExamples.execute();

Bitno je primetiti da je kod gore blokirajući i ne smemo ga pozvati iz UI niti. Ovo možemo rešiti tako što ćemo ovo staviti u AsyncTask ili napraviti jos jednu nit sa kojom ćemo se nekako sinhronizovati ali Retrofit nam može pomoći oko ovoga na mnogo jednostavniji način.

      Call<List<Example>> callExamples = exampleRest.getExamples("test");
call.enqueue(new Callback<List<Example>>() {  
    @Override
    public void onResponse(Call<List<Example>> call, Response<List<Example>> response) {
        if (response.isSuccessful()) {
            // Everything was successful list of Examples is available inside the response object
        } else {
            // error response
        }
    }
    @Override
    public void onFailure(Call<List<Example>> call, Throwable t) {
        Log.d("Error", t.getMessage());
        Toast.makeText(context,t.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

Imena pozvanih implementiranih metoda iz intefejsa su poprilično jasna i u suštini bitno je samo da obradimo slučajeve kada je poziv uspeo i kada je došlo do greške. Takođe primetimo i da se u kodu krije Toast.makeText(…) i to bez slanja poruke na Looper Main niti. Ovo je moguće zbog toga što Retrofit podrazumevano ove metode poziva u Main niti na Android-u.

Šta ako želimo da parametrizujemo naš poziv i putanjom i upitnim parametrom? Bez Retrofit-a verovatno bismo pribegli nekakvom spajanju String-ova ili korišćenju Uri.Builder klase i njene predugačke sintakse. Ali uz ovu biblioteku URL možemo jednostavno manipulatisati annotaticijama.

      public interface ExampleRest {
    @GET("examples/{id}")
    Call<List<Example>> getExampleById(@Path("id") int exampleId, @Query("sort") String sort);
}

Menjanje putanje urađeno je korišćenjem alphanumeričke vrednosti unutar {} i @Path anotacijom kako bismo vrednost parametra prosleđenog u metodu koristili kao zamenu. Treba znati i da {} nije limitirano samo na kraj URL-a već se može koristiti na bilo kojem delu istog.
@Query koristimo kako bismo dodali upitni parametar iza URL-a, takođe možemo koristiti i tipiziranu mapu @QueryMap Map<String, int> params ako želimo da ubacimo višestruke parametre.

Slanje podataka je isto veoma jednostavno, potrebno nam je samo da definišemo metod sa POST anotacijom sa našim podacima u telu kao:

      @FormUrlEncoded
@POST("examples")
Call<User> updateExample(@Field("display_name") String name, @Field("date") String date);

ili

     @Multipart
@POST("examples")
Call<User> updateExample(@Field("thumbnail") RequestBody photo);

@FormUrlEncoded bi trebalo koristiti kada imamo mali broj tekstualnih podataka, kao recimo neke ključ/vrednost parove koje bi korisnik da je u pretraživaču uneo kroz formu. @Multipart bi valjalo koristiti za binarne fajlove kako bismo izbegli dodatno zauzeće memorije pri enkodiranju ne-alphanumerka kada se koristi Url enkoding. Takođe fajl koji želimo da pošaljemo smestili bismo u MultipartBody.Part. Moguće je slati i druge tipove osim RequestBody-a ukoliko iste Retrofit ume da serializuje što zavisi od konvertera koji koristimo (podrazumevano GSON).

Podrazumevana podešavanja Retrofit-a poprilično su optimalna i logična. No ukoliko želimo da ih izmenimo možemo koristiti ulančani Builder patern za to:

      final OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .writeTimeout(60, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .connectTimeout(60, TimeUnit.SECONDS)
    .build();

Retrofit retrofit = new Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .client(okHttpClient)
    .baseUrl("http://example.com/v2")
    .build();

Retrofit je naime toliko modularan da je čak moguće promeniti i sloj (ovde nazvan klijent) koji koristi za HTTP operacije. Podrazumevano ovo je OkHttp još jedna od Square biblioteka otvorenog koda.
Takođe logika serializacije/deserializacije delegira se specijalnim klasama zvanim Converters koje možemo definisati svoje ili koristiti neke od već postojećih:

JSON:

  • Gson: com.squareup.retrofit:converter-gson
  • Jackson: com.squareup.retrofit:converter-jackson
  • Moshi: com.squareup.retrofit:converter-moshi

Protocol Buffers:

  • Protobuf: com.squareup.retrofit:converter-protobuf
  • Wire: com.squareup.retrofit:converter-wire

XML:

  • Simple XML: com.squareup.retrofit:converter-simplexml

Java string-ovi i primitivni tipovi kao i njihove „upakovane“ vrednosti

  • Scalars Converter: com.squareup.retrofit2:converter-scalars:latest.version

Zaključak

Imajući u vidu da mrežna komunikacija na Android-u može biti poprilično komplikovana kao i da obično želimo da vreme trošimo na pisanje koda koji je specifičan za našu aplikaciju a ne povezujuće logike, korišćenje postojećih biblioteka može biti veoma dobro rešenje. Iako Retrofit ovde definitivno nije jedini izbor, najverovatnije jeste najpopularniji i dosta je korišćen u produkciji barem što se tiče povezivanja sa Rest servisima.
I da ne zaboravim da pomenem da sve ove lepote Retrofita možete bez problema koristiti i na Java aplikacijama koje rade na JVM-u.

Share

Prijavi se da prvi dobijaš nove blogove i vesti.

Ostavite odgovor

Igor Čordaš

Senior Android Developer @Endava
mm

Zainteresovan da vidi, nauči i napravi razna zanimljiva rešenja na Android platformi od telefona do frižidera.

Aktivan član domaće IT zajednice u oblastima Java tehnologija, Data Science, Machine Learning, Internet of things i Game Developmenta.

Trenutno angažovan na razvoju sistemskih aplikacija za jednog od vodećih proizvođača mobilnih uređaja.

Prijavi se da prvi dobijaš nove blogove i vesti.

Kategorije