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
- stiahnite si serverovskú časť pre REST API implementovanú pomocou Spring Boot
- stiahnite si zdrojové kódy pre čast Androidu
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 typuTextView
- 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
- dlhotrvajúce operácie nesmú prebiehať v hlavnom vlákne
- 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 úlohyAsyncTaskLoader
— 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í
- stačí otočiť mobil, aktivita sa zastrelí a vytvorí, ale
- napr. sa neprejavia modifikácie widgetov v
onPostExecute()
- ak kód v AsyncTasku beží dlhšie než aktivita, po jeho dokončenie môže ovplyvňovať už odstrelenú aktivitu
- 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…
- periodicky kontrolujeme
- ľ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
CursorLoader
i - môžeme však načítavať ľubovoľné iné zdroje
Načítavanie REST API na pozadí
Bojový plán:
- načítame z REST API cez HTTP reťazec
- transformujeme ho na objekt JSON
- ten transformujeme ho na vlastnú triedu
Vote
reprezentujúcu hlas - to všetko urobíme na pozadí
Práca s HTTP klientom
K dispozícii sú dve rozličné implementácie:
HttpURLConnection
- odporúčaná voľba autorov Androidu
- klasické Java API
- s optimalizáciami
- od 2.3: transparentná kompresia cez GZIP
- od 4.0: cachovanie odpovedí
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ž
Scanner
om aleboBufferedReader
om - a následne ho pošleme do konštruktora
JsonObject
u, 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 AsyncTaskLoader
i
- 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
- u nás: vrátime
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 AsyncTaskLoader
a
- nepodporuje kešovanie
- čiže má častý prístup na sieť
- možno vylepšiť
- na domácu úlohu
- riaďme sa heslom Dianne Hackborn: “prosím, prosím, prosím, prosím, pozrite sa do zdrojákov
CursorLoader
a a prepíšte ich”. - musíme ošetriť veľa okrajových prípadov a životný cyklus
- pozri Android Design Patterns: Implementing Loaders
- a tiež
AbstractObjectLoader
v našom Git projekte
Konzumujeme Loader
y 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 loaderaonLoadFinished()
: 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 nafalse
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:
- opäť komunikácia na pozadí
- odošleme hlas a nestaráme sa
- 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
, inakfalse
.
- dohoda: ak zahlasujeme áno, pošleme
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
- vytvoríme intent s triedou služby
- napcháme do intentu pod dohodnutý kľúč booleovskú hodnotu indikujúcu stav hlasu
- 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
- vysielačom bude intent service
- stačí, keď budeme vysielať do aplikácie (nemusíme do celého systému)
- získame lokálny broadcast manager
- vytvoríme inštanciu
Intent
u - 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
- 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 akoIntent
- 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
- dôkaz v dokumentácii
registerReceiver()
triedyContext
- dôkaz v dokumentácii
- 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:
- deklaratívne v manifeste
- na domácu úlohu
- pre prijímače, ktoré nepotrebujú pracovať s GUI
- 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
- Hidden Pitfalls of AsyncTask
- Starting Background Services
- Android Design Patterns: Implementing Loaders
- a dokumentácia Androidu ku všetkým konceptom