180 minút s Androidom: 8. stretnutie [Online hlasovanie]

Ukážková aplikácia

Vytvorme aplikáciu, v ktorej môžeme hlasovať pre danú otázku buď áno alebo nie. Otázky sa nachádzajú na serveri a sú dostupné cez REST API (JSON).

Výsledok

android-votr-01

Koncepty, ktoré zvládneme

  • loadery vlastných dát
    • kedysi sme používali kurzorové loadery, dnes si napíšeme vlastný
  • komunikácia cez HTTP
  • zabudovaná knižnica pre JSON
  • IntentService pre asynchrónne spracovanie dát
  • broadcastovanie intentov do systému
  • odchytávanie broadcastov v systéme

Návrh GUI

  • dva vnorené lineárne layouty
  • tlačidlá sú reprezentované klikateľnými (android:clickable="true") widgetami typu TextView
  • obom nastavíme android:layout_weight na 1
  • zaberú celú šírku a budú mať rovnakú šírku

Asynchrónne operácie

Dve zásady pri práci s Androidom

  1. dlhotrvajúce operácie nesmú prebiehať v hlavnom vlákne
  2. widgety možno modifikovať len z hlavného vlákna

Zásady sú prevzaté z programovania v Swingu.

  • Ak vykonávame dlhotrvajúce operácie v hlavnom vlákne, po 5 sekundách dostaneme ANR: Application Not Responding
    • máme veľmi nervózneho používateľa
  • V novších Androidoch aplikácia rovno spadne
    • explicitný zákaz sieťového I/O v hlavnom vlákne!

Mnoho nástrojov pre dlhotrvajúce operácie

  • AsyncTask — jednoduché asynchrónne úlohy
  • AsyncTaskLoader — načítavanie zdrojov na pozadí
  • IntentService — služba v duchu fire-and-forget, “zašli operáciu a zabudni”.
  • android.app.DownloadManager — systémový správca sťahovania súborov cez HTTP
    • nespomínal sa na cvičení
  • android.os.CountdownTimer — odpočítavanie bomby ;-)
    • nespomínalo sa na cvičení
  • Handler — manuálne spracovávanie správ vo fronte

AsyncTask

  • spustí úlohu v samostatnom vlákne
    • analógia SwingWorkera z Java Swingu
  • poskytuje metódy na modifikáciu GUI v hlavnom vlákne
    • dodržanie zásady č. 2
  • životný cyklus spriahnutý s aktivitou
    • ak kód v AsyncTasku beží dlhšie než aktivita, po jeho dokončenie môže ovplyvňovať už odstrelenú aktivitu
      • stačí otočiť mobil, aktivita sa zastrelí a vytvorí, ale AsyncTask to neovplyvní
    • napr. sa neprejavia modifikácie widgetov v onPostExecute()
  • pozor na zastavenie (cancellation)
    • periodicky kontrolujeme isCancelled()
    • alebo čakáme na výnimku s prerušením (interrupt) vlákna
    • ak robíme internú neprerušiteľnú operáciu, zastavujeme špinavými trikmi zvonku
      • odstreľujú sa inputstreamy…
  • ľahké na implementáciu, zložité okrajové prípady
  • odporúčané na kratučké operácie, kde až nevadí, že sa aktivita reštartne

AsyncLoadery

  • načítavanie zdrojov na pozadí
    • API Android 4.x+
    • alebo v kompatibilnej knižnici
  • už sme raz videli v CursorLoaderi
  • môžeme však načítavať ľubovoľné iné zdroje

Načítavanie REST API na pozadí

Bojový plán:

  1. načítame z REST API cez HTTP reťazec
  2. transformujeme ho na objekt JSON
  3. ten transformujeme ho na vlastnú triedu Vote reprezentujúcu hlas
  4. to všetko urobíme na pozadí

Práca s HTTP klientom

K dispozícii sú dve rozličné implementácie:

HttpURLConnection

Apache HTTP Client

  • pekné API
  • klasika v Jave
  • prakticky zamrazené
  • menej bugov na 2.0–2.2 než HttpURLConnection
    • ale na tieto verzie už nemusíme dbať
  • nejeden projekt stiahne najnovšiu verziu JARka a zahrnie ju do projektu

Použitie

URL url = new URL(SERVER_URL);
InputStream in = url.openStream();

Práca s JSON

  • k dispozícii je parser JSONu
  • modelovaný podľa org.json
  • pozor, len parser, žiadne magické mapovania na objekty
  • môžeme však dotiahnuť napr. knižnicu GSON
    • domáca úloha

Plán práce

  • parser nevie priamo načítavať InputStream
  • vybudujeme ale reťazec či už Scannerom alebo BufferedReaderom
  • a následne ho pošleme do konštruktora JsonObjectu, ktorý sa z neho naparsuje
  • JSONObject sa správa ako mapa
  • vytiahneme z nej príslušné atribúty a napcháme do inštancie objektu Vote

Kód

public Vote parseVote(InputStream in) throws JSONException, IOException {
    return toVote(new JSONObject(toString(in)));
}   

public Vote toVote(JSONObject jsonVote) throws JSONException {
    Vote vote = new Vote();
    vote.setId(UUID.fromString(jsonVote.getString("id")));
    vote.setName(jsonVote.getString("name"));
    vote.setDescription(jsonVote.getString("description"));
    return vote;
}

protected String toString(InputStream in) throws IOException {
    BufferedReader reader = null;
    try {
        StringBuilder buffer = new StringBuilder();
        reader = new BufferedReader(new InputStreamReader(in));
        String line = null;
        while((line = reader.readLine()) != null) {
            buffer.append(line);
        }
        return buffer.toString();
    } finally {
        if(reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                Log.w(TAG, "Cannot close the reader", e);
            }
        }
    }
}

Použitie v AsyncTaskLoaderi

  • generický parameter T reprezentujúci návratový objekt
  • prekryjeme loadInBackground()
    • metóda beží v separátnom vlákne
  • výsledný Vote vrátime z metódy

Obsluha výnimiek

  • ak nastane výnimka, zalogujeme
  • nesmieme hádzať vlastné výnimky
    • úbohý spracovávač v pracovnom vlákne umrie na výnimke, ktorú nečakal
  • musíme umne vymyslieť návratový typ
    • u nás: vrátime null a nestaráme sa
    • inde: objekt nesúci výsledok i prípadný kód chyby

Kód

package sk.upjs.ics.android.votr;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Scanner;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;

public class VoteLoader extends AsyncTaskLoader<Vote> {

    private static final String TAG = VoteLoader.class.getSimpleName();

    public VoteLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    @Override
    public Vote loadInBackground() {
        Scanner scanner = null;
        try {
            InputStream in = new URL("http://158.197.35.237/votr/votes/1?json").openStream();

            StringBuilder sb = new StringBuilder();
            scanner = new Scanner(in);
            while(scanner.hasNextLine()) {
                sb.append(scanner.nextLine());
            }

            JSONObject json = new JSONObject(sb.toString());

            Vote vote = new Vote();
            vote.setName(json.getString("name"));
            vote.setDescription(json.getString("description"));

            return vote;
        } catch (MalformedURLException e) {
            // nikdy sa nestane, adresa je napevno
        } catch (IOException e) {
            Log.e(TAG, "Cannot load vote due to I/O exception", e);
        } catch (JSONException e) {
            Log.e(TAG, "Cannot load vote due to JSON exception", e);
        } finally {
            if(scanner != null) {
                scanner.close();
            }
        }

        return null;
    }
}

Nevýhody nášho AsyncTaskLoadera

Konzumujeme Loadery v aktivite

  • deja vu: loadery kurzorov
  • držíme sa kompatibilnej knižnice
  • oddedíme od FragmentActivity
  • aktivita nech implementuje android.support.v4.app.LoaderManager.LoaderCallbacks
  • prekryjeme tri callbackové metódy
    • onCreateLoader(): vytvorí inštanciu nášho loadera
    • onLoadFinished(): ak sa objavia načítané dáta
      • narveme ich do widgetov
    • onLoaderReset(): nezaujímame sa o ne

Progress bary

  • nerobili sme na cvičení, ale je to jednoduché a estetické
  • ak máme action bar (lištu akcií) v Android 4.x+, môžeme do nej dať ukazovateľ priebehu

    • točiaci sa kruh
  • v metóde onCreate() poprosíme o vlastnosť okna (window feature) s názvom “neurčitý priebeh

    • nevieme totiž, koľko percent už stiahneme cez HTTP
    • poprosiť musíme ešte pred setContentView()
  • Ukážka:

    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    setContentView(R.layout.activity_main);
    setProgressBarIndeterminateVisibility(true);
    
  • v metóde onLoadFinished() potom schováme ukazovateľ priebehu nastavením na false

Nutné veci pre spustenie

  • nezabudnime spustiť loader:

    getSupportLoaderManager().initLoader(VOTE_LOADER_ID, Bundle.EMPTY, this);
    
  • nezabudnime prideliť v manifeste právo internetu

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

Kód

package sk.upjs.ics.android.votr;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.view.View;
import android.view.Window;
import android.widget.TextView;

public class MainActivity extends FragmentActivity implements LoaderCallbacks<Vote> {
    private static final int VOTE_LOADER_ID = 0;

    private TextView voteNameTextView;
    private TextView voteDescriptionTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        setContentView(R.layout.activity_main);

        setProgressBarIndeterminateVisibility(true);

        voteNameTextView = (TextView) findViewById(R.id.vote_name_text_view);
        voteDescriptionTextView = (TextView) findViewById(R.id.vote_description_text_view);

        getSupportLoaderManager().initLoader(VOTE_LOADER_ID, Bundle.EMPTY, this);
    }

    @Override
    public Loader<Vote> onCreateLoader(int loaderId, Bundle bundle) {
        if(loaderId == VOTE_LOADER_ID) {
            return new VoteLoader(this);
        } 

        return null;
    }

    @Override
    public void onLoadFinished(Loader<Vote> loader, Vote vote) {
        voteNameTextView.setText(vote.getName());
        voteDescriptionTextView.setText(vote.getDescription());

        setProgressBarIndeterminateVisibility(false);
    }

    @Override
    public void onLoaderReset(Loader<Vote> loader) {
        // do nothing
    }
}

Poznámky k loaderom

  • sú korektne späté s cyklom aktivity
  • akurát sú neefektívne, lebo nekešujeme ;-)

Odosielanie do REST API na pozadí

Idea:

  1. opäť komunikácia na pozadí
  2. odošleme hlas a nestaráme sa
  3. maximálne chceme získať toast

IntentService

  • služba, ktorá beží na pozadí
  • postupne prijíma intenty
  • radí ich do frontu
  • a postupne po jednom spracováva v samostatnom vlákne
  • výhoda:
    • neriešim multithreading
    • spracovávanie nebrzdí hlavné vlákno GUI
  • skvelá na jednorazovky, ktoré sú nezávislé od činností používateľa v aktivite
    • use-case: pošli cez HTTP obrázok na server
    • plus ďalšie operácie s notifikáciami
  • pozor na to, že intenty sa spracovávajú po jednom!

  • popis v samostatnom článku spolu s príkladom sťahovania súboru

Implementácia

  • nezabudnime deklarovať v manifeste!

    <service
        android:name=".SubmitVoteIntentService"
        android:exported="false" >
    </service>        
    
  • android:exported="false": služba je dostupná len v rámci našej aplikácie

  • z prichádzajúcej správy reprezentovanej bundlom vyberieme hodnotou pod definovaným kľúčom
    • dohoda: ak zahlasujeme áno, pošleme true, inak false.
  • kód:

    public static final String INTENT_EXTRA_VOTE = "VOTE";
    ...
    @Override
    protected void onHandleIntent(Intent intent) {
        try {
            boolean vote = intent.getBooleanExtra(INTENT_EXTRA_VOTE, false);
            ...
    

Spustenie z aktivity

  1. vytvoríme intent s triedou služby
  2. napcháme do intentu pod dohodnutý kľúč booleovskú hodnotu indikujúcu stav hlasu
  3. intentservice spustíme zároveň s odoslaním správy

Kód:

Intent intent = new Intent(this, SubmitVoteIntentService.class);
intent.putExtra(SubmitVoteIntentService.INTENT_EXTRA_VOTE, vote);
startService(intent);

Komunikácia od IntentService k aktivite

  • služby nemajú používateľské rozhranie
    • vyplýva to z ich definície
  • ale môžu komunikovať s aktivitami

Spôsoby

  • ResultReceiver
    • ak komunikujeme s aplikáciou na jedinom mieste
    • nerobili sme na cviku (domáca úloha)
    • inšpirujte sa článkom Starting Background Services
  • BroadcastManager / BroadcastReceiver
    • do systému, resp. do aplikácie odošleme správu štýlom “rozhlas” a možno ju niekto vyzdvihne a spracuje
    • ten “niekto” bude aktivita

Vysielač BroadcastManager: rozhlasová stanica intentov

  1. vysielačom bude intent service
  2. stačí, keď budeme vysielať do aplikácie (nemusíme do celého systému)
  3. získame lokálny broadcast manager
  4. vytvoríme inštanciu Intentu
  5. dohodneme v nej názov akcie reprezentovaný reťazcom
    • reťazec musí byť unikátny, teda mal by obsahovať menný priestor (balíček)
    • napríklad cez celý názov inštancie Vote
  6. odošleme ho cez sendBroadcast()

Kód

private void broadcastVote(boolean vote) {
    LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
    Intent intent = new Intent(SubmitVoteIntentService.class.getPackage().getName() + ".Vote");
    broadcastManager.sendBroadcast(intent);
}

Metódu broadcastVote() zavolajme na konci onHandleIntent(), čím zaručíme vysielanie správy.

Prijímač BroadcastReceiver

  • dedí od BroadcastReceivera
  • prekrýva onReceive(), kde dostane správu ako Intent
  • v našom prípade prijíma aktivita, teda implementácia bude vnorená trieda, ktorá bude pracovať s GUI
    • vypustí toast

Prijímače broadcastov a GUI

  • prijímač, ktorý zaregistrujeme ručne v aktivite, pobeží v hlavnom vlákne
  • metóda teda musí zbehnúť rýchlo
  • inak kotví hlavné vlákno

Kód

public class VoteBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(MainActivity.this, "Vďaka za hlas!",
                Toast.LENGTH_SHORT).show();
    }
}

Registrácia prijímača

Registrovať môžeme dvoma spôsobmi:

  1. deklaratívne v manifeste
    • na domácu úlohu
    • pre prijímače, ktoré nepotrebujú pracovať s GUI
  2. manuálne
    • v rámci aktivity

Ručná registrácia prijímača

  • v metóde onResume() aktivity ho zaregistrujeme
  • v metóde onPause() ho deregistrujeme
    • neviditeľná aktivita bez GUI nemá byť prečo aktualizovaná, aj tak nebude vidieť výsledok

Filtre intentov

  • broadcast pošle do systému správu
  • potenciálne existuje veľké množstvo komponentov, ktoré ju môžu spracovať
    • aktivity, služby, …
  • aby sa dosiahla rozumná výkonnosť, musíme povedať, ktorým správam náš prijímač porozumie
  • dosiahneme to cez intent filter (filter intentov)
  • príklad filtra, ktorý rozumie len správam s akciou rovnou zadanému reťazcu:

    IntentFilter filter = new IntentFilter(Vote.class.getName());
    

Implementácia registrácie

@Override
protected void onResume() {
    IntentFilter filter = new IntentFilter(Vote.class.getName());
    receiver = new VoteBroadcastReceiver();

    LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);

    super.onResume();
}
  • nezabudnime zavolať rodičovskú implementáciu!
  • nezabudnime poznačiť prijímač do inštančnej premennej, budeme ju potrebovať pri deregistrácii

Implementácia deregistrácie

  • ak neodregistrujeme prijímač, máme memory leak
  • príklad:

    @Override
    protected void onPause() {
        LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
        super.onPause();
    }   
    

Zdroje

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *