Kurs Android (11)

Autor: Damian Chodorek • Opublikowany: 9 sierpnia 2015 • Ostatnia aktualizacja: 13 sierpnia 2015 • Kategoria: android, kursy

Aplikacja korzystająca z Web Service’ów (usług internetowych). Część 1. Pobieranie danych z serwera.

Czym jest Web Service?

Web Service to standard, który służy do komunikacji pomiędzy heterogenicznymi systemami lub aplikacjami. Jest niezależny od platformy sprzętowej oraz szczegółów implementacji danej funkcjonalności. Zazwyczaj dane przekazywane są przy pomocy protokołu HTTP.

Przykładowo aplikacja stworzona w języku Java, może wysłać odpowiednie parametry do serwera, który stworzony jest w PHP. Ten wywoła odpowiednie funkcje, a następnie zwróci odpowiedź zawierającą dane.

Przy pomocy Web Service’ów można np pobrać dane pogodowe dla danego obszaru lub dokonać synchronizacji pomiędzy urządzeniem a serwerem. Chodzi więc o wymianę danych.

Aby lepiej zrozumieć czym są Web Services rozważmy następujące technologie:

  • CORBA (OMG)technologia zapewniająca komunikację pomiędzy obiektami pracującymi w heterogenicznych (różnorodnych) systemach komputerowych. Obiekty pełniące dowolne funkcje mogą być zaimplementowane w różnych językach programowania, na dowolnej platformie sprzętowej, pod kontrolą różnych systemów operacyjnych. (wikipedia). Potężne narzędzie. Problem tkwi w jego stopniu skomplikowania.
  • DCOM (Microsoft) – rozproszona platforma obliczeniowa ściśle związana z technologiami Microsoftu.
  • RMI (Sun Microsystems) – technologia programowania rozproszonego. Problem tkwi w jej zależności od języka programowania (Javy).
  • Web Services (W3C) – to ewolucja powyższych technologii.

Bardzo często przy opisie Web Service’ów opisuje się protokół SOAP, którym nie będziemy się zajmować w tym artykule, ale warto go znać. W tym celu odsyłam na stronę opisującą jego podstawy: SOAP Introduction.

Dziś opisze Web Services typu REST. REST to wzorzec narzucający dobre praktyki tworzenia architektury aplikacji rozproszonych. Nie jest to protokół ani konkretna technologia, ale zbiór zasad. Krótkie i szybkie wyjaśnienie architektury REST znajdziesz na tej stronie.

Jedyne co musisz wiedzieć, aby kontynuować tę lekcję, to fakt, iż komunikacja z serwerem odbywa się przy pomocy protokołu HTTP oraz jego podstawowych metod:

  • GET – chcesz dostać dane od serwera,
  • POST – chcesz umieścić dane na serwerze,
  • PUT – chcesz zaktualizować dane na serwerze,
  • DELETE – chcesz usunąć dane z serwera.

Załóżmy, że masz adres jakiegoś zasobu (danych): http://damianchodorek.com/cat. W przykładzie zasobem są koty (cat). Jeśli użyjesz tego URL’a z metodą GET, to dostaniesz listę wszystkich kotów. Jeśli skorzystasz z metody POST, musisz wysłać dane, które będą wstawione do tego zbioru danych, czyli nowego kota. Jeśli użyjesz metody DELETE z URL’em http://damianchodorek.com/cat/2, usuniesz kota o identyfikatorze 2. DELETE z z URL’em http://damianchodorek.com/cat/ spowoduje usunięcie wszystkich kotów.

To jak przechowywane są dane (o kotach) oraz jak odbywają się operację ich zapisu czy dodawania, nie interesuje Cię. Ty musisz znać tylko odpowiednie URL’e, a serwer ma zadbać o resztę. Jeśli pobierasz dane GET na URL’u http://damianchodorek.com/cat/2 – dostaniesz informacje o kocie, który ma identyfikator 2. W jakiej postaci? Wzorzec REST tego nie określa. Może to być XML:

