Če program vsebuje kakršen koli proces, ki traja nekaj časa, preden se zgodi oziroma izvede, uporabnik to zazna kot neodzivno aplikacijo, saj za trenutek (ali več) vse »zmrzne«. Programski jeziki so namreč narejeni tako, da se ukazi izvajajo po vrsti, v istem vrstnem redu, kot so poklicani. Tej vrsti pravimo nit (thread). Privzeto imamo v Javi le eno nit, ki ji pravimo glavna nit (main thread), v androidnem okolju pa tudi nit uporabniškega vmesnika (UI thread), saj na njej vedno tečejo vsaj neposredni ukazi za komponente uporabniškega vmesnika.

Če imamo torej kakšen ukaz, ki traja dlje časa, hkrati pa ne želimo, da ustavi glavno nit, nam Java omogoča, da ustvarimo zanj novo nit, ki bo tekla neodvisno od glavne niti. Tako bo uporabniški vmesnik med izvajanjem tega ukaza še vedno odziven na uporabnikove klike, medtem ko se bo ukaz izvajal v ozadju.

Primer težave s počasno aplikacijo najdemo že v naši aplikaciji, ko uporabnik izbere zavihek Sporočila ali Zemljevid. Za slednjega je proces približno takšen:

1. Uporabnik klikne na zavihek.
2. Ustvari se instanca razreda MessagesMapActivity.
3. Aplikacija s strežnika prebere sporočila.
4. Prebrana sporočila se preslikajo v objektno obliko.
5. Sporočila se izrišejo na zemljevidu.
6. Zavihek z zemljevidom in sporočili se prikaže.

retja točka je najpočasnejša, saj gre za povezavo na oddaljen strežnik in branje z njega. Dobra praksa za uporabnike je, da se v glavni niti hitro izvedejo koraki 1, 2 in 6, koraki 3, 4 in 5 pa se izvedejo v ozadju. Uporabnik bi tako takoj ob kliku na zavihek videl zemljevid, sporočila pa bi se na zemljevidu prikazala čez nekaj trenutkov. Ker peti korak očitno deluje na uporabniški vmesnik, mora teči na niti uporabniškega vmesnika, a ga vseeno umeščamo na drugo nit, saj se izvede takoj za tem, ko se izvede proces pod številko štiri.

Shema niti: modra je nit uporabniškega vmesnika, rdeča pa stranska. Zavihek se prikaže, preden so podatki prebrani.

Obstaja več načinov, kako lahko ustvarimo vzporedne niti. Najpreprostejši način je, da kodo, za katero želimo, da se izvaja v svoji niti (npr. MessagesRetriever.INSTANCE.readMessages()),
zavijemo v nit:

new Thread(new Runnable() {
public void run() {
MessagesRetriever.INSTANCE.readMessages();
}
}).start();

Ta koda res ni zahtevna, vendar pa je primerna le, če želimo, da se nekaj le izvede in popolnoma nič ne vpliva na naš uporabniški vmesnik. Če pa bi želeli na primer v razredu MessagesListActivity asinhrono prikazati še sporočilo, bi morali zgornjemu primeru znotraj metode run() dodati še kodo:

MessagesListActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MessagesListActivity.this, "test", Toast.LENGTH_SHORT).show();
}
});

Tako postane koda že dokaj nepregledna, a na srečo obstaja lepša rešitev.

AsyncTask

Android vsebuje abstrakten razred AsyncTask, ki nam omogoča preprosto izvajanje kode v ločeni niti in pošiljanje rezultatov nazaj na glavno nit, hkrati pa omogoča celo posodabljanje napredka. Razred uporabimo tako, da napravimo nov podrazred, mu določimo tip vhodnih podatkov, tip napredka in tip izhodnih podatkov (rezultat). Nato povozimo metodi doInBackground in onPostExecute – prva bo samodejno tekla na ločeni niti, druga pa bo dobila rezultate prve in jih aplicirala na glavni niti. Pa prenehajmo z abstraktnim razlaganjem in si poglejmo konkreten primer na naši kodi.

Najprej v paketu com.example ustvarimo nov vmesnik MessagesLoadable. Z desnim gumbom kliknemo na paket in izberemo New > Java Class. Pri Name napišemo MessagesLoadable, pri Kind pa izberemo Interface. To nam ustvari in odpre nov vmesnik, v katerega dodamo metodo:

public interface MessagesLoadable {
void loadMessages(List< Message> messages);
}

Ta vmesnik bo namenjen temu, da bomo lahko z isto kodo poslali prebrana sporočila na pogled z zemljevidom ali na pogled s seznamom.

Zdaj lahko ustvarimo razred AsyncMessagesLoader, ki bo v ozadju (torej na ločeni niti) naložil sporočila s strežnika in jih posredoval želenim pogledom. Z desnim gumbom kliknemo na paket com.example in spet izberemo New > Java Class. Pri Name napišimo AsyncMessagesLoader, pri Kind pa pustimo Class. Idea nam ustvari in odpre nov razred, za katerega želimo, da razširja razred AsyncTask, zato mu dodamo extends AsyncTask< Void, Void, List< Message> > . Tu prvi Void pomeni, da ne bomo podali nobenih parametrov (če bi želeli kot parameter dodati kakšno besedilo, bi tu napisali na primer String), drugi Void pomeni, da ne bomo posodabljali stanja med samim izvajanjem, List< Message> pa predstavlja tip rezultata, ki ga bomo vrnili, torej seznam sporočil. Ko to storimo, nam Idea javi napako, da manjka metoda doInBackground, s klikom na Alt+Enter > Implement methods... pa nam jo samodejno doda.

Zdaj pa v razred vnesimo še naslednjo kodo in pojasnimo, kaj pomeni:

public class AsyncMessagesLoader extends AsyncTask< Void, Void, List< Message> > {
private MessagesLoadable messagesLoadable;

public AsyncMessagesLoader(MessagesLoadable messagesLoadable) {
this.messagesLoadable = messagesLoadable;
}

@Override
protected List< Message> doInBackground(Void... voids) {
return MessagesRetriever.INSTANCE.readMessages();
}

@Override
protected void onPostExecute(List< Message> messages) {
super.onPostExecute(messages);
messagesLoadable.loadMessages(messages);
}
}

Najprej ustvarimo konstruktor, ki za parameter sprejme instanco razreda MessagesLoadable in jo shrani v polje messagesLoadable. To instanco bomo osvežili z novimi sporočili, ko jih bomo prebrali. V metodo doInBackground dodajmo kodo za branje sporočil. Ta se bo izvedla v ozadju, kar je naš cilj. Na koncu pa povozimo še metodo onPostExecute, ki se bo izvedla po koncu metode doInBackground in sprejela rezultat, torej seznam prebranih sporočil. Ta sporočila posredujmo v messagesLoadable.

Ko uporabnik preklopi zavihek, se začnejo nalagati podatki s strežnika.

Klicanje niti na zemljevidu

Ko smo pripravili vzporedno nit, jo moramo še poklicati. Najprej napravimo to za zavihek z zemljevidom, ker bo tu malenkost manj dela. Odprimo razred MessagesMapActivity in mu dodajmo vmesnik: implements MessagesLoadable. Idea nas opozori, da manjka metoda loadMessages(List< Message> messages), zato ji dovolimo, da nam jo napravi. Vanjo prestavimo kodo iz metode reloadMarkers, pri čemer MessagesRetriever.INSTANCE.readMessages() zamenjajmo z messages – želimo namreč prikazati kar sporočila, ki smo jih dobili s parametri metode.
V metodo reloadMarkers, ki je zdaj prazna, dodamo kodo

new AsyncMessagesLoader(this).execute();

Ta koda ustvari in požene naš razred z ločeno nitjo. Parameter this pomeni, da se bo na koncu izvedene niti poklicala metoda loadMessages trenutnega razreda. Če dodamo še prijazni sporočili uporabniku, bi morali ti metodi zdaj imeti naslednji videz:

public void reloadMarkers() {
Toast.makeText(this, "Nalagam sporočila...", Toast.LENGTH_SHORT).show();
new AsyncMessagesLoader(this).execute();
}

public void loadMessages(List< Message> messages) {
dialogOverlay.clear();
for (Message message : messages) {
dialogOverlay.addItem(message.getLatitude(), message.getLongitude(),
MessageFormat.format("{0} - {1}", message.getAuthor(), DATE_FORMAT.format(message.getDate())),
message.getMessage());
}
Toast.makeText(this, "Sporočila osvežena.", Toast.LENGTH_SHORT).show();
}

Če zdaj poženemo našo aplikacijo, se bodo sporočila na zemljevid že nalagala v ozadju, kar bo pomenilo, da se nam bo, če kliknemo na zavihek Zemljevid, ta pokazal takoj, sporočila pa šele čez nekaj trenutkov, na kar nas bo aplikacija tudi opozorila.

Ko se podatki naložijo, se prikažejo na zemljevidu, izpiše pa se tudi obvestilo o tem.

Pa še v seznamu

Zdaj podoben postopek ponovimo še za zavihek s seznamom. Tu imamo nekaj dodatnega dela: ker smo prej samo podali seznam elementov, ga bomo zdaj spreminjali dinamično in večkrat. Za to bomo potrebovali novo polje v razredu MessagesListActivity, ki bo vsebovalo seznam trenutno prikazanih elementov. Odprimo torej omenjeni razred in vanj dodajmo polje ArrayAdapter< String> listAdapter ter popravimo metodo onCreate takole:

private ArrayAdapter< String> listAdapter;

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listAdapter = new ArrayAdapter< String> (this, R.layout.regions_list_item);
setListAdapter(listAdapter);
getListView().setBackgroundResource(R.drawable.main_bg);
}

Ta koda v metodi onCreate ustvari instanco razreda ArrayAdapter in ga shrani v polje listAdapter, tako da je koda precej podobna prejšnji. V nasprotju s prejšnjim postopkom pa mu tukaj ne podamo seznama elementov, ker bomo tega podali pozneje, asinhrono.
Zdaj pa temu razredu, enako kot pri zemljevidu, dodajmo vmesnik: implements MessagesLoadable. Tudi tokrat nas Idea opozori na pomanjkanje implementiranih metod in nam jih na našo željo tudi napravi. Vnesimo v novo metodo naslednjo kodo in dopišimo še eno pomožno metodo:

@Override
public void loadMessages(List< Message> messages) {
listAdapter.clear();
for (String message : flattenMessages(messages)) {
listAdapter.add(message);
}
Toast.makeText(this, "Sporočila osvežena.", Toast.LENGTH_SHORT).show();
}

private List< String> flattenMessages(List< Message> messagesList) {
List< String> messages = new ArrayList< String> ();
for (Message message : messagesList) {
messages.add(MessageFormat.format("{0} - {1}\n{2}",
message.getAuthor(), DATE_FORMAT.format(message.getDate()), message.getMessage()));
}
return messages;
}

Metoda flattenMessages je v bistvu preimenovana metoda getMessages, ki smo ji dodali parameter messagesList in ga uporabili namesto prejšnje kode MessagesRetriever.INSTANCE.readMessages(). Metoda loadMessages najprej počisti s seznama vse elemente, nato pa gre po vseh sporočilih (pretvorjenih v String) in jih doda na seznam. Na koncu pa še obvesti uporabnika o uspešnosti akcije. Enako kot pri zemljevidih se bo ta koda poklicala, ko se bo končala nit razreda AsyncMessagesLoader. Tega pa je treba najprej še poklicati. Spet ponovimo, kar smo napravili pri zemljevidih, in napišimo kodo, ki bo najprej obvestila uporabnika, da smo začeli brati podatke, nato pa jih začela brati v ločeni niti:

public void reloadMessages() {
Toast.makeText(this, "Nalagam sporočila...", Toast.LENGTH_SHORT).show();
new AsyncMessagesLoader(this).execute();
}

Zdaj pa le še pokličimo to metodo na koncu metode onCreate in že se bodo podatki prebrali v ozadju, ko bomo odprli ta zavihek.

Osveževanje

S kodo, ki smo jo pripravili do zdaj, zelo preprosto napravimo osveževanje iz menija, torej dodamo v meni nov gumb »Osveži«, ki bo na trenutni strani ponovno naložil sporočila s strežnika. Najprej odprimo datoteko main_menu.xml in napravimo podoben vnos, kot smo ga napravili za gumb za izhod iz aplikacije:

< item android:id="@+id/refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/refresh"/>

V datoteko z besedili strings.xml dodajmo besedilo tega gumba:

< string name="refresh"> Osveži< /string>

Napravimo tudi ikone za vse resolucije za ta gumb – slike z imenom ic_refresh.png. Nato odprimo datoteko MojMikroAndroidActivity.java in v metodo onOptionsItemSelected dodajmo primer, ko uporabnik pritisne na naš novi gumb – podobno, kot je to napravljeno za gumb za izhod:

case R.id.refresh:
refresh();
return true;

Nato pa napišimo še metodo refresh, ki bo pognala osveževanje:

private void refresh() {
LocalActivityManager localActivityManager = getLocalActivityManager();
Activity currentActivity = localActivityManager.getCurrentActivity();
if (currentActivity instanceof MessagesListActivity) {
((MessagesListActivity) currentActivity).reloadMessages();
} else if (currentActivity instanceof MessagesMapActivity) {
((MessagesMapActivity) currentActivity).reloadMarkers();
}
}

Ta metoda bo poklicana vsakič, ko bo uporabnik pritisnil na gumb »Osveži« v meniju, za tem pa najprej pogledala, kateri zavihek (aktivnost) je trenutno odprt, in pognala njegovo metodo za osveževanje podatkov, ki smo jo prej napisali. Zdaj lahko kodo preizkusimo.

Če uporabnik klikne na gumb »Osveži«, sproži nalaganje podatkov s strežnika.

Zaključek

Vzporedno procesiranje podatkov je dvorezen meč. Po eni strani omogoči uporabniku neprekinjen nadzor nad aplikacijo ter boljšo izrabo procesorske moči, sploh zdaj, ko se tudi v androidnih napravah uveljavljajo večjedrni procesorji. Po drugi strani pa je treba paziti, da ne napravimo zmešnjave, če hkrati prejmemo podatke iz več niti. A z malo testiranja in logičnega mišljenja lahko težave preprečimo ter ponudimo hitro in stabilno aplikacijo.

Kot vedno najdete izvorno kodo tokratnega članka na spletni strani logica.pro/mojmikro, hkrati pa lahko tam skupaj ugotovimo, kaj vse bi še lahko dali na svoje niti.

Moj mikro, september – oktober 2012 | Rok Končina