Kurs Android (14)

Autor: Damian Chodorek • Opublikowany: 18 października 2015 • Ostatnia aktualizacja: 6 stycznia 2016 • Kategoria: android, kursy

Aplikacja korzystająca z Web Service’ów (usług internetowych). Część 3. Wykorzystanie biblioteki Retrofit.

W dwóch niedawnych lekcjach pokazałem jak odbierać i wysyłać dane na serwer (lekcja 11, lekcja 12). Wykorzystałem do tego wyłącznie wbudowane klasy Java i Android. Dziś pokażę jak skorzystać z popularnej biblioteki, która została stworzona do łatwej i przyjemnej obsługi serwisów internetowych. Wszystko co zrobiliśmy poprzednim razem, dziś zrobimy łatwiej, szybciej i lepiej.

Retrofit

Najprościej ujmując – Retrofit to biblioteka, która zamienia API REST’owe na obiekty Javy oraz vice versa. Przy jej pomocy łatwo, szybko i profesjonalnie obsłużysz zapytania do serwisów internetowych. Wykorzystanie biblioteki ułatwia mechanizm adnotacji Java.

W tej lekcji skorzystamy z trzech warstw:

  • POJO (Plain Old Java Object) – to klasy modelu. Posiadają jedynie pola, gettery i settery. Ich zadaniem nie jest wykonywanie skomplikowanych operacji, a jedynie przechowywanie informacji. Retrofit z łatwością konwertuje takie obiekty do/z formatu JSON.
  • Interfejs zapytań do API – to interfejs oparty na bibliotece Retrofit. Będą w nim zdefiniowane metody odpowiadające poszczególnym endpoint’om (przypominam, że endpoint to po prostu jakiś adres, który udostępnia API REST’owe). Do każdej metody Java będzie przypisana metoda HTTP (GET, PUT, POST, DELETE).
  • Adapter REST’owy – klient, którego użyjemy do wykonywania zapytań.

Jak dodać Retrofita do projektu?

Jeśli chodzi o Retrofita to mamy dwie możliwości: wersja 2.0 beta lub 1.9 release. Skorzystamy z wersji stabilnej 1.9. Możemy ją pobrać ze strony: http://mvnrepository.com/artifact/com.squareup.retrofit/retrofit/1.9.0.

Lekcja zrealizowana będzie w środowisku Android Studio. Utwórz nowy projekt i umieść go w pakiecie com.damianchodorek.kurs.android14. System automatyzacji budowy Gradle, który wykorzystywany jest w Android Studio ma to do siebie, że w łatwy i bezbolesny sposób dodasz potrzebną bibliotekę.

W tym celu otwórz plik android14/app/build.gradle. Sekcję dependencies uzupełnij zgodnie z kodem poniżej (w zasadzie to musisz dodać tylko linijkę compile 'com.squareup.retrofit:retrofit:1.9.0'):

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    compile 'com.squareup.retrofit:retrofit:1.9.0'
}

Teraz kliknij Build oraz Rebuild project. Biblioteka zostanie pobrana i wkomponowana w projekt. Jeśli korzystasz z innego środowiska programistycznego, to ze strony http://mvnrepository.com/artifact/com.squareup.retrofit/retrofit/1.9.0 możesz pobrać bibliotekę w formacie JAR.

Pozwolenie na skorzystanie z sieci

Oczywiście jeśli aplikacja ma korzystać z Web Service’u, to musi łączyć się z internetem. Aby to było możliwe w pliku AndroidManifest.xml dodajemy odpowiedni wpis:

<uses-permission android:name="android.permission.INTERNET" />

Tworzymy warstwę modelu

Klasy modelu zostaną zrealizowane w postaci zwykłych prostych POJO. Dane będą odbierane i wysyłane przy pomocy formatu JSON, który jak pamiętamy składa się z par klucz-wartość. Format danych należy więc odwzorować w kodzie naszej aplikacji. Dla przypomnienia dane mają następujący format JSON:

{
    "id" : "some id",
    "name" : "some name"
}

Utwórz nowy pakiet o nazwie pojo i stwórz w nim klasę DataBody. Kod jest następujący:

public class DataBody{
    private String name;
    private int id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

Jak widzisz klasa dokładnie mapuje się na dane w formacie JSON. Skorzystamy z niej zarówno do wysłania jak i odbioru danych.

Interfejs zapytań do API

Tutaj zaczyna się magia Retrofita. Utwórz interfejs o nazwie MyWebService:

public interface MyWebService {
    @GET("/wsexample/") // deklarujemy endpoint oraz metodę
    void getData(Callback<DataBody> pResponse);

    @POST("/wsexample/") // deklarujemy endpoint, metodę oraz dane do wysłania
    void postData(@Body DataBody pBody, Callback<DataBody> pResponse);
}

Powyższy kod powinien być zrozumiały. Adnotacje języka Java posłużyły do zadeklarowania rodzaju metody HTTP. Zdefiniowaliśmy również endpoint czyli punkt docelowy API. Każda metoda posiada parametr Callback<DataBody> pResponse. Zapytania Retrofita są wykonywane asynchronicznie. Kiedy nadejdzie odpowiedź z Web Service’u zostanie wywołana odpowiednia metoda klasy Callback<DataBody>.

Layout aplikacji

Najważniejsze komponenty już prawie gotowe. Czas na layout, który składa się z dwóch przycisków.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button_get"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:text="GET" />

    <Button
        android:id="@+id/button_post"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:text="POST" />

</LinearLayout>

Cała reszta

Na tym etapie zdefiniujemy ciało głównej aktywności. Utworzymy adapter, o którym wcześniej wspomniałem. Do prezentacji wyników skorzystamy z mechanizmu logowania i metody Log.d(). Poniżej klasa MainActivity.

public class MainActivity extends AppCompatActivity {

    // tag, który jest wykorzystany do logowania
    private static final String CLASS_TAG = "MainActivity";

    // adapter REST z Retrofita
    RestAdapter retrofit;
    // nasz interfejs
    MyWebService myWebService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ustawiamy wybrane parametry adaptera
        retrofit = new RestAdapter.Builder()
                 // adres API
                .setEndpoint("http://damianchodorek.com/")
                 // niech Retrofit loguje wszystko co robi
                .setLogLevel(RestAdapter.LogLevel.FULL)   
                .build();

        // tworzymy klienta
        myWebService = retrofit.create(MyWebService.class);

        // ustawiamy przycisk GET
        findViewById(R.id.button_get).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    myWebService.getData(new Callback<DataBody>() {
                        @Override
                        public void success(DataBody myWebServiceResponse, Response response) {
                            Log.d(CLASS_TAG, myWebServiceResponse.getName());
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Log.d(CLASS_TAG, error.getLocalizedMessage());
                        }
                    });

                } catch (Exception e) {
                    Log.d(CLASS_TAG, e.toString());
                }
            }
        });

        // ustawiamy przycisk POST
        findViewById(R.id.button_post).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    // zdefiniujmy dane, które mają by wysłane
                    DataBody body = new DataBody();
                    body.setId(1234);
                    body.setName("my name!");

                    myWebService.postData(body, new Callback<DataBody>() {
                        @Override
                        public void success(DataBody myWebServiceResponse, Response response) {
                            Log.d(CLASS_TAG, myWebServiceResponse.getName());
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Log.d(CLASS_TAG, error.getLocalizedMessage());
                        }
                    });

                } catch (Exception e) {
                    Log.d(CLASS_TAG, e.toString());
                }
            }
        });
    }
}

Na tym etapie program powinien już poprawnie działać. O ile po kliknięciu przycisków nic się nie dzieje na ekranie urządzenia mobilnego, to całkiem sporo komunikatów pojawiło się w logu. Poniższy log jest efektem kliknięcia na przycisk POST.

Warto zwrócić uwagę na kilka wpisów logu.

  • Kolorem czerwonym zaznaczyłem komunikat Retrofita. Mówi on pod jaki adres zostało wysłane zapytanie.
  • Kolorem żółtym zaznaczyłem komunikat mówiący o tym jakie dane wysłano do serwera.
  • Kolorem niebieskim zaznaczyłem dane, które przyszły z serwera.
  • Kolorem zielonym zaznaczyłem wypisaną wartość pola klasy DataBody (myWebServiceResponse.getName()).

Jakie wnioski wynikają z logów? Po pierwsze taki, że dzięki logom Retrofita możemy łatwo prześledzić jego działanie. Tzn. kiedy wysłano zapytanie, jak ono wygląda, kiedy przyszła odpowiedź oraz jaką ma zawartość.

Drugi wniosek jest taki, że bez naszej ingerencji odpowiedź serwera w postaci obiektu JSON została skonwertowana przez Retrofita na obiekt Java. Świadczy o tym kod:

myWebService.postData(body, new Callback() {
    @Override
    public void success(DataBody myWebServiceResponse, Response response) {
        Log.d(CLASS_TAG, myWebServiceResponse.getName());
    }

    @Override
    public void failure(RetrofitError error) {
        Log.d(CLASS_TAG, error.getLocalizedMessage());
    }
});

Jak wspomniałem wcześniej metoda postData() ma dwa parametry. Pierwszy to obiekt DataBody, który Retrofit zamienia na JSON i wysyła na serwer. Drugi parametr to obiekt Callback, którego odpowiednie metody są wywoływane przez Retrofita w przypadku sukcesu lub porażki połączenia z serwerem. W przypadku sukcesu wywoływana jest metoda success(). Dane, które dostaniemy od Web Service’u automatycznie zostaną skonwertowane na obiekt klasy DataBody.

Aby ten mechanizm działał, obiekt DataBody musi być POJO, a więc mieć pola, gettery i settery. Co więcej nazwy pól oraz metod muszą odpowiadać nazwom pól obiektu JSON, który wędruje do/z serwera. Retrofit pozwala jednak na używanie innych nazw, ale musimy skorzystać z adnotacji @SerializedName. Przypuśćmy, że pola naszej klasy Java nie mogą mieć takich nazw jak pola obiektu JSON. Zmiana w kodzie będzie wyglądać następująco:

public class DataBody {
    @SerializedName("name")
    private String myName;
    @SerializedName("id")
    private int myId;

    public String getMyName() {
        return myName;
    }

    public void setMyName(String myName) {
        this.myName = myName;
    }

    public int getMyId() {
        return myId;
    }

    public void setMyId(int myId) {
        this.myId = myId;
    }
}

Bez adnotacji @SerializedName nie powiodłaby się konwersja pomiędzy obiektami Java a JSON i dostalibyśmy błędy.

Kod Web Service’u

Żeby wszystko było jasne, zaprezentuję kod Web Service’u, z którego korzystamy. Został stworzony w PHP. Oto jego prosty skrypt:

<?
switch($_SERVER['REQUEST_METHOD'])
{
    case 'GET': 
        echo 
            '{"id": '.rand(1, 100).',"name":"example_'.rand(1, 100).'"}';
        break;
    case 'POST':
        $data = file_get_contents('php://input');
        $json = json_decode($data);
        $name = $json->{'name'};
        
        if(isset($name) && $name!=''){
            http_response_code(200);
            echo 
                '{"id": '.rand(1, 100).',"name":"'.$name.'"}';
        }else
            // bad request
            http_response_code(400);
        break;
}

?>

Więcej możliwości

Retrofit to biblioteka o potężnych możliwościach. W tym artykule poznałeś tylko jej podstawy. Możesz np. zdefiniować nagłówek user agent przy pomocy setRequestInterceptor(). Możemy również zdefiniować i ustawić własną klasę do obsługi wszystkich błędów za pomocą metody setErrorHandler.

retrofit = new RestAdapter.Builder()
    .setEndpoint("http://damianchodorek.com/")
    .setLogLevel(RestAdapter.LogLevel.FULL)
    .setErrorHandler(/*obiekt klasy obsługi błędów*/)
    .setRequestInterceptor(/*klasa generująca user agent*/)
    .build();

Oczywiście dane, które wysyłamy na serwer nie muszą być w formacie JSON. Możemy wysłać je np. w formacie formularza (tak jak wysłanie formularza HTML’owego) przy pomocy adnotacji @FormUrlEncoded.

@FormUrlEncoded
@POST("/wsexample/")
void postData(@Field("name") String pName, Callback pResponse);

W ten sposób wartość parametru pName zostanie potraktowana jak pole formularza o nazwie name.

Jeśli chodzi o parametry metody GET, to możemy definiować je dynamicznie. Zmienimy nasz przykład, aby endpoint był parametrem metody. Najpierw zmieńmy deklarację metody w interfejsie MyWebService.

@GET("/{endpoint}/")
void getData(@Path("endpoint")String pEndpoint, Callback pResponse);

Dodajemy parametr pEndpoint, którego wartość będzie wstawiona zamiast {endpoint}. Teraz zmieńmy wywołanie metody w głównej aktywności.

myWebService.getData("wsexample", new Callback() {
    @Override
    public void success(DataBody myWebServiceResponse, Response response) {
        Log.d(CLASS_TAG, myWebServiceResponse.getName());
    }

    @Override
    public void failure(RetrofitError error) {
        Log.d(CLASS_TAG, error.getLocalizedMessage());
    }
 });

Oczywiście rzadko parametrem jest adres endpoint’u. To jedynie prosty przykład. W praktyce parametrem jest np. id jakiegoś zasobu:

@GET("/group/{id}/users")
List groupList(@Path("id") int groupId, @Query("sort") String sort);

Oczywiście adnotacja @Query odpowiada za doklejenie odpowiednich parametrów do URL’a po znaku ?, np.: /group/1234341/users?sort=asc.

Retrofit pozwala nawet modyfikować nagłówek HTTP przy pomocy adnotacji @Headers.

Jak wspomniałem wcześniej, możliwości biblioteki są ogromne. W artykule poznałeś jej podstawy oraz przejrzeliśmy najważniejsze i najczęściej używane funkcjonalności. To z pewnością wystarczy Ci do obsługi nawet zaawansowanych API. Jeśli czytałeś lekcję 11 oraz lekcję 12, to zauważyłeś, że aplikacja o tej samej funkcjonalności została uproszczona w znaczny sposób przez bibliotekę Retrofit. Dodatkowo mamy pewność, że nasza obsługa protokołu HTTP jest poprawna.

część 15

7 komentarzy

  • phoenix37 napisał(a):

    Hej,
    Po zaimplementowaniu przedstawionego tutaj przykładu mam błąd:
    Retrofit: java.net.SocketTimeoutException: failed to connect to /192.168.213.1 (port 80) after 15000ms

    Pomożesz ?

    • Damian Chodorek napisał(a):

      Cześć. Wyjątek java.net.SocketTimeoutException przeważnie oznacza, że nawiązanie połączenia z serwerem trwało zbyt długo i Retrofit postanowił je zerwać. Często jest to błąd tymczasowy. Spotkałem się również z przypadkiem, że na telefonie, który posiadał kartę sim bez środków na koncie (więc nie było dostępnego internetu), wszystkie zapytania Retrofita kończyły się własnie tym błędem. Dla adresu „http://damianchodorek.com/wsexample/” taki się pojawił?

  • Adrian napisał(a):

    Świetny artykuł, jednak czy mógłbyś dopisać jakiś przykład dla tablicy elementów? ;)

  • Adrian napisał(a):

    Da się zrobić coś takiego że w jsonie mam
    array(‚error’=>1, body=>array());
    i aby w onResponse sprawdzało czy error = 1
    czy musiałbym zaimplementować nową klasę:

    public class onResponseRetrofit {
    @SerializedName(„error”)
    private ErrorInfo error;

    @SerializedName(„articleCategoryList”)
    private ArticleCategoryList articleCategoryList;
    }

    • Damian Chodorek napisał(a):

      Jeśli chodzi o tablice w jsonie to nie ma problemu. W ciele głównej klasy deklarujemy tablicę np, int [] lub MyClass [] i Retrofit sam spróbuje zmapować tablicę jsonową na Javową :)

  • wiktorl4z napisał(a):

    Cześć!
    Niestety ale trochę się w kodzie pozmieniało. Zrobiłem aplikację na podstawie bloga ale z nowym kodem. Kod udostępniłem na githubie.
    https://github.com/Wiktorl4z/Retrofit-for-Android
    Pozdro

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.