<cats>
    <cat>
        <id>2</id>
        <name>edgar</name>
    </cat>
</cats>

lub JSON:

{
    "id": 2,
    "name": "edgar"
}

Jeśli chodzi o REST, należy jeszcze pamiętać, że każde zapytanie jest samowystarczalne. Sesja pomiędzy klientem a serwerem nie jest utrzymywana. Każde pojedyncze zapytanie zawiera wszelkie informacje potrzebne do jego przetworzenia przez serwer.

Web Service w stylu REST jest więc łatwym sposobem na komunikację twojej aplikacji Androidowej z serwerem. Cały proces wymaga trzech kroków:

  • wykorzystanie odpowiedniego URL’a do nawiązania połączenia z serwerem,
  • odebranie odpowiedzi od serwera,
  • przetworzenie i wykorzystanie odpowiedzi na użytek aplikacji.

Layout aplikacji

Najpierw stwórzmy graficzny interfejs użytkownika. Aplikacja będzie prosta więc interfejs również będzie prosty. Stwórz nowy projekt o nazwie com.example.com.damianchodorek.kurs.android11. Dokonaj edycji pliku res\layout\activity_main.xml w następujący sposób:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/start_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Start" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Odpowiedź serwera:"
        android:textSize="20sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/response_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="id: "
        android:textSize="15sp" />

    <TextView
        android:id="@+id/response_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="name: "
        android:textSize="15sp" />

</LinearLayout>

Nawiązanie połączenia z serwerem

W tym celu skorzystamy z klasy AsyncTask, która przeznaczona jest do wykonywania czasochłonnych operacji w tle oraz aktualizacji interfejsu graficznego. Dlaczego nadaje się idealnie do tego przykładu? Ponieważ naszym zadaniem jest połączenie się z serwerem, które musi zostać wykonane w osobnym wątku. Dane, które zostaną odebrane, będą wyświetlone na ekranie.

Do nawiązania połączenia wykorzystamy klasę URL oraz jej metodę openConnection(). Poniżej prosty przykład klasy AsyncTask z ważnymi komentarzami.

public class MainActivity extends Activity {

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

        // TO DO
    }

    private class WebServiceHandler extends AsyncTask<String, Void, String> {

        // okienko dialogowe, które każe użytkownikowi czekać
        private ProgressDialog dialog = new ProgressDialog(MainActivity.this);

        // metoda wykonywana jest zaraz przed główną operacją (doInBackground())
        // mamy w niej dostęp do elementów UI
        @Override
        protected void onPreExecute() {
            // wyświetlamy okienko dialogowe każące czekać
            dialog.setMessage("Czekaj...");
            dialog.show();
        }

        // główna operacja, która wykona się w osobnym wątku
        // nie ma w niej dostępu do elementów UI
        @Override
        protected String doInBackground(String... urls) {

            try {
                // zakładamy, że jest tylko jeden URL
                URL url = new URL(urls[0]);
                URLConnection connection = url.openConnection();

                // pobranie danych do InputStream
                InputStream in = new BufferedInputStream(
                        connection.getInputStream());

                // konwersja InputStream na String
                // wynik będzie przekazany do metody onPostExecute()
                return streamToString(in);

            } catch (Exception e) {
                // obsłuż wyjątek
                Log.d(MainActivity.class.getSimpleName(), e.toString());
                return null;
            }

        }

        // metoda wykonuje się po zakończeniu metody głównej,
        // której wynik będzie przekazany;
        // w tej metodzie mamy dostęp do UI
        @Override
        protected void onPostExecute(String result) {

            // chowamy okno dialogowe
            dialog.dismiss();

            try {
                // reprezentacja obiektu JSON w Javie
                JSONObject json = new JSONObject(result);

                // pobranie pól obiektu JSON i wyświetlenie ich na ekranie
                ((TextView) findViewById(R.id.response_id)).setText("id: "
                        + json.optString("id"));
                ((TextView) findViewById(R.id.response_name)).setText("name: "
                        + json.optString("name"));

            } catch (Exception e) {
                // obsłuż wyjątek
                Log.d(MainActivity.class.getSimpleName(), e.toString());
            }
        }
    }

    // konwersja z InputStream do String
    public static String streamToString(InputStream is) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder stringBuilder = new StringBuilder();
        String line = null;

        try {

            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line + "\n");
            }

            reader.close();

        } catch (IOException e) {
            // obsłuż wyjątek
            Log.d(MainActivity.class.getSimpleName(), e.toString());
        }

        return stringBuilder.toString();
    }

}

Stworzyliśmy klasę. Powinniśmy ją wywołać tak: new WebServiceHandler().execute(). Kiedy to uczynimy zostanie najpierw wywołana metoda onPreExecute(), następnie doInBackground(). Kiedy zostanie zakończona, jej wynik zostanie zwrócony do następnej metody: onPostExecute().

Klasa AsyncTaks może również wykonywać aktualizacje UI w trakcie wykonywania operacji w tle, aby powiadomić użytkownika o postępie operacji, jednak tej funkcjonalności nie wykorzystujemy w naszym przykładzie. Klasa jest parametryzowana: AsyncTask<Params, Progress, Result>.

  • Params – typ parametrów, które będą przekazane do głównej operacji.
  • Progress – typ danych, które będą przekazane do aktualizacji postępu operacji.
  • Result – typ danych, które są wynikiem działania operacji głównej.

Zwróć uwagę na wykorzystanie klasy JSONObject. Klasa służy do reprezentacji obiektu JSON w języku Java. Jest to zbiór par klucz-wartość. Jak zapewne się domyślasz – klasa jest często wykorzystywana przy pracy z obiektami JSON.

Do całości brakuje implementacji metody onCreate() w klasie MainActivity:

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

    ((Button) findViewById(R.id.start_button))
            .setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    new WebServiceHandler()
                            .execute("http://damianchodorek.com/wsexample/");
                }
            });
}

Jak widzisz, wykorzystamy przykładowy Web Service (a raczej jego uproszczoną makietę) http://damianchodorek.com/wsexample/, zwracający dane, który stworzyłem na potrzeby tej lekcji. Wejdź na stronę http://damianchodorek.com/wsexample/ i zobacz jaki będzie wynik. Właśnie takie dane w formacie JSON otrzyma aplikacja.

Trzeba jeszcze dodać wpis <uses-permission android:name="android.permission.INTERNET"></uses-permission> w pliku AndroidManifest.xml, który pozwoli nam na połączenie z internetem.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.com.damianchodorek.kurs.android11"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="20" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />z

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>

Na koniec zamieszczam skrypt PHP, który służy do generowania odpowiedzi serwera:

<?

echo 
'{
    "id": '.rand(1, 100).',
    "name":"example_'.rand(1, 100).'"
}';

?>

część 12

9 komentarzy

  • IWantToBeAndroidProgrammer napisał(a):

    Jak wywołać parametr ‚first’ z takiego klucza:
    {„results”:[{„gender”:”male”,”name”:{„title”:”mr”,”first”:”aiden”,”last”:”chen”},”location”:{„street”:”4392 lunn avenue”,”city”:”gisborne”,”state”:”manawatu-wanganui”,”postcode”:47410},”email”:”aiden.chen@example.com”,”login”:{„username”:”smallfish243″,”password”:”hermes”,”salt”:”6rc9bBVt”,”md5″:”6e66dbf5c985cfa1c413982f7d454d70″,”sha1″:”877593c2deb120831641ead6594f98c6a2853d0f”,”sha256″:”db5c81d6dff6529351e85f055afca2f886182f8dd6dfab292489cb509f1556a2″},”registered”:1071742883,”dob”:46211680,”phone”:”(847)-775-6334″,”cell”:”(326)-297-9672″,”id”:{„name”:””,”value”:null},”picture”:{„large”:”https://randomuser.me/api/portraits/men/32.jpg”,”medium”:”https://randomuser.me/api/portraits/med/men/32.jpg”,”thumbnail”:”https://randomuser.me/api/portraits/thumb/men/32.jpg”},”nat”:”NZ”}],”info”:{„seed”:”6a36ac578163a217″,”results”:1,”page”:1,”version”:”1.0″}}

    Klucz pochodzi ze strony api.randomuser.me

  • Początkujący napisał(a):

    Super kurs!! Jak najwięcej takich

  • Michał napisał(a):

    Witam. A jeśli chciałbym aby dane pobrane ze strony same się aktualizowały np. co minutę a nie po użyciu przycisku to co trzeba dodać i gdzie. Pozdrawiam

    • Damian Chodorek napisał(a):

      Witam. Istnieje kilka sposobów na zrobienie czegoś takiego. Najprostszy jaki przychodzi mi do głowy, to wykorzystanie klasy Handler. Klasa MainActivity powinna mieć pole

      Handler handler = new Handler().

      Tworzymy również obiekt Runnable, który będzie odpalał nasze zadanie, np.

      Runnable task = new Runnable(){
      @Override
      public void run(){
      new WebServiceHandler().execute(„http://damianchodorek.com/wsexample/”);
      }
      }

      Następnie w onCreate(), zamiast clickListenera dajemy handler.postDelayed(task, 60000). To sprawi, że za 60s zostanie wywołana akcja pobrania danych. Aby ją ponownie powtórzyć trzeba dodać na samym końcu metody onPostExecute() tę samą linijkę, czyli:

      handler.postDelayed(task, 60000)

      To nam zapętli odświeżanie danych. Nie testowałem tego, lecz wymyśliłem z głowy, aczkolwiek myślę, że możesz popróbować w ten sposób.

      Inny sposób to użycie klasy ScheduledThreadPoolExecutor, która posiada metodę scheduleWithFixedDelay(). Umożliwia ona uruchamianie danej akcji co określony interwał czasu.

  • Mateusz napisał(a):

    Hej, mam pytanie odnośnie zabezpieczania metod wykonujących POSTa do serwera. Jak mogę to zrobić, by można je było wykonywać np. tylko i wyłącznie poprzez kliknięcie przycisku w aplikacji? Nie chcę by ktoś, kto np. z jakiegoś powodu zna URL do web serwisu wstawił mi do bazy danych milion rekordów. Thx

  • Mateusz napisał(a):

    Ok, przeczytałem że można informację o użytkowniku przechowywać w nagłówku requesta i na tej podstawie sprawdzać czy request może być wykonany bądź nie. Czy polecasz takie rozwiązanie, czy lepiej próbować czegoś innego?

    • Damian Chodorek napisał(a):

      Wszystko zależy od tego jaki poziom bezpieczeństwa chcesz utrzymywać. Na pewno domena obsługująca ruch protokołem https jest podstawą. Zamiast bezpośredniej informacji o użytkowniku (jak np. login) można wysyłać do serwera jakiś token, który został wygenerowany przy logowaniu (dzięki temu klient nie musi pamiętać loginu i hasła a jedynie znać token) i który jest ważny np. przez godzinę. Temat bezpieczeństwa jest dosyć złożony i wymaga większego zgłębienia. Wracając bezpośrednio do Twojej odpowiedzi – informację o użytkowniku można przesyłać w nagłówku lub w ciastku. Mniej polecane jest w adresie URL. Najważniejsze, aby komunikacja z serwerem była odpowiednio zaszyfrowana.

  • wiktorl4z napisał(a):

    Cześć, skopiowałem kod dodałem internet do manifestu i w onPostExecute zwraca nulla :

  • Daniel napisał(a):

    Cześć.
    Brakuje nawiasu while ((line = reader.readLine()) != null) {
    stringBuilder.append(line + "n");
    Po za tym funkcjonalność

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.