14. lecke Üzenőfal (2)

Online Android szakkör (DKRMG)


Elérkeztünk az Android szakkör utolsó leckéjéhez. A leírás végéig befejezzük az előző alkalommal megkezdett üzenőfal alkalmazásunkat: végre kilistázzuk a telefon kijelzőjén a közös üzeneteket, illetve kicsit kitupírozzuk a korábbi kódunkat, hogy jobban kezelje a hibákat. Az elkészült programmal már tényleg könnyen lehet használni a webes felület nélkül is a közös üzenőfalat.

Egy kis nehézség

Az előző lecke végén jogosan tehettük volna fel a kérdést: ha egyetlen paranccsal tudtunk üzenetet küldeni, akkor miért nem írtuk meg a lista lehívás kódját is? Azért használunk külső API-kat, hogy utána gyorsan és könnyedén tudjunk programokat írni! Miért nem fejeztük be ezt az egész Üzenőfal alkalmazást már akkor?

A válasz sajnos az, hogy a hosszan tartó műveletek kicsit megnehezítik a mai programozók életét. Mire is gondolunk itt egészen pontosan? Ha egy bonyolult matematikai problémát szeretnénk megoldani (például a π kiszámítása nagyon pontosan) vagy egy nagy méretű fájlt szeretnénk feldolgozni (például egy komplett nagyszótár), az bizony eltarthat jó pár másodpercig. Hasonlóképpen a hálózati kommunikáció (pl. az üzenőfal lehívása vagy bármilyen fájl letöltése) is eltarthat pár másodpercig, főként ha mondjuk akadozik az internet! Miért is gond ez?

Időutazás

Tegyünk egy rövid időutazást körülbelül húsz évvel ezelőttre, amikor a modern grafikus operációs rendszerek még gyerekcipőben jártak, és a felhasználók többnyire csak úgynevezett parancssorokat használtak a számítógépük működtetése közben. A parancssorok világában, mint azt az alábbi képek is mutatják, a számítógépünk egyszerre mindig egy dologgal foglalkozott. Ha elindítottunk egy játékot, vagy csak megnyitottunk egy dokumentumot, a képernyőről minden mást eltűnt. Nem volt még egér sem, nem hogy több érintést érzékelő LCD képernyő.

Ebben a világban teljesen normális dolog lett volna írni egy olyan programot, ami az elindulásától kezdve a felhasználó felé se nézve elvégzi a dolgát, majd végeredményként visszadob valami kedves üzenetet (az eredménnyel, vagy valami barátságtalan hibakóddal). Nem kellett más programokkal és a felhasználóval versenyezni; a számítógép, ha a mi programunkat futtatta, akkor csak ránk figyelt.

Ezzel szemben

Nem csak a számítógépeink változtak az elmúlt évtizedek folyamán (mind méretben és tudásban), de a felhasználók elvárásai is módosultak. Nézz csak kicsit körül a számítógéped monitorján! Hány program is fut egymás mellett?

Sőt! Nem elég, hogy több programot futtatsz egyszerre, de a programok többségétől elvárod, hogy egyszerre több mindent is csináljon! Képzeld el például, mennyire felháborító volna, ha az egész számítógéped lefagyna, amíg a következő online videót be nem tölti a böngésződ!

A tanulság

Az egyik legelső leckében leszögeztük: az általunk írt Java program sorok mindig szépen szabályosan futnak le egymás után. Minden parancs megvárja, amíg az előző utasítás el nem végezte a feladatát. Viszont a fenti kitérőből megtanultuk: a mai világban egyszerűen nem szabad hosszú másodpercekig felelőtlenül feltartani a programunk / számítógép / okostelefon futását.

Utolsó próbatételként írjuk be az alábbi sorokat az üzenőfal programunkhoz, a küldés gomb lenyomását kezelő függvényének elejére:

try {
    Thread.sleep(30000); // aludjunk most itt 30 másodpercet
} catch (InterruptedException e) {
}

Futtasd a programot, és nézd meg, hogy mi történik a gomb lenyomása után! Ha nem tapasztalsz semmi különöset, próbáld meg többször megnyomni a gombot, szerkeszteni az EditText szövegét, stb. Hogy viselkedik a program? Írd meg e-mailben, hogy mik a tapasztalataid!

Ha kipróbáltad, utána gyorsan töröld ki a fenti kódot! Nem szeretjük az alvó, lusta programokat!

Nem lelőve a poént a pontos viselkedésről annyit megsúghatunk, hogy se a programunk, se az Android rendszer nem nézi jó szemmel ezt a tíz másodperces akadást!

De várjunk csak! Hiszen ebben a függvényben küldjük el az üzenetünket, ami természetesen hálózati kommunikációval jár! Miért nem akadozik a kozosUzenetek.insert(uzenet); parancs?

Az Azure API tudja, hogy a program futását nem szabad megakasztani, ezért a hálózati kommunikációt a háttérben végzi. Az insert utasításunk valójában csak egy ígéretet tesz nekünk, hogy megpróbálja elküldeni az üzenetünket, és tovább engedi a program futását. Az üzenet küldése ilyenkor egy másik program szálon a háttérben történik.

És mindez úgy működött az előző leckében, hogy nem is tudtunk róla. Igen ám, de ha a háttérben futó művelet eredményről szeretnénk értesítést kapni, azért már dolgoznunk is kell egy keveset! Ez pedig főként az üzenetek lehívásánál elkerülhetetlen lesz.

Üzenet küldése - visszajelzéssel

Az előző leckében már megírtuk, hogy a telefonunkról tudjunk üzenetet küldeni a közös üzenőfalra. Ez jó esetben működött is, de elég bizonytalan megoldás volt: ha a szerver valamiért nem működik például, vagy csak ki volt kapcsolva a wifi, arról mi nem szerzünk tudomást, legfeljebb úgy, hogy nem találjuk az üzenetünket a webes változaton. Ezen jó lenne javítani!

Mi az az ígéret (avagy ListenableFuture)?

Az előző leckében úgy küldtük el az üzenetet, hogy a közös üzenetek listáját jelképező kozosUzenetek változónak meghívtuk az insert belső függvényét.

kozosUzenetek.insert(uzenet);

Ennek az eredményével nem foglalkoztunk. Pedig érdemes, ugyanis ez az insert függvény egy ígéretet ad vissza (a változó típusa ListenableFuture<valami>). Az ígéretet pedig később számon kérhetjük! De mi is ez az ígéret? Egy sokáig tartó művelet eredményét nem várhatjuk meg, hiszen fentebb megbeszéltük, hogy akkor lefagyna az alkalmazás. Ez az ígéret megoldást nyújt erre, ugyanis rá lehet akasztani két speciális függvényt, amik majd csak akkor fognak lefutni, ha a munka befejeződött.

Mit értünk a munka befejeződésén? Két eshetőséggel kell számolnunk:

  1. Siker (onSuccess): a telefon elküldte az üzenetet a szervernek, az megkapta, feldolgozta, hozzáadta a többihez, és ezt vissza is mondja a telefonnak. Vagyis a munka akkor ért véget, ha a telefon megbizonyosodott róla, hogy az üzenet célba ért.

  2. Hiba (onFailure): valahol valami hiba miatt nem sikerül a megígért művelet.

Most mentsük el ezt az ígéretet, hogy később számon kérhessük. Ehhez kell egy változó a küldés gomb lenyomását kezelő függvénybe, aminek a típusa ListenableFuture<Uzenet>. Hívjuk mondjuk kuldes-nek. Tehát: a

kozosUzenetek.insert(uzenet);

sor a következőre változik:

ListenableFuture<Uzenet> kuldes = kozosUzenetek.insert(uzenet);

Az ígéret beváltása

Most már tudjuk, hogy mit ígér nekünk az Azure szolgáltatás, kérjük is számon rajta, hiszen az ígéret szép szó, ha betartják, úgy jó!

Ezt úgy tudjuk elérni, ha az alábbi sorokat beillesztjük az insert parancs alá (feltételezzük, hogy az ígéret a kuldes nevű változóban van.):

// kuldes ígéret számonkérése a jövőben
Futures.addCallback(kuldes, new FutureCallback<Uzenet>() {
    @Override
    public void onSuccess(Uzenet result) {
        // {1} Ez az onSuccess függvény fut,
        // ha sikerült elvégezni a munkát (elküldeni az üzenetet)
    }

    @Override
    public void onFailure(Throwable t) {
        // {2} Ez az onFailure függvény fut, ha valami hiba történt
    }
});

Nézzük meg részletesebben a két függvényt. Az {1} onSuccess akkor fut le, ha a szerver feldolgozta az üzenetünket, minden rendben volt, nem történt hiba, és ez a válasz vissza is jutott hozzánk. Ide érdemes írni azokat a sorokat, amik pl. értesítik a felhasználót, hogy sikerült elküldeni az üzenetet. Pl. egy kis rezgés (bővebben: szorgalmi)

A {2} onFailure akkor fut, ha a szerver valami miatt nem fogadta el az üzenetünket, vagy el se jutott odáig (pl. nincs bekapcsolva a WiFi). Ilyenkor érdemes megjeleníteni egy Toast* üzenetet.

*Itt most a Gyorssegélyben látottaktól eltérően a this helyére <activityneve>.this-t kell írni. pl.: MainActivity.this. Ennek okait nem részleteznénk itt a lecke közepén.

Próbáljuk ki, hogy továbbra is működőképes-e az alkalmazásunk. Teszteljük kikapcsolt WiFivel is (figyelj, hogy a mobilnet se legyen bekapcsolva!), hogy megjelenik-e a Toast.

Üzenetek letöltése

Az üzenet küldésekor láthattuk, hogyan lehet a hosszan tartó műveletek ígéreteit elmenteni egy változóba, és rögtön megszabni azt is, hogy a sikeres/sikertelen befejeztük után mi történjen. A létező üzenetek lehívásakor gyakorlatilag ugyanezt fogjuk tenni.

Új függvény

Mielőtt megírjuk magát a letöltést végző kódunkat, gondoljuk át, hogy mikor, minek hatására szeretnénk majd letölteni az üzeneteket:

  1. Induláskor jó lenne, ha rögtön megjelennének az üzenetek

  2. Ha új üzenetet küldtünk, újra le kell tölteni az üzenőfal tartalmát, hogy lássuk, megjelent-e az új bejegyzés

  3. Felmerülhet az igény manuális frissítésre is (pl. menüből)

  4. Hogyan értesülünk a mások által írt új üzenetekről? Az ügyesebbek akár egy időzítőt is beállíthatnak, ami rendszeresen, pl. percenként frissíti az üzenetek listáját.

Láthatjuk, hogy akár négy különböző helyen is szükségünk van az üzenetek letöltésére és megjelenítésére. Elméletileg működne az is, hogy egy helyen megírjuk, majd az összes többi helyre átmásoljuk a kódot, de ez egyrészt egy nagyon csúnya, és nem hatékony megoldás, másrészt saját magunk ellen dolgoznánk, ugyanis ha kiderül, hogy valamit módosítani kell a letöltést végző kódon (pl. ilyenkor is szeretnénk egy rezgést), akkor azt mind a négy helyen módosítani kell. Szinte biztos, hogy valamelyiket el fogod felejteni, vagy csak simán elrontod… Sokkal előrelátóbb megoldás, ha az üzenetek szerverről történő letöltéséhez írunk egy külön függvényt, és ezt csak meg kell hívni ott, ahol le szeretnénk tölteni az üzeneteket.

Ennek a függvénynek nem lesz se paramétere, sem visszatérési értéke, így kezdetben nagyon egyszerű lesz.

public void letolt() {

}

Ezt a függvényt az onCreate és a gomb onClick függvényekkel egy szintre rakd, figyelj, hogy még véletlenül se egy másik függvényen belülre!

Üzenetek szűrése

Az üzenetek letöltése hálózati kommunikációval jár, tehát mindaz igaz rá, amit az üzenet küldésénél már láttunk, és ezt is egy ígéret segítségével fogjuk megoldani.

Először nézzük meg, hogyan tudjuk az összes üzenetet elkérni a szervertől:

kozosUzenetek.execute();

Ha jobban belegondolunk, ezzel nem biztos, hogy jól járnánk. Amikor ezt írom 40 üzenet van eltárolva, pedig eddig csak mi küldtünk be teszt-üzeneteket. Ha mindenki elkezdi próbálgatni az alkalmazását, több száz, akár több ezer üzenet is lehet majd a felhőben. Ezt mind letölteni sokáig tarthatna, és a telefon sem feltétlenül tudná hatékonyan megjeleníteni.

Jó lenne lekorlátozni, hogy csak néhány, pl. 15 üzenetet töltődjön le. Erre való a .top(<szám>) belső függvény. Ezt az .execute() elé kell beszúrni, így:

kozosUzenetek.top(15).execute();

Most már kezelhető mennyiségű üzenet érkezik majd, de pontosan melyikek? Ezt az Azure felhő dönti el, és nem biztos, hogy ő is arra a tizenöt üzenetre gondol, mint mi. Célszerű lenne a 15 legfrissebbet lekérni. Ehhez először meg kell mondani, hogy rendezetten kérjük az üzeneteket, mégpedig a __createdAt tulajdonságuk alapján csökkenő sorrendben. Erre az .orderBy(<tulajdonság>,<irány>) belső függvény használható, így:

kozosUzenetek.orderBy("__createdAt", QueryOrder.Descending).top(15).execute();

A fenti sor megkéri a szolgáltatást, hogy először rendezze az üzeneteket, majd az első tizenöt elemet küldje el. Fordítva is működne? Gondold végig!

Most már meghatároztuk, hogy melyik üzeneteket és milyen sorrendben kérjük. De fentebb már említettük, az .execute() függvény nem várhatja meg az üzenetek lekérését. Inkább, a korábbi .insert(uzenet)-hez nagyon hasonló ígérettel tér vissza. Ezt mentsük is el! Az ígéret típusa: ListenableFuture<MobileServiceList<Uzenet>>, neve legyen letoltes!

public void letolt() {
    ListenableFuture<MobileServiceList<Uzenet>> letoltes = kozosUzenetek.orderBy("__createdAt",QueryOrder.Descending).top(15).execute();
}

Az ígéret beváltása

A letöltés ígéretének beváltása nagyon fog hasonlítani az üzenetküldés ígéretének beváltásához:

Futures.addCallback(letoltes, new FutureCallback<MobileServiceList<Uzenet>>() {
	@Override
	public void onSuccess(MobileServiceList<Uzenet> result) {
		// {1} ez fut, ha sikerült elvégezni a munkát (letölteni az üzeneteket)
	}

	@Override
	public void onFailure(Throwable t) {
		// {2} Ez fut, ha valami hiba történt
	}
});

Nézzük meg, mit tudunk csinálni az {1} onSuccess függvényben: Szemfülesebbek észrevehették, hogy hogyan épül fel az ígéret típusa.

  1. Az eleje kötelezően ListenableFuture, ez jelzi, hogy egy ígérettel van dolgunk.

  2. Utána két kacsacsőr között található, hogy mire tesz ígéretet, azaz milyen típusú "gyümölcse lesz a munkának". Ugyan ez a típus jelenik meg az {1} onSuccess függvény egyetlen paraméterének típusában is.

Remélem senki nem lepődik meg, ha azt állítjuk, hogy az {1} onSuccess függvény paramétere a letöltött üzenetek listája, hiszen ez a "munka gyümölcse". Már csak egyetlen zavaró tényező van: Mi az a MobileServiceList<Uzenet>? Miért nem kapunk egy jól ismert ArrayList<Uzenet> listát?

Valójában az MobileServiceList is egyfajta ArrayList, csak az Azure fejlesztői kiegészítették kicsit. A mi szempontunkból ugyan olyan a kettő.

  1. Sikeres eredmény esetén töröljük az uzenetek lista jelenlegi tartalmát (uzenetek.clear();)

  2. Utána pedig rögtön hozzá is adhatjuk a letöltött üzenetek összes elemét a saját listánkhoz, így:

uzenetek.addAll(result);

Ezek után ne felejtsük el értesíteni az adaptert a változásról, írjunk valami figyelmeztetést a {2} onFailure függvénybe (ami ugye akkor fut le, ha valami hiba történt) és készen is vagyunk az üzenetek letöltését végző függvényünk megírásával!

Reméljük, sikerült követni, de ha esetleg mégsem, vagy csak leellenőriznéd a munkádat, kattintás után láthatod egyben a letöltő függvényt.

public void letolt() {
    ListenableFuture<MobileServiceList<Uzenet>> letoltes =
            kozosUzenetek
                .orderBy("__createdAt", QueryOrder.Descending)
                .top(15)
                .execute();

    Futures.addCallback(letoltes, new FutureCallback<MobileServiceList<Uzenet>>() {
        @Override
        public void onSuccess(MobileServiceList<Uzenet> result) {
            uzenetek.clear();
            uzenetek.addAll(result);
            adapter.notifyDataSetChanged();
        }

        @Override
        public void onFailure(Throwable t) {
            Toast.makeText(MainActivity.this, "Hiba az üzenetek letöltésénél!",
				Toast.LENGTH_SHORT).show();
            Log.e("MainActivity", "Hiba az üzenetek letöltésekor.", t);
        }
    });
}

Felesleges részek eltávolítása a programból

Pár felesleges részt illene eltávolítani a programunkból. Jelenleg a Küldés gombra nyomáskor létrehozunk egy új üzenetet, azt egyrészt elküldjük a szervernek (kozosUzenetek.insert(uzenet); ígéret), másrészt hozzáadjuk a saját helyi listánkhoz is. Ha felidézzük az imént megírt letöltő függvényünket, akkor láthatjuk, hogy ott azt írtuk, hogy amint megérkeznek a szerverről a friss üzenetek, a saját listánkat teljesen kiürítjük (uzenetek.clear();), tehát feleslegesen adjuk hozzá. Töröljük ki azt az utasítást, ami az uzenetek listához adja hozzá az üzenetet (uzenetek.add(uzenet);)

Szintén felesleges az előző lecke elején az onCreate-be írt üdvözlő üzenet hozzáadása is, ugyanis az is ki lesz törölve.

A letöltés elindítása / függvény hívása

A lecke befejezéséhez már csak egy apró, ám annál fontosabb feladat maradt hátra: használjuk is az imént megírt függvényünket!

Ehhez annyit kell tennünk, hogy ahol szeretnénk, hogy letöltődjenek az üzenetek, ott meghívjuk a saját letolt() függvényünket. Ilyen hely lehet például az onResume függvény (meg kell írni!), hogy mindig, amikor a felhasználó megpillantja az alkalmazásunkat letöltődjenek a legfrissebb üzenetek. Pl így:

@Override
protected void onResume() {
    super.onResume();
    letolt();
}

De érdemes az üzenet küldésekor (feltéve, hogy sikeres volt, tehát az ottani {1} onSuccess függvény végén) is frissíteni a listát, hogy a felhasználó láthassa a frissen beküldött üzenetét.

Vége

A program elméletileg most már teljesen működőképes. Ellenőrizd, majd az elkészült projektet küldd el nekünk a feltöltő oldalon keresztül!

Szorgalmi feladatok

1*. Felhasználóbarátabb üzenetküldés

Jelenleg az üzenet küldésekor a következők történnek:

  1. Elkészítjük az üzenet objektumot

  2. Odaadjuk az Azure-nak, és kapunk cserébe egy ígéretet, ami majd egyszer később teljesül (vagy hibát jelez)

  3. Töröljük az EditText tartalmát.

  4. Teljesül az ígéret

  5. Sikerült: rezgés

  6. Hiba történt: Toast

Remélhetőleg látható ennek a folyamatnak a hibája: Ha valami probléma adódott az üzenet elküldésével, akkor mielőtt erről tudomást szerzünk egy Toast formájában, már kitöröltük az EditText tartalmát. Vagyis teljesen és visszavonhatatlanul elveszett az üzenet tartalma! Reméljük, nem kell mondani, hogy ez milyen rossz fényt vetne az alkalmazásunkra és a programozóra… Javítsuk!

Több megoldás lehet a problémára, nézzük a legegyszerűbbet.

  1. Az egész küldő függvény legelején tiltsuk le a küldés gombot (setEnabled(false) belső függvénye). Ez azért kell, hogy amíg meg nem bizonyosodtunk az üzenetküldés eredményéről, nem próbálja újraküldeni a felhasználó az üzenetet. (Lehet, hogy csak lassú a net, de ha a türelmetlen felhasználó többször megnyomja a gombot, akkor ugyan lassan, de több példányban lesz elküldve az üzenet.)

  2. Hozzuk létre az üzenetet, adjuk oda az Azure-nak, és írjuk meg az ígéret beváltásához szükséges kód vázát, pont, mint eddig.

  3. Töltsük ki a két függvényt:

  4. Az {1}-esben (azaz siker esetén) továbbra is jelezhetjük egy rövid rezgéssel a sikert, viszont fontos, hogy itt töröljük ki az EditText tartalmát, és ne felejtsük el újra engedélyezni a gombot (setEnabled(true))!

  5. a {2}-esben (azaz hiba esetén) lehet mondjuk egy hosszabbat rezegni, Toast-ot megjeleníteni, viszont ami fontos, hogy itt ne töröljük ki az EditText tartalmát, viszont itt is engedélyezzük a gombot!

Ez azért jó, mert így biztosan nem veszik el az elküldeni kívánt üzenet. Ha hiba történik (például nincs bekapcsolva a WiFi) van lehetősége korrigálni a felhasználónak (pl. bekapcsolni a WiFi-t) és újra megpróbálkozni a küldéssel, anélkül, hogy újra be kéne gépelnie az üzenetét.

2*. Frissítés gombnyomásra

Nem túlzás azt állítani, hogy az üzenőfal alkalmazásunk igen felhasználóbarát az üzenetek elküldésénél. Most javítsunk egy kicsit a letöltésen is!

Az egyik legszembetűnőbb hiányosság az, hogy az üzenetek listáját csupán két esetben töltjük le:

Tételezzük fel egy pillanat erejéig, hogy a Aladár, a szakkör egy igen lelkes fiktív tagja az üzenőfalon szeretne hosszú társalgást folytatni a hozzá hasonlóan valós Bélával. Amikor Aladár feltesz egy kérdést, utána nincs más megoldás: újra kell indítania párszor a programot, hogy meglássa, válaszolt-e Béla.

Ezt a problémát szeretnénk megoldani, első lépésben egy igen egyszerű "frissítés" gomb elhelyezésével, aminek segítségével a felhasználó bármikor lehívhatja az üzenetek listáját!

  1. Először is helyezzünk el egy új gombot a layout szerkesztőben. Ne feledkezzünk el az id tulajdonságáról!
  2. Hozzunk létre egy új kattintást kezelő függvényt, majd rendeljük hozzá az új gombhoz
  3. A kattintást kezelő függvény egyszerűen hívja meg a letoltes() függvényünket.

Gomb helyett használhatunk egy felugró menüt is. Ehhez segítséget itt találtok.

A gombnyomásos megoldás nagyban megkönnyíti Aladár és Béla dolgát. Ha még ennél is jobb megoldást szeretnénk, akkor érdemes lehet az üzeneteket egy időzítő segítségével pl. pár másodpercenként lehívni. Ezt az 5*. szorgalmi feladatban meg is tesszük.

3*. Üres lista?!

Ha alaposan letesztelted a hibakezelést is, akkor észrevehetted, hogy amikor pl. nincs internet, akkor a listában nem jelenik meg egy elem sem. Ez nem túl informatív. Sokkal jobb felhasználói élményt nyújtana, ha ilyenkor valami szöveg látszódna, mutatva, hogy itt elvileg egy lista van, csak most épp üres.

Ez meg lehet oldani egy macerásabb, és egy egyszerűbb módon is, az előbbi kitalálását rátok bízzuk, de utóbbit megmutatjuk, mert egy hasznos kis funkció.

  1. Először is ki kell találni, hogy mit szeretnénk megjeleníteni, amikor üres a lista.
    Ez lehet bármi, amit amúgy meg tudnánk jeleníteni a képernyőn. Lehet egy gomb, egy kép, egy szöveg, stb. Sőt, aki megcsinálta az előző lecke 3. szorgalmiját, az már tudhatja, hogy akár bonyolultabb layoutokat is be lehet ide rakni.
    Itt példaként egy szöveget és egy frissítés gombot fogunk megjeleníteni, de aki biztos a dolgában, nyugodtan eltérhet ettől.
  2. Rakjuk össze a layoutot!
    1. Ehhez először is ideiglenesen rejtsük el a listánkat a layout szerkesztőben: állítsuk a visibility tulajdonságát gone-ra, így nem fog bezavarni. Később ne felejtsük el visszaállítani visible-re!
    2. Húzzunk be fent középre egy újabb RelativeLayout komponenst. Érdemes beállítani az layout:alignComponent tulajdonságon belül a bottom:top sort mondjuk az alsó EditText-re, vagy a küdés gombra.
      Ez mondja meg, hogy az aktuális komponens alja (bottom) melyik másik komponens tetejéhez (top) igazodjon.
    3. Ebbe pakoljuk bele, amit szeretnénk megjeleníteni, például egy-két szöveget és egy gombot, tetszőlegesen elrendezve, színezve, stb.
    4. Az új RealtiveLayout komponensnek adjunk egy azonosítót (id), pl.: ures
    5. Ha mindennel készen vagyunk, rejtsük el az új RelativeLayoutot (visibility tulajdonság: gone) és jelenítsük meg újra a listát (visibility tulajdonság: visible).
  3. Menjünk át a Java kódba, és csináljunk egy új változót az új RelativeLayoutnak (típusa RelativeLayout, neve lehet pl.: ures), majd az onCreate-ben rendeljük is hozzá a findViewById függvénnyel.
  4. Végül mondjuk meg (szintén az onCreate-ben) a ListView-nak, hogy ez jelenjen meg, ha nincsen egy üzenet sem:
    list.setEmptyView(ures);
    Figyelj oda, hogy a ListView típusú változódat használd, ne az adaptert, és ne is az ArrayList-et!
  5. Próbáld ki, hogy kikapcsolt internet mellett megjelenik-e az új komponens!
  6. Már csak annyi maradt hátra, hogy az új gombnak csinálj egy onClick függvényt, rendeld hozzá a layout szerkesztőben, és a függvényben hívd meg a letoltes() függvényedet.

4*. Régebbi üzenetek betöltése

Jelenleg az alkalmazásunk mindig a 15 legfrissebb üzenetet jeleníti meg. Ez így is van rendjén, hiszen az üzenőfalak sajátossága, hogy többnyire csak a legújabb bejegyzések érdeklik a felhasználót. Néha azonban bizony előfordulhat, hogy vissza szeretnénk keresni egy régi beszélgetést, vagy csak gyönyörködni szeretnénk a többi szakkörös felhasználó szárnypróbálgató üzeneteiben. Viszont továbbra is igaz, hogy nem lenne bölcs döntés egyszerre lehívni a szolgáltatás összes korábbi üzentetét, hiszen az feleslegesen terhelné mind a telefont, mind a hálózatot.

Egy egyszerű megoldás lehet a problémára, ha elhelyezünk egy újabb gombot a felhasználói felületre, amivel jelezhetjük: nekünk ennél több üzenetre lesz szükségünk.

A pontos megvalósítást rátok bízzuk! Első megközelítésként érdemes lehet létrehozni egy int típusú változót, ami a letöltendő üzenetek számát tárolja (kezdetileg 15, de gombnyomásra/menüből növelhető). Bátrabbak kísérletezhetnek a lekérdezésnél a skip függvénnyel is!

5*. Üzenetek frissítése adott időközönként

A második szorgalmi végén utaltunk már erre a feladatra: jó lenne, ha a programunk futás közben (onResume-tól kezdve egészen onPause-ig) folyamatosan hívná az üzeneteket, mondjuk három másodpercenként.

Első megközelítésben megnézzük, hogy miként tudunk az Android rendszerben ilyen időzítőt létrehozni. Azonban még most le kell szögezni: a megoldás igen naiv, hiszen pár másodpercenként frissíti a teljes lista tartalmát!

A probléma ennél a feladatnál is ugyanaz, mint korábban: a letolt(); függvényünket szeretnénk meghívni adott időközönként, ez azonban csak a programunk fő száláról történhet meg. A program fő szálát pedig nem tarthatjuk fel sokáig (emlékszel még a lecke elején a Thread.sleep(30000) eredményére?). Ezért az Android rendszerben a következő megoldás vált elfogadottá:

Létre hozunk egy futtatható objektumot, ami a főszálon fut le, majd dolga végeztével megkéri a rendszert, hogy futtassa őt újra adott idő elteltével.

Nézzük ezt meg a gyakorlatban:

  1. Elsőként hozzunk létre egy új változót az Activityben, ami segítségünkre lesz a futtatásoknál. Ez a Handler objektum képes futtatni az objektumunkat akár rögtön, akár adott idő elteltével.

    android.os.Handler handler;
  2. Adjunk értéket az új változónak az onCreate függvény végén.

    handler = new android.os.Handler();
  3. Most hozzunk létre még egy változót, ami ezúttal magát a futásra váró függvényt csomagolja be.

    Runnable refreshTask;
  4. Amikor értéket adunk ennek a változónak, rögtön megadhatjuk azt a függvényt is, amit le szeretnénk majd futtatni adott időközönként. A változónk run belső kis függvénye az, ami az objektum futtatásánál elindul.

    refreshTask = new Runnable() {
        @Override
        public void run() {
            // TODO: hívd meg a letolt(); függvényt!
    
            // vége a függvénynek. Most kérjük meg a rendszert, hogy futtasson minket újra 3000ms (3s) elteltével
            handler.postDelayed(refreshTask, 3000);
        }
    };
  5. Remélhetőleg eddig követhető volt: most már van egy Handler változónk, aki segít futtatni a függvényünket, illetve egy Runnable refreshTask;, ami gyakorlatilag a frissítés függvényt csomagolja be. Ha egyszer elindítjuk ezt a frissítés függvényt, akkor őt onnantól kézbe veszi az irányítást, és gondoskodik róla, hogy három másodpercenként újra lefusson (handler.postDelayed(refreshTask, 3000);).

    Tehát nincs más dolgunk, mint gondoskodni az első futásról. Például indíthatjuk a függvényt rögtön az onResume végén:

    handler.post(refreshTask);
  6. Érdemes még arról is gondoskodni, hogy a program leállításakor ez az ismétlődő refreshTask is legyen szíves megpihenni. Ehhez az onPause függvény végére kell odabiggyesztenünk az alábbi kódot:

    handler.removeCallbacks(refreshTask);
  7. Érdemes megemlíteni, hogy így rögtön az onResume függvényünk kérvényezni fogja az első frissítést. Tehát az ottani letolt(); sort akár törölhetjük is.

N*. ???

Van még ötleted? Nem kell itt megállnod. A program rengeteg irányba fejleszthető! Például a lekérés kódja lehetne kicsit okosabb úgy, hogy csak bizonyos dátum utáni üzenetek iránt érdeklődjön, így elkerülve az üzenetek folyamatos újratöltését! De a lehetőségek tárháza végtelen. A feltöltő oldalra bármilyen plusz munkát feltölthetsz. Illetve ha kérdésed támad az ötleteddel kapcsolatban, írd meg bátran, hátha tudunk segíteni!