Jedným klikom na tlačidlo zverejnite svoju prítomnosť v miestnosti a nájdite svojich priateľov. Ukážeme si sieťovú komunikáciu cez HTTP a prejdeme možnosti pre spúšťanie činností na pozadí.
AsyncTask
pre vykonávanie činností na pozadí, aby aplikácia ostala svižnáAsyncLoader
pre načítavanie dát do adaptérov na pozadíAplikácia ponúkne možnosť oznámiť svetu prítomnosť používateľa. Netreba prepadať paranoji: jednoducho po stlačení tlačidla sa odošle na vzdialený server reťazec s menom prítomného.
Popri tom bude zobrazovať zoznam prítomných v miestnosti, ktorý sa opäť načíta zo servera. Používateľ bude môcť tento zoznam aktualizovať buď ručne, alebo automaticky. Ako bonus periodickej aktualizácie bude používateľ vidieť notifikácie o počte ľudí v miestnosti.
Založme si nový projekt s názvom Presentr a balíčkom
sk.upjs.ics.android.presentr
na platforme Android 4.0.3. a
vytvorme prázdnu aktivitu MainActivity
.
Najprv poďme implementovať ohlasovanie prítomnosti, ktoré v používateľskom rozhraní sprístupnime pomocou tlačidla na action bare. Následne tlačidlo sfunkčníme a v obsluhe kliknutia naň odošleme serveru cez HTTP protokol informáciu o prihlásení. Samotné odosielanie však musíme vykonať na pozadí, aby nebrzdilo používateľské rozhranie.
Do menu_main.xml
dodajme definíciu tlačidla lišty akcií:
<item android:id="@+id/iAmHereAction"
android:title="Som tu!"
app:showAsAction="always" />
Do kódu aktivity dodáme kostru obsluhy kliknutia na tlačidlo:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.iAmHereAction) {
sendPresence();
return true;
}
return super.onOptionsItemSelected(item);
}
private void sendPresence() {
}
Metóda sendPresence()
je zatiaľ prázdna: čaká na kód odoslania prítomnosti!
AsyncTask
¶Zrejme sa zhodneme, že aplikácie by mali byť čo najsvižnejšie: veďže si porovnajte aplikáciu na staršom a pomalšom Androide so svižnými iPhonami!
Vždy, keď máme v úmysle programovať dlhotrvajúcu činnosť, nesmieme to spraviť v hlavnom vlákne, teda priamo v metóde, pretože potom by sme zdržovali prekresľovanie používateľského rozhrania. Namiesto toho musíme spustiť túto činnosť na pozadí, v samostatnom vlákne.
Ak to nedodržíme, uvidíme neslávne známe ANR (Application Not Responding*), ktorý sa prejaví upozornením:
V Logcate navyše uvidíme hlášku
I/Choreographer: Skipped 301 frames! The application may be doing too much work on its main thread.
Štandardne je táto hláška zobrazená po piatich sekundách zdržania používateľského rozhrania.
„Americkí vedci zistili, že“ ľudia nerozpoznajú činnosť s oneskorením menším než desatina sekundy. Všetko dlhšie by sme mohli chápať ako dlhotrvajúcu činnosť. V praxi sa však osvedčí iná úvaha: vždy keď plánujeme pracovať s databázou alebo sieťou, alebo mienime spustiť siahodlhý výpočet. Na druhej strane, ak výpočet potrvá dlhšie, než je plánovaná životnosť aktivity (typicky dlhšie než jednotky sekúnd), budeme musieť použiť iné techniky.
AsyncTask
predstavuje triedu pre asynchrónnu úlohu, teda činnosť spúšťanú na pozadí,
v samostatnom vlákne.
Použitie triedy spočíva predovšetkým vo vytvorení jej podtriedy a prekrytí viacerých metód, spomedzi ktorých nás zaujímajú dve:
doInBackground()
obsahuje kód pre činnosť bežiacu na pozadí. Metóda sa spustí v samostatnom vlákne. onPostExecute()
sa vykoná, keď činnosť na pozadí dobehne. Metóda sa spustí v hlavnom vlákne.Vytvorme teda podtriedu SendPresenceAsyncTask
, ktorá bude dediť od AsyncTask
u. Musíme sa však ešte porozprávať o troch generických parametroch, ktoré reprezentujú postupne:
Naša úloha bude mať na vstupe meno používateľa, ktorého prihlásime, reprezentované reťazcom. Progres nebudeme sledovať, na čo využijeme špeciálny dátový typ Void
(s veľkým V). Výsledok činnosti budeme monitorovať len v rovine "podarilo sa"/"nepodarilo sa prihlásiť", teda Boolean
om.
Triedu deklarujeme ako privátnu vnútornú triedu hlavnej aktivity. Prečo? Kvôli pohodlnosti, čo hneď uvidíme.
private class SendPresenceAsyncTask extends AsyncTask<String, Void, Boolean> {
@Override
protected Boolean doInBackground(String... params) {
String username = params[0];
return true;
}
@Override
protected void onPostExecute(Boolean success) {
String message;
if(success) {
message = "You are here!";
} else {
message = "Try again!";
}
Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT)
.show();
}
}
Naša trieda bude dediť od AsyncTask<String, Void, Boolean>
. Keďže vstupný parameter je reťazcový, v metóde doInBackground()
dostaneme pole reťazcových parametrov (áno, v Jave je String...
chápaný ako premenlivý počet parametrov, ku ktorému pristupujeme ako k poľu). Z nich nás však zaujíma len prvý prvok poľa, ktorý vytiahneme do premennej. Metóda musí vracať Boolean
výsledok (tak sme si to totiž nastavili v treťom generickom parametri).
Ak dobehne doInBackground()
, výsledok (u nás Boolean
˙) sa zjaví ako parameter metódy onPostExecute()
, kde s ním môžeme pracovať. Ak sa podarilo prihlásiť, upozorníme používateľa toastom, a ak nie, tak ... upozorníme používateľa toastom. Všimnite si výhodu vnútornej triedy: keďže vytváranie toastu potrebuje
kontext, vieme pristúpiť k inštancii vonkajšej triedy cez MainActivity.this
.
Vďaka vnútornej triede vidíme všetky inštančné premenné vonkajšej triedy, teda napríklad aj prípadné premenné s widgetmi. Ale pozor!
V metóde doInBackground()
nesmieme pristupovať ku widgetom, ani žiadnym iným zložkám používateľského rozhrania. Táto metóda beží na pozadí a ak by sme túto zásadu nedodržali, môže dôjsť k Naozaj Strašidelným Vláknovým Chybám (NSVCh).
A naopak, metóda onPostExecute
beží v hlavnom vlákne, a teda pokojne môžeme upravovať widgety. Ale opäť pozor, v onPostExecute()
nesmú bežať dlhotrvajúce operácie, pretože inak poprieme hlavný účet AsyncTasku
.
AsyncTask
u¶AsyncTask
vytvoríme a spustíme v našej predpripravenej metóde:
private void sendPresence() {
SendPresenceAsyncTask sendPresenceAsyncTask = new SendPresenceAsyncTask();
sendPresenceAsyncTask.execute("robert1");
}
Jednoducho vytvoríme inštanciu, a následne ju spustíme, pričom do ne pošleme do nej reťazcový parameter s menom používateľa, ktorého máme prihlásiť.
V metóde doInBackground()
odošleme cez HTTP príkaz na server. Na klasickú webovú komunikáciu platí klasická classa: je ňou HttpUrlConnection
z balíčka java.net
. Postup práce bude nasledovný:
URL
reprezentujúci adresu, na ktorú sa budeme pripájaťopenConnection()
, čím získame pripojenie na server. Výsledný všeobecný objekt pretypujeme na HttpURLConnection
. setRequestMethod()
).getInputStream()
získame prúd bajtov s odpoveďou zo serveragetResponseCode()
môžeme testovať stavové kódy odpovede disconnect()
.V našej aplikácii potrebujeme zavolať HTTP POST na zverejnenej adrese služby, pričom nepožadujú sa od nás žiadne ďalšie dáta v tele správy. K tejto adrese akurát musíme prilepiť meno používateľa z parametra metódy doInBackground()
. Z URL
získame HttpUrlConnection
, na ktorom nastavíme HTTP metódu POST
a odpoveď od servera, v podobe stavového kódu získame cez getResponseCode()
.
@Override
protected Boolean doInBackground(String... params) {
HttpURLConnection connection = null;
try {
String username = params[0];
String urlString = "https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users/" + username;
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
if(connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return false;
}
return true;
} catch (IOException e) {
Log.e(getClass().getName(), "Unable to send presence", e);
return false;
} finally {
if(connection != null) {
connection.disconnect();
}
}
}
Všimnime si len, že booleovský výsledok metódy doInBackground()
nastavíme podľa toho, či sa komunikácia so serverom podarila, a či je stavový kód HTTP v poriadku (HTTP OK, 200).
Nezabudnime po skončení práce zatvoriť pripojenie v bloku finally
, ktorý sa vykoná bez ohľadu na to, či nastala výnimka alebo nie! Android totiž optimalizuje prácu s nízkoúrovňovým pripojením cez recykláciu socketov, a bez zatvorenia pripojenia nemôže dôjsť k recyklácii ani uvoľneniu zdrojov.
Ak sa chceme pripojiť k internetu, potrebujeme právo internetu, čiže do manifestu dodáme:
<uses-permission android:name="android.permission.INTERNET" />
V tejto chvíli môžeme aplikáciu spustiť a skúšať posielať svoju prítomnosť na vzdialený server.
Keďže sieťová komunikácia je z princípu pomalá (2G pripojenie so slabým signálom môže viesť k dlhým latenciám), od Androidu 3.0 je zakázané pristupovať k sieti z hlavného vlákna. Ak sa o to pokúsite (napr. pripojením k internetu z obsluhy kliku na tlačidlo), uvidíte výnimku android.os.NetworkOnMainThreadException
.
AsyncTaskmi
¶AsyncTask
¶Ak je aktivita pozastavené (napr. príde na popredie iná aktivita), spustený AsyncTask
sa nepozastaví. Treba si dať na to pozor.
AsyncTask
u bráni uvoľnovaniu pamäte¶AsyncTask
slúži na vykonávanie dlhotrvajúcich úloh, ale pozor! Ak má takáto úloha dlhšiu životnosť než je životnosť aktivity, z ktorej bola spustená, môžu nastať podivné problémy.
Asynchrónna úloha implementovaná ako vnútorná trieda v sebe nesie referenciu na vonkajšiu triedu: koniec-koncov využívame ju pri prístupe ku kontextu cez MainActivity.this
v našom príklade.
Predstavme si situáciu, že sa počas behu asynchrónnej úlohy príde na popredie iná aktivita, a Android sa rozhodne našu MainActivity
zničiť, aby uvoľniť pamäť. Alebo si predstavte situáciu, keď používateľ otočí zariadenie, čo takisto povedie k ničeniu aktivity a jej zneplatneniu. Lenže pozor, stále beží úloha na pozadí, ktorá má silný vzťah s aktivitou! A keďže garbage collector nikdy neuprace objekty, ktoré majú silný vzťah s inými objektami, pamäť sa uvoľní až po dobehnutí úlohy. Na obrázku je tento silný vzťah zobrazený modrastou farbou „mraku“.
AsyncTask
u mení vlastnosti neplatnej aktivity¶Ďalší, očividnejší problém, nastáva, keď má AsyncTask
dlhšiu životnosť než aktivita.
Všimnime si opäť obrázok, ktorý zhora nadol sleduje vývin v čase. Aktivita MainActivity
spustí asynchrónnu AsyncTask
, lenže skôr než úloha dobehne, je zničená a zneplatnená. Android následne vyrobí novú inštanciu aktivity, ktorá sa zobrazí používateľovi. O niečo neskôr dobehne úloha na pozadí, a má sa v onPostExec()
aktualizovať používateľské rozhranie. Lenže... AsyncTask
je úzko spätý s aktivitou, ktorá sa už zneplatnila a ak sa pokúsi meniť jej widgety, používateľ to nikdy neuvidí! Prečo? Lebo používateľ už vidí novšiu, znovuvytvorenú inštanciu. (A ak sa pýtate, či možno vrátiť na popredie staršiu inštanciu, tak to nie je možné, ani to nedáva zmysel.)
Problémy so životnosťou sa riešia špinavými trikmi, ktoré však vyžadujú hlbšie znalosti platformy. Najlepšie riešenie tohto problému je v správnom rozhodnutí, či má AsyncTask
vôbec zmysel. Ak úloha trvá niekoľko málo sekúnd, a nepotrebujeme bezpodmienečne obdivovať jej výsledok v používateľskom rozhraní, sme v suchu. Toto je presne náš prípad: pošleme na server údaje o prihlásení a výsledok zobrazený v Toaste nám úplne postačuje.
Ak úloha trvá dlhšie alebo chceme bezpodmienečne vidieť výsledok či neúspech, musíme použiť iné nástroje.
Keďže už vieme ohlasovať svoju prítomnosť, je čas implementovať aj opačný smer: zisťovať zoznam prihlásených. V prvej verzii vytvoríme ručne aktualizovateľný zoznam, ktorý neskôr vylepšíme o automatické obnovovanie.
Plán práce bude nasledovný:
ArrayAdapter
a zoznamu.ListView
Vzdialená služba poskytuje na adrese https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users zoznam aktuálne prihlásených používateľov vo formáte JSON.
Výstup z našej prezenčnej služby vyzerá nasledovne:
[
{
"login": "novotnyr"
},
{
"login": "mr.been"
},
{
"login": "qwerty0123"
},
]
Služba poskytuje pole používateľov, kde každým prvkom je objekt s jediným atribútom login
, obsahujúcim reťazec.
Vytvoríme si pomocnú triedu PresenceDao
, ktorá poskytne zoznam používateľských mien stiahnutý zo systému. Na pripojenie k serveru využijeme opäť protokol HTTP, v podobe triedy URL
.
Jej tvorba pôjde v nasledovných krokoch:
Na rozdiel od odosielania dát, ktoré sme dokončili, tuto sa pripojíme na server cez HTTP metódu GET
, a nebudeme potrebovať posielať žiadne špeciálne dáta. V takom prípade stačí vytvoriť objekt URL
a zavolať na ňom metódu openStream()
, ktorý vráti prúd dát InputStream
s odpoveďou zo servera. Po načítaní dát prúd zatvoríme.
URL url = new URL(...);
InputStream in = url.openStream();
...
in.close();
Parser JSONu, ktorý použijeme v ďalšom kroku, požaduje vstup v tvare reťazca. Urobíme si teda pomocnú metódu, ktorá prevedie InputStream
na String
:
private String toString(InputStream in) {
Scanner scanner = new Scanner(in, "utf-8");
StringBuilder sb = new StringBuilder();
while(scanner.hasNextLine()) {
sb.append(scanner.nextLine());
}
return sb.toString();
}
Špecifikácia JSONu umožňuje držať štruktúrované dáta v objektoch a poliach, ku ktorým existuje v Androide protipól v podobe triedy JSONObject
, JSONArray
. Každá z týchto tried má konštruktor, vďaka ktorému sa dokáže naparsovať z reťazca.
V našom prípade máme dáta zo servera reprezentované reťazcom JSONu obsahujúcim pole. Vytvoríme teda JSONArray
, kde čakáme, že v premennej json
je reťazec s obsahom zo servera.
JSONArray people = new JSONArray(json);
Cez JSONArray
vieme iterovať tradičným cyklom for
(žiaľ, trieda nepodporuje vylepšenú for-each
syntax. Každý prvok poľa vytiahneme ako JSONObject
, vytiahneme z neho atribút login
a výsledok budeme kumulovať v celkovom zozname reťazcov s menami:
List<String> peopleNames = new ArrayList<String>();
for(int i = 0; i < people.length(); i++) {
JSONObject person = (JSONObject) people.get(i);
String login = (String) person.get("login");
peopleNames.add(login);
}
Metódy JSONovských tried vracajú výnimku JSONException
a to obvykle v prípade, že sa pri parsovaní narazí na syntaktickú chybu. V prípade našej aplikácie ľubovoľnú výnimku odchytíme, vypíšeme do logu a vrátime prázdny zoznam.
Po skončení práce nezabudneme uzavrieť InputStream
z výsledku pomocou metódy close()
.
Celá trieda bude vyzerať nasledovne:
package sk.upjs.ics.android.presentr;
import android.util.*;
import org.json.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class PresenceDao {
public static final String DEFAULT_SERVICE_URL = "https://ics.upjs.sk/~novotnyr/android/demo/presentr/index.php/available-users";
private URL serviceUrl;
public PresenceDao() {
try {
this.serviceUrl = new URL(DEFAULT_SERVICE_URL);
} catch (MalformedURLException e) {
// URL is hardwired and well-formed
}
}
public List<String> loadUsers() {
InputStream in = null;
try {
in = this.serviceUrl.openStream();
String json = toString(in);
JSONArray people = new JSONArray(json);
List<String> peopleNames = new ArrayList<String>();
for(int i = 0; i < people.length(); i++) {
JSONObject person = (JSONObject) people.get(i);
String login = (String) person.get("login");
peopleNames.add(login);
}
return peopleNames;
} catch (IOException e) {
Log.e(getClass().getName(), "I/O Exception while loading users", e);
return Collections.EMPTY_LIST;
} catch (JSONException e) {
Log.e(getClass().getName(), "JSON parsing error while loading users", e);
return Collections.EMPTY_LIST;
} finally {
if(in != null) {
try {
in.close();
} catch (IOException e) {
// nothing can be done at this point
}
}
}
}
private String toString(InputStream in) {
Scanner scanner = new Scanner(in, "utf-8");
StringBuilder sb = new StringBuilder();
while(scanner.hasNextLine()) {
sb.append(scanner.nextLine());
}
return sb.toString();
}
}
Ak chceme asynchrónne načítať dáta z ľubovoľného dátového zdroja a výsledok spracovať v aktivite, pričom chceme rešpektovať životný cyklus aktivity a jeho záludnosti, môžeme použiť známy mechanizmus loaderov. Doposiaľ sme loadovali len dáta z databázy pomocou CursorLoader
a.
Nemusíme sa však obmedzovať len na loadery z content providerov: vieme si totiž pripraviť aj vlastnú implementáciu loadera, ktorá bude vyťahovať dáta zo vzdialeného servera.
Pri implementácii loadera využijeme odporúčanie Dianne Hackborne: „prosím, prosím, prosím, prosím, pozrite sa do zdrojákov CursorLoader
a a prepíšte ich”.
AbstractObjectLoader
¶Stiahnime si teda kostru v podobe triedy AbstractObjectLoader
, pridajme ju do projektu a následne vytvoríme jej podtriedu, kde budeme ťahať zoznam používateľov z webu.
AbstractObjectLoader
¶Náš loader teda bude dediť od AbstractObjectLoader<List<String>>
, pričom generický typ zodpovedá zoznamu reťazcov (s menami), ktoré budeme načítavať.
Implementácia kódu bude jednoduchá: využijeme náš PresenceDao
, a v metóde doInBackground()
, ktorá sa vykoná na pozadí, v samostatnom vlákne, vrátime zoznam používateľkých mien.
package sk.upjs.ics.android.presentr;
import android.content.Context;
import java.util.List;
public class PresenceLoader extends AbstractObjectLoader<List<String>> {
private final PresenceDao presenceDao;
public PresenceLoader(Context context) {
super(context);
this.presenceDao = new PresenceDao();
}
@Override
public List<String> loadInBackground() {
return this.presenceDao.loadUsers();
}
}
Ak máme vlastný loader, s aktivitou ho prepojíme tradičným spôsobom cez LoaderCallbacks
, a metódu initLoader()
a spol.
Aktivita nech implementuje LoaderManager.LoaderCallbacks<List<String>>
(dáta sú reprezentované zoznamom), v metóde onCreate()
inicializujeme loader:
getLoaderManager().initLoader(USER_LIST_LOADER, Bundle.EMPTY, this);
a dodáme implementáciu pre tri metódy callbackov loadera. V prvej vyrobíme novú inštanciu nášho PresenceLoader
a, a ďalšie dve necháme prázdne do momentu, kým si nepridáme používateľské rozhranie (budeme v nich pracovať s adaptérom zoznamu).
@Override
public Loader<List<String>> onCreateLoader(int id, Bundle args) {
return new PresenceLoader(this);
}
@Override
public void onLoadFinished(Loader<List<String>> loader, List<String> data) {
}
@Override
public void onLoaderReset(Loader<List<String>> loader) {
}
Jadro aktivity bude tvoriť jediný zoznam ListView
. Do layoutového súboru teda dodáme:
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<ListView
android:id="@+id/peopleListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</RelativeLayout>
Do kódu aktivity dodáme inštančnú premennú pre zoznam a ihneď dodáme aj adaptér.
Kým v prípade dát z databázy sme používali CursorAdapter
, tu sa môžeme vrátiť k jednoduchému adaptéru pracujúcemu nad poľom či zoznamom, teda k ArrayAdapter
u. Dáta z loadera sú totiž pevné a menia sa len vtedy, ak prídu zo servera nanovo.
Dodajme teda do aktivity získavanie ListView
, a vytváranie adaptéra, ktorý si poznačíme do inštančnej premennej
public static final int USER_LIST_LOADER = 0;
private ListView peopleListView;
private ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
peopleListView = (ListView) findViewById(R.id.peopleListView);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
peopleListView.setAdapter(adapter);
getLoaderManager().initLoader(USER_LIST_LOADER, Bundle.EMPTY, this);
}
Teraz máme k dispozícii takmer všetko: i loader, i adaptér a poďme ich prepojiť:
Doplňme teda do metód onLoadFinished()
a onLoaderReset()
príslušný kód.
Prvá metóda bude jednoduchá: získame zoznam používateľských mien z loadera a keďže poľový adaptér má podobné metódy ako zoznam List
, jednoducho do adaptéra vložíme nové dáta a ešte predtým pre istotu zoznam premažeme.
@Override
public void onLoadFinished(Loader<List<String>> loader, List<String> presentUsers) {
this.adapter.clear();
this.adapter.addAll(presentUsers);
}
Druhá metóda, volaná pri zneplatnení dát, jednoducho odstráni dáta z adaptéra.
@Override
public void onLoaderReset(Loader<List<String>> loader) {
this.adapter.clear();
}
Aktualizácia zoznamu ListView
sa bude diať priamo. Adaptér ListView
vie, že po pridaní prvku má upozorniť widget, ktorý z neho číta dáta, že je potrebné sa prekresliť.
Dáta sa teraz načítajú hneď po štarte aktivity. Dodajme si však tlačidlo, ktorým vieme vyvolať aktualizáciu dát.
Predovšetkým, dodajme novú položku do definičného súboru action baru, teda do menu_main.xml
. Položku schováme do dodatočných akcií, čo povedie k tomu, že na lište bude stále len jedno tlačidlo.
<item android:id="@+id/refreshPeopleAction"
android:title="Obnoviť"
app:showAsAction="never" />
Potom upravme obsluhu kliknutia na tlačidlo na lište akcií:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch(id) {
case R.id.iAmHereAction:
sendPresence();
return true;
case R.id.refreshPeopleAction:
refreshPeople();
return true;
}
return super.onOptionsItemSelected(item);
}
Metóda refreshPeople
bude zatiaľ prázdna:
private void refreshPeople() {
}
Ako obnoviť zoznam dát?
Doposiaľ sme loader vždy len štartovali cez loader managera a jeho metódu initLoader()
. V aplikácii však možno loadery aj reštartnúť, čo znamená, že sa predošlé dáta zahodia a namiesto nich sa objavia čerstvé nové dáta z loadera.
Ak by bol svet krásny, tento kód by fungoval:
private void refreshPeople() {
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}
Žiaľ, realita ukazuje, že v životnom cykle Loader
ov je chyba. Namiesto očakávaného cyklu onLoaderReset()
-> onLoadFinished()
sa však dejú čudesné veci, ktoré nie sú veľmi dobre zdokumentované.
Nie je vôbec jasné, kedy sa volá metóda onLoaderReset()
. Nedá sa spoľahnúť na to, že reštart loadera povedie k volaniu onLoaderReset()
.
Spoľahlivé riešenie vedie k preventívnemu ručnému uprataniu dát. Buď zavoláme metódu onLoaderReset()
ručne, alebo, ako v tomto prípade, vyčistíme adaptér.
private void refreshPeople() {
this.adapter.clear();
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}
Aplikácia beží! Nielenže pri štarte načítava nové dáta, ale po kliknutí na tlačidlo v lište akcií vie dáta ručne aktualizovať!
V Androide nemáte len aplikácie pozostávajúce z aktivít, do ktorých môžete ďubať prstom. A nejde len o prípady, keď je aktivita pauznutá (lebo je prekrytá inou aktivitou) alebo zničená (lebo sa vyčerpali systémové zdroje). Popri nich si radostne na pozadí cvrliká množstvo služieb, ktoré nepotrebujú žiadne používateľské rozhranie: budíky, ktoré čakajú na moment, keď vás o 6:30 vyženú z postele; kontrola (G)mailu, a mnoho iných, ktoré nevnímate dovtedy, kým o sebe nedajú vedieť notifikáciou.
Služby (services) sú presne tým komponentom, ktorý môže bežať na pozadí, uskutočňovať dlhotrvajúce operácie a to bez toho, aby potrebovali kamaráta v podobe aktivity, teda bez toho, aby vyžadovali používateľské rozhranie.
Naša služba bude robiť to isté, čo robí náš loader: bude na pozadí sťahovať zoznam prihlásených ľudí. Lenže nebude to úplný duplikát, a ani to nespraví z loadera zbytočnosť. Služby, na rozdiel, od loaderov, budeme môcť periodicky spúšťať, čo si ukážeme neskôr. A ako bonus si predvedieme, ako môže služba upozorňovať používateľa na dôležité udalosti pomocou systémovej notifikácie.
IntentService
¶Najjednoduchším typom služby je IntentService
, ktorá sa presne hodí na prípad, keď:
Tutoriálovým príkladom je odosielanie fotky na server: je to činnosť, ktorá je príliš dlhá na vykonanie v AsyncTasku
, a zakázaná v hlavnom vlákne.
PresenceService
, ktorá oddedí od IntentService
.onHandleIntent()
Vytvorme v projekte prázdnu triedu a naplňme ju kódom:
package sk.upjs.ics.android.presentr;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
public class PresenceService extends IntentService {
public static final String WORKER_THREAD_NAME = "PresenceService";
public PresenceService() {
super(WORKER_THREAD_NAME);
}
@Override
protected void onHandleIntent(Intent intent) {
Log.i(getClass().getName(), "Downloading users...");
}
}
Implicitný konštruktor musí byť prítomný: bez neho sa nebude dať vytvoriť inštancia služby. V tomto prípade sme povinní zavolať rodičovský konštruktor s reťazcom popisujúcim názov vlákna, v ktorom služba pobeží. (Je to kvôli ladiacim dôvodom.)
onHandleIntent()
¶Jadrom je metóda onHandleIntent()
, v ktorej sa nachádza samotný kód služby, ktorý má bežať na pozadí. Zatiaľ sa toho veľa neudeje: jeden výpis do logu, ktorý časom vylepšíme.
Služby sú popri aktivitách základnými komponentami aplikácie a je nutné ich uviesť do manifestu. Do elementu <application>
v manifeste zaveďme nový element <service>
s názvom triedy našej služby:
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<service android:name=".PresenceService" />
</application>
Službu odpáľme z aktivity, z metódy onCreate()
. Spustenie dosiahneme zaslaním správy, teda intentu, podobne ako keby sme chceli spustiť novú aktivitu. Vytvoríme teda nový intent s názvom triedy služby:
Intent startPresenceService = new Intent(this, PresenceService.class);
a pomocou startService()
ju naštartujeme.
V príklade máme metódu runServiceButtonClick()
, ktorá sa zavolá po kliknutí na tlačidlo (prepojenie je vďaka atribútu android:onClick="runServiceButtonClick"
v XML layoute aktivity):
public class MainActivity extends Activity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent startPresenceService = new Intent(this, PresenceService.class);
startService(startPresenceService);
}
...
Služba sa spustí na pozadí, vypíše do logu jeden riadok a ukončí sa.
sk.upjs.ics.android.presentr I/sk.upjs.ics.android.presentr.PresenceService﹕ Downloading users...
IntentService
?¶Po naštartovaní nášho IntentService
sa v systéme vytvorí jediná inštancia, ktorá spustí samostatné vlákno (tzv. worker thread), ktorý bude čakať na prichádzajúce intenty. Každý prichádzajúci intent (teda každý intent, ktorý aktivita poslala cez startService()
) sa zaradí do frontu a postupne sa spracováva v metóde onHandleIntent()
. Ak by sme službu spúšťali klikaním na tlačidlo, na ktoré by sme zbesilo poklikali trikrát v tesnom závese, spôsobili by sme odoslanie troch intentov, ktoré sa postupne spracujú.
Ak náš intent service spracuje všetky intenty čakajúce vo fronte, automaticky sa ukončí.
Chcete to vidieť v akcii?
Ak prekryjeme onCreate()
, vieme reagovať na situáciu, keď sa služba vytvorí:
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "Presence service created");
}
Takisto vieme reagovať na moment, keď sa služba plánuje zničiť, či už preto, že dobehla, alebo preto, že sa v systéme minuli zdroje a treba ju odstreliť:
@Override
public void onDestroy() {
Log.i(LOG_TAG, "Presence service destroyed");
super.onDestroy();
}
V oboch prípadoch nezabúdame volať rodičovské metódy!
Pri štarte aktivity uvidíme, že sa spustí služba, vykoná sa výpis do logu a ihneď sa služba ukončí.
Ak má služba kontaktovať server a sťahovať dáta, musíme dodať príslušný kód. Opäť využijeme PresenceDao
, ktorý pridáme do nášho intent servicu ako inštančnú premennú:
private PresenceDao presenceDao = new PresenceDao();
Použitie v metóde bude jednoduché:
@Override
protected void onHandleIntent(Intent intent) {
List<String> users = presenceDao.loadUsers();
Log.i(getClass().getName(), "Downloaded user list: " + users.size() + " are present");
}
Niektoré služby naozaj nepotrebujú používateľské rozhranie v podobe ťažkotonážnej aktivity. (Napr. sťahovanie súboru nepotrebuje aktivitu, ktorá zaberie celý displej progressbarom...) Napriek tomu je často vhodné indikovať, že sa niečo na pozadí naozaj deje: nie je nič horšie než naspúšťať stopäťdesiat skrytých služieb, ktoré zožerú všetku baterku a nechať používateľa sa veľmi diviť, čo sa to všetko s jeho mobilom deje.
Notifikácie sú výbornou možnosťou, ako nemať GUI a zároveň indikovať progres.
Budeme potrebovať:
NotificationManager
a, čo je správca notifikácií zodpovedný za ich zobrazovanie, skrývanie a obsluhu udalostí.Notification.Builder
, ktorý vybuduje notifikáciu Notification
.NotificationManager
a.Do služby hoďme pomocnú metódu, ktorá zobrazí notifikáciu:
private void triggerNotification(List<String> users) {
Notification notification = new Notification.Builder(this)
.setContentTitle("Presentr")
.setContentText("Počet ľudí v miestnosti: " + users.size())
.setContentIntent(getEmptyNotificationContentIntent())
.setTicker("Presentr")
.setAutoCancel(true)
.setSmallIcon(R.mipmap.ic_launcher)
.getNotification();
NotificationManager notificationManager
= (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify("Presentr", 0, notification);
}
Rozoberme si postupne túto metódu.
Na vybudovanie notifikácie sa nepoužíva konštruktor, ale builder, teda pomocná trieda Notification.Builder
, ktorá umožňuje nastaviť všakovaké vlastnosti notifikácie a následne získať hotový objekt. (Kiežby sa builder používal aj pri príprave SQL dopytov...)
Postupne nastavujeme:
Nezabudnime po vybudovaní získať objekt notifikácie pomocou metódy getNotification()
.
Objekt notifikácie musí obsahovať prinajmenšom:
setSmallIcon()
)setContentTitle()
)setContentText()
)Ak niektorú z týchto požadovaných vlastností vynecháte, notifikácia sa jednoducho nezobrazí, čo môže byť dosť frustrujúce.
Notifikácie nemusia slúžiť len na zobrazovanie informácií. Neraz sa čaká, že po kliknutí na ne sa spustí ďalšia aktivita: ak príde notifikácia o SMSke, asi si tú správu chcete prečítať; ak sa zjaví notifikácia o neprijatom hovore, chcete možno zavolať danému človeku.
Na to slúži content intent: jednoducho deklarujete intent, ktorý sa odošle po kliknutí na správu.
To však nie je náš prípad: u nás sa na content intent vykašleme. Ak používateľ klikne na notifikáciu, nestane sa nič špeciálne: notifikácia sa zruší vďaka nastavenému auto cancel.
Content intent však nemôžeme vynechať: Android sa začne sťažovať výnimkou. Na vytvorenie prázdneho intentu poslúži pomocná metóda:
public PendingIntent getEmptyNotificationContentIntent() {
int REQUEST_CODE = 0;
int NO_FLAGS = 0;
PendingIntent contentIntent = PendingIntent.getActivity(this, REQUEST_CODE, new Intent(), NO_FLAGS);
return contentIntent;
}
Pending intent predstavuje akéhosi prostredníka, ktorý umožní odoslať intent s takými právami a za rovnakých okolností, ako keby to robila samotná aplikácia, a to i vtedy, ak príslušná aktivita už/ešte nebeží. (Nezabudnite, pokojne sa môže stať, že download beží, i keď GUI aktivity neexistuje.).
Inými slovami, pending intent je bežný intent, ku ktorému priradíme kľúč (token) umožňujúce „otvoriť pomyselné dvere aktivity tak, akoby to robila samotná aplikácia“.
Vytvoríme teda inštanciu pending intentu nad prázdnym intentom (new Intent()
), s implicitnými nulovými hodnotami v ostatných parametroch a so štandardným kontextom (keďže služba je zároveň kontextom).
NotificationManager
a¶Získanie manažéra notifikácií je jednoduché: ide o systémovú službu:
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Toto je jednoduché:
notificationManager.notify(USER_LIST_NOTIFICATION_ID, notification);
Metóda notify()
potrebuje dva parametre: číslo identifikujúce konkrétnu notifikáciu a samotný objekt notifikácie. ID notifikácie je unikátne pre celú aplikáciu a dá sa využiť na aktualizáciu existujúcej notifikácie: stačí druhýkrát notify
kovať manažéra s identickým identifikátorom a aktualizovaným objektom Notification
.
Použitie v kóde jednoducho vytiahne zoznam používateľských mien a pošle ho do našej pomocnej metódy.
@Override
protected void onHandleIntent(Intent intent) {
List<String> users = presenceDao.loadUsers();
Log.i(getClass().getName(), "Downloaded user list: " + users.size() + " are present");
triggerNotification(users);
}
IntentService
k aktivite¶Napriek tomu, že služby nemajú používateľské rozhranie, a typicky sa s používateľom „rozprávajú“ cez notifikácie, existujú situácie, keď chceme, aby komunikovali s používateľmi.
Priamočiary spôsob využíva vzor broadcast manager-broadcast receiver, kde sa do „éteru“ vysielajú správy reprezentované intentami tak, ako to robí bežný rozhlas. Správy môžu prijímať poslucháči, broadcast receivers, ktorí ich môžu spracovať. V našom prípade bude vysielačom služba a prijímačom aktivita.
Služba môže vysielať správy reprezentované intentami kamsi von, do éteru, a nemusí sa vôbec starať, či ich niekto prijme. Ako máme chápať éter?
Jedna z možností je poslať správu do celého systému, a to pomocou metódy sendBroadcast()
. To je však v tomto prípade prílišné mrhanie prostriedkami.
Ak chceme posielať správu len v rámci komponentov aplikácie, vieme využiť sprostredkovateľa v podobe triedy LocalBroadcastManager
. Do tohto objektu pošleme intent, ktorý sa rozdistribuujeme všetkým zaregistrovaným poslucháčom.
V službe si teda vytvorme pomocnú metódu broadcastPresence()
private void broadcastPresence(List<String> users) {
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
Intent intent = new Intent(PRESENCE_INTENT_ACTION);
intent.putExtra(PRESENCE_INTENT_EXTRAS, (Serializable) users);
broadcastManager.sendBroadcast(intent);
}
Potrebujeme dohodnúť dve záležitosti:
Využijeme pri tom dve konštanty:
public static final String PRESENCE_INTENT_ACTION = PresenceService.class.getPackage().getName() + ".Presence";
public static final String PRESENCE_INTENT_EXTRAS = "userList";
Ak máme inštanciu lokálneho vysielača, cez metódu sendBroadcast()
odošleme intent do éteru.
Metódu broadcastPresence()
zavolajme na konci onHandleIntent()
, čím zaručíme vysielanie správy.
BroadcastReceiver
¶Prijímačom broadcastovaných intentov je podtrieda triedy BroadcastReceiver
, ktorá v metóde onReceive()
spracováva prijatý intent. V našom prípade bude prijímačom vnorená trieda, ktorá môže aktualizovať zoznam používateľov. (Samozrejme, bude duplikovať loader, ale opäť: to všetko je príprava na periodické spúšťanie úlohy.)
Prijímač vytvoríme nasledovne:
onReceive()
Triedu poslucháča vytvoríme vo vnútri MainActivity
. Ako vidno, jednoducho vytiahneme z intentu príslušný zoznam používateľov spod príslušného kľúča v extras a aktualizujeme adaptér.
private class PresenceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
List<String> users = (List<String>) intent.getSerializableExtra(PresenceService.PRESENCE_INTENT_EXTRAS);
adapter.clear();
adapter.addAll(users);
}
}
Prijímač, ktorý zaregistrujeme ručne v aktivite, pobeží v hlavnom vlákne. Metóda onReceive()
preto musí zbehnúť rýchlo (inak brzdí používateľské rozhranie) a platia pre ňu rovnaké obmedzenia, ako sme spomínali pri AsyncTask
u.
Dôkaz tohto tvrdenia sa nachádza v dokumentácii metódy registerReceiver()
triedy Context
.
Poslucháča môžeme registrovať dvoma spôsobmi:
Ručná registrácia prijímača pozostáva z dvoch fáz:
onResume()
poslucháča zaregistrujemeonPause()
ho deregistrujemeAk je aktivita pozastavená, jej používateľské rozhranie je skryté, a preto nie je dôvod, aby prijímala intenty: koniec koncov, výsledok aj tak nebude viditeľný.
Správa odoslaná do éteru môže mať množstvo poslucháčov, ale i naopak: éterom môže putovať obrovské množstvo správ. Aby sa dosiahla rozumná výkonnosť pri spracovávaní správ, musíme povedať, ktorým správam náš prijímač porozumie. Dosiahneme to cez filter intentov.
Intent Filter (filter intentov) predstavuje akýsi firewall na intenty postavený pred aplikačný komponent. Predstavuje pravidlo, ktoré špecifikuje intenty, na ktoré dokáže komponent reagovať, a ktoré je potrebné odfiltrovať.
Filter intentov vie intent prijať či zamietnuť podľa troch vlastností: podľa akcie (action), dát (data) a kategórie (category). My si ukážeme rozhodovanie podľa akcie.
Ak chceme prijímať len intenty odosielané našou službou, stačí prijať tie, ktoré majú nastavenú akciu na konštantu sk.upjs.ics.android.presentr.PresenceService#PRESENCE_INTENT_ACTION
.
Dosiahneme to cez intent filter:
IntentFilter filter = new IntentFilter(PresenceService.PRESENCE_INTENT_ACTION);
Ako sme spomínali vyššie, registráciu poslucháča vykonáme v metóde onResume()
aktivity:
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter(PresenceService.PRESENCE_INTENT_ACTION);
this.presenceBroadcastReceiver = new PresenceBroadcastReceiver();
LocalBroadcastManager.getInstance(this).registerReceiver(this.presenceBroadcastReceiver, filter);
}
Vytvoríme teda intent filter s príslušným pravidlom, následne vytvoríme nášho poslucháča a poznačíme si ho do inštančnej premennej a nakoniec ho zaregistrujeme v lokálnom broadcast manažérovi.
Inštančná premenná je dôležitá, budeme ju totiž potrebovať pri odregistrovávaní poslucháča:
private PresenceBroadcastReceiver presenceBroadcastReceiver;
V Androide platí, že vždy po sebe musíme upratať: inak máme memory leak, teda zaberáme pamäť, ktorá sa nedá uvoľniť. Odregistrovanie urobíme v metóde onPause()
aktivity a bude veľmi jednoduché:
@Override
protected void onPause() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(this.presenceBroadcastReceiver);
super.onPause();
}
Jednoducho odregistrujeme inštanciu poslucháča, ktorú máme v inštančnej premennej.
Poďme teraz k veľkému finále: máme službu, ktorá broadcastuje dáta do aktivity a poďme si ju naplánovať na pravidelné spúšťanie.
Ak chceme spúšťať periodické operácie i v čase, keď aplikácia nebeží, použijeme systémovú službu android.app.AlarmManager
. Operačný systém optimalizuje spúšťanie naplánovaných úloh, pričom berie do úvahu ich prioritu, stav uspatia telefónu a šetrenie baterky.
AlarmManager
a.Vytvorme si samostatnú triedu PresenceScheduler
s metódou schedule()
, ktorá zoberie kontext užitočný vo viacerých metódach, a naplánuje úlohu.
Následne získajme inštanciu AlarmManager
a podobne, ako sme to robili v prípade správcu notifikácií:
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Spustenie služby vykonáme v dvoch fázach. Najprv vytvoríme intent, ktorým spustíme náš intent service:
Intent intent = new Intent(context, PresenceService.class);
Následne ho potrebujeme obaliť do pending intentu, pretože potrebujeme dať alarm manažérovi oprávnenie posielať intenty do služby s rovnakým oprávnením, ako by to robila naša aplikácia.
PendingIntent pendingIntent = PendingIntent.getService(context, SERVICE_REQUEST_CODE, intent, NO_FLAGS);
V tomto PendingIntent
e uvedieme:
NO_FLAGS
rovnú nule.Pending intent následne naplánujeme na alarm manažérovi cez niektorú z metód setXXX()
. Vo všeobecnosti mám e k dispozícii dve metódy:
setRepeating()
plánuje presne, s čo najmenšími odchýlkami, ale je náročnejší na batériu.setInexactRepeating()
plánuje spúšťanie s istou toleranciou.Nám nezáleží na presnosti, preto si zvolíme druhú možnosť a uvedieme parametre:
ELAPSED_REALTIME
bez dodatku, aby sme šetrili baterku.ELAPSED_REALTIME
udáva počet milisekúnd od bootu, vrátane času, keď zariadenie spalo.RTC
udáva počet milisekúnd od začiatku unixovej epochyWAKEUP
: ak zariadenie spí a nastane alarm, zariadenie sa zobudí.Výsledný kód vyzerá nasledovne:
package sk.upjs.ics.android.presentr;
import android.app.*;
import android.content.*;
import android.os.SystemClock;
public class PresenceScheduler {
private static final int SERVICE_REQUEST_CODE = 0;
private static final int NO_FLAGS = 0;
public static void schedule(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, PresenceService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, SERVICE_REQUEST_CODE, intent, NO_FLAGS);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), 7 * 1000, pendingIntent);
}
}
Plánovanie odpálime jednoducho: v hlavnej aktivite v metóde onCreate()
zavoláme:
PresenceScheduler.schedule(this);
Správca alarmov následne začne periodicky spúšťať našu službu, ktorá každých 7 sekúnd stiahne údaje zo servera, broadcastne ich, a následne aktivitový poslucháč prijme zoznam prihlasovacích mien, aktualizuje adaptér, čo povedie k prekresleniu zoznamu.
Prehľad naplánovaných úloh môžeme získať pomocou nástroja adp
, ktorý sa nachádza v inštalačnom adresári, v podadresári sdk/platform-tools
.
Volaním adb shell dumpsys alarm
uvidíme výpis aktívnych alarmov:
Uvidíme tiahly výpis, z ktorého nás bude zaujímať len:
ELAPSED #0: Alarm{5368fe78 type 3 sk.upjs.ics.android.presentr}
type=3 when=+6s28ms repeatInterval=7000 count=1
operation=PendingIntent{53615cb4: PendingIntentRecord{537263e0 sk.upjs.ics.android.presentr startService}}
a
Alarm Stats:
sk.upjs.ics.android.presentr
15ms running, 0 wakeups
28 alarms: flg=0x4 cmp=sk.upjs.ics.android.presentr/.PresenceService
Vidíme, že máme naplánovaný alarm so sedemsekundovou periódou, ktorý zatiaľ prebehol 28 krát.
Účel:
Technický popis:
Problémy:
AsyncTask
môže nechtiac meniť aktivitu, ktorá už neexistuje alebo je neplatná, a používateľ ju už neuvidíAsyncTask
Hack:
Účel:
Technický popis:
Problémy:
onLoadFinished()
sa nedajú meniť fragmentyÚčel:
Technický popis:
Problémy:
Výsledný projekt má zdrojové kódy na GitHub.com.