DevTutorial 7 - ArrayList en ItemLayout via BaseAdapter aan ListView koppelen

DevTutorial 7 - ArrayList en ItemLayout via BaseAdapter aan ListView koppelen

Wat gaan we doen

In deze DevTutorial gaan de we de app uit de vorige DevTutorial verder afmaken. We gaan de tweede Activity met de naam TweetsActivity tweets laten downloaden vanaf Twitter.com en ze tonen in een ListView: . Het leuke aan deze DevTutorial is dat je vrij uitgebreid leert hoe je een ListView kan gebruiken, dit is een van de meest gebruikte Views in alle apps. Hij is ook een beetje ingewikkeld, daarom is er één hele DevTutorial aan gewijd. Bij het leren gebruiken van een ListView gaan we de volgende onderwerpen bij langs:

Tussen neus en lippen door worden soms kort bepaalde classes uitgelegd en wat hun functie is, ik ging er vanuit dat je dat nu wel aan kan. Het is nog wel steeds goed opletten, maar het begrijpen wordt waarschijnlijk steeds makkelijker omdat je het beter kan plaatsen in je huidige Androidkennis. Dit is de laatste "lange" DevTutorial, best veel basis Androidconcepten zijn hierna wel uitgelegd. In DevTutorial 8, 9 en 10 krijg je meer code voorbeelden over hoe je bepaalde functionaliteit kan gebruiken. Succes ermee!

De app verder ontwerpen

In de tweet Activity die we in de vorige DevTutorial TweetsActivity genoemd hebben moet een lijst-weergave komen die je kan scrollen. De View die je daarvoor kan gebruiken is de ListView. In die ListView moeten individuele tweets getoond worden die je kan scrollen. Voor het weergeven van elke individuele Tweet moet je een aparte layout maken, deze layout wordt vervolgens voor elke tweet opnieuw aangemaakt. Alle layouts van die tweets worden vervolgens onder elkaar gezet zodat het eruit ziet als een lijst. Eerst moet je een ListView toevoegen aan je layout van TweetsActivity:

Je layout tweets.xml moet er nu als volgt uitzien: .

Nu gaan we een apart layout-bestand maken dat gebruikt wordt elke keer als er een tweet wordt getoond in de ListView lvTweets:

Je layout tweet_listitem.xml moet er nu als volgt uit zien: .

De app verder programmeren

Het doel van deze DevTutorial is om uiteindelijk de ListView te kunnen vullen met de juiste layout voor elk item én de juiste gegevens daarin te tonen. Eerst moet er een Tweet-object aangemaakt worden waar je elke tweet die je downloadt in kan opslaan. Tijdens het maken van de Tweet-class leer je ook hoe je een datum die is opgeschreven in platte tekst in kan lezen naar het Date-object. Wanneer je eenmaal weet hoe je een individuele Tweet kan opslaan leer je over het opslaan van meerdere Tweets. Je leest over de ArrayList<E> class, dat is class waar je objecten van eenzelfde type achter elkaar in kan opslaan. Dit is een belangrijke class en dit principe wordt erg veel gebruikt bij het maken van apps net zoals in deze DevTutorial. Als je een lijst van Tweets kan opslaan ga je deze gebruiken om de BaseAdapter class te subclassen: hierdoor heb je een eigen class die je verder kan programmeren zodat je controle hebt over hoe de ListView visueel wordt opgebouwd. De paragrafen die daarna komen houden zich bezig met het aanpassen van code die je al eerder hebt gebruikt in de MainActivity class. Een uitzondering hierop vind je in de laatste paragraaf waar je leert hoe je een JSONArray vanuit een JSONvult in een een for block. Die JSONArray gebruik je daarna om je lijst van gegevens te vullen zodat je deze kan gebruiken om met je eigen subclass van de BaseAdapter class de ListView aan te sturen. Dit klinkt nu misschien nog een beetje als nerd-taal maar hopelijk is dat veranderd aan het einde van deze DevTutorial:)

Tweet object aanmaken: Date uitlezen uit JSON

Wanneer je straks tweets gaat downloaden moet je ze kunnen opslaan. Toen je gegevens ging downloaden van de Twitter-gebruiker sloeg je deze op in een apart TwitterUser object. Op dezelfde manier gaan we elke tweet die we downloaden opslaan in een Tweet object. Van een tweet willen we gaan tonen wat de inhoud van de tekst in de tweet was en wanneer hij was aangemaakt. De tekst van de tweet kunnen we opslaan in een String object en de aanmaak-datum van de tweet gaan we opslaan in een Date object. In DevTutorial 3 heb je al eerder gewerkt met een Date object toen je een afspraakplanner maakte. De Tweet class zal dus erg veel lijken op de TwitterUser class die je in de vorige DevTutorial maakte omdat je weer een JSON uitleest. Er zijn echter twee verschillen. Het uitlezen van een datum uit een JSON naar een Date object heeft iets meer regels code nodig dan simpelweg een String uitlezen uit een JSON. Verder ontvangt de constructor van het Tweet object geen String waar de JSON inzit, maar ontvangt deze constructor al het aangemaakte JSON object, dus een object van het type JSONObject. Dit laatste komt omdat er al eerder in de code een JSONArray wordt aangemaakt (dat is een lijst waar standaard al allemaal JSONObjects in zitten). Dit is een voorbeeld van een JSON die je terugkrijgt als je tweets download via Twitter.com: [gist id=1274073] Als je dit vergelijkt met de JSON uit de vorige DevTutorial: [gist id=1044937] Dan zie je dat de TwitterUser-JSON begint met een '{' terwijl de tweets JSON begint met een '[' en daarna pas een '{'. Dit heeft te maken met dat je in de TwitterUser-JSON enkel één Twitter-gebruiker downloadt, terwijl je in de Tweet-JSON meerdere tweets downloadt. Dat de Tweets-JSON begint met een '[' betekent dat er tussen het begin van JSON '[' en het eind van de JSON ']' meerdere JSON objecten staan met elk hun eigen elementen. In dit geval staan staan er tussen de '[' en ']' meerdere tweets gescheiden met een komma, waarbij elke tweet begint met een '{' en eindigt met een '}'. Als je nu naar één zo'n JSON-Tweet object kijkt dan bestaat die uit de volgende code: [gist id=1274077] Aan het einde van één zo'n JSON-Tweet object staan de elementen text en created_at: [gist id=1274086] Je kan zien dat het element created_at een lange afkorting bevat van de tijd waarop de tweet is aangemaakt: dag van de week, maand, dag van de maand, uren, minuten, seconden, tijdzone en als laatste het jaartal. Je ziet ook dat er Engelse namen gebruikt worden voor de weekdagen en de maanden. Om die lange tijd-afkorting om te kunnen zetten naar een Date object heb je het SimpleDateFormat object nodig, deze had je al gebruikt in DevTutorial 3 toen je een Date object om ging zetten naar een String object: [gist id=1063180] Nu ga je dit object weer gebruiken, maar deze keer ga je hem op de omgekeerde manier gebruiken: het omzetten van een String object naar een Date object. Voordat het SimpleDateFormat object de String kan inlezen naar een Date object, moet je aangeven hoe de afkorting is opgebouwd: dag van de week, maand, dag van de maand, uren, minuten, seconden, tijdzone en als laatste het jaartal. Zoals je in deze tabel kan opzoeken doe je dat met de volgende String: [gist id=1274102] Maar als je deze String zou gebruiken werkt het nog steeds niet goed: als je een Nederlands Androidapparaat deze code laat uitvoeren zoekt hij naar de Nederlandse variant ervan: "Don Okt 06 18:15:27 +0000 2011" in plaats van "Thu Oct 06 18:15:27 +0000 2011". Of wanneer je een Frans Androidapparaat de code zou laten uitvoeren gaat hij op zoek naar "Jeu Oct". Maar de JSON die je van Twitter.com krijgt is altijd in de Engelse taal. Daarom moet je een andere constructor gebruiken als je een nieuw object wil maken van het type SimpleDateFormat: Hiermee geef je twee berichten (parameters) mee aan de constructor, de eerste is de datum/tijd afkorting, en de tweede is een Locale object dat aangeeft voor welke taal je de SimpleDateFormat wil aanmaken. De Locale class kan je hier vinden. Wil je een Locale aanmaken dan moet je de constructor gebruiken en een afkorting van de taal meegeven, hier zie je drie voorbeelden: [gist id=1274130] Voor een aantal landen zijn statische variabelen gemaakt, hierdoor hoef je niet opnieuw de constructor te gebruiken maar kan je gewoon de naam van de taal opgeven: [gist id=1274133] Hoe het ook zij, je hebt een String nodig die de de afkortingen van de datum en tijd beschrijft en een Locale object met de gewenste taal. Dit komt erop neer dat je de volgende code kan gebruiken om de de Date String uit de JSON om te zetten naar een Date object: [gist id=1274141] Wanneer je een SimpleDateFormat object hebt kan je met een String de method parse aanroepen en als resultaat daarvan krijg je een Date object terug. Nu je dit weet kan je je Tweet class kan schrijven:

[gist id=1274162]

[gist id=1274175]

[gist id=1274178]

[gist id=1274182]

Uiteindelijk moet je class er zo uit zien:

[gist id=1274186]

ArrayList<Tweet> en de List interface

Je hebt nu een Tweet object waar je een tweet in kan opslaan. Straks wil je op de een of andere manier meerdere Tweets op kunnen slaan. Je zou het op deze manier kunnen doen: [gist id=1274323]

Als je je afvraagt waarom ik in dit voorbeeld met de nummering van Tweet objecten ben begonnen bij het getal 0: opsommingen in Android (en in programmeren in het algemeen) beginnen bijna altijd bij het getal 0. Daarna komt pas het getal 1 (dus eigenlijk als tweede getal). Een voorbeeld is dat je een String hebt waar het woord "huis" in staat: wanneer je het eerste karakter uit het woord wil opvragen moet je de method charAt gebruiken en daarbij het getal 0 als parameter meegeven, of als je het karakter op positie nummer 1 opvraagt dan krijg je de letter u terug. Ander voorbeeld is als je in een lijst van verschillende getallen verwijst naar het getal op de 10e positie, dan rekent Android vanaf het getal 0, dus daarmee vraag je eigenlijk het 9e getal op ( 0, 1, 2, 3, 4, 5, 6, 7, 8 en 9 zijn samen tien getallen).

Maar dat wordt best lastig om deze class definitief te ontwerpen: je weet niet hoeveel tweets er gedownload worden, en het is best lastig om voor elke tweet dan een aparte get method te maken: [gist id=1274944] Alleen al als je 6 tweets wil opslaan krijg je een vrij lange class, terwijl daar dan nog de andere code in moet. Als oplossing voor dit probleem kan je een lijst gebruiken om meerdere objecten van hetzelfde type op te slaan. In dit geval wil je Tweet objecten op kunnen slaan in die lijst. Nu komen twee alinea's met belangrijke zinnen, je gaat de basis leren van wat een interface is. In Android heb je meerdere classes die zich gedragen als een lijst. Dat betekent: er zijn methods in die classes aanwezig die je nodig hebt als je de class als een lijst wil gebruiken. Ergens in Android is beschreven: als een class deze specifieke methods heeft, dan is het een lijst, onafhankelijk van welke superclass de class heeft. Die beschrijving van wanneer iets een List (lijst) is, dat noem je de List interface, hier betekent interface dus hoofdzakelijk "een beschrijving van verplichte methods". Op deze pagina zie je de beschrijving van de List interface. De andere pagina's die ik je ooit gaf waren beschrijvingen van classes, en die begonnen altijd met public class en dan de naam van de class (bijvoorbeeld View). Echter, deze pagina begint met de tekst public interface List. Een interface is dus wat anders dan een class, om de eerder genoemde redenen: het is alleen een beschrijving van methods, zonder dat er code in de methods zelf staat (verder kan je ook statische variabelen in een interface opschrijven, maar dat gebruik je eigenlijk haast nooit). Als je verder naar de pagina van de List interface kijkt, dan zie je allerlei methods staan zoals: add, addAll, clear, get en remove. Als je de get method of de remove method aanroept moet je een int (een getal zonder komma's) meegeven, en die int verwijst naar de positie van het element dat je wil opvragen of verwijderen. Lees anders eens de beschrijvingen van de methods die ik noemde, dan krijg je al een aardig beeld van wat een List doet: een List heeft methods voor het opvragen, opslaan, aanpassen en verwijderen van objecten in een lijst, gebonden aan de volgorde van de List. Dat is precies wat je nodig hebt als je met meerdere objecten wil werken ;) Als je deze methods wil gebruiken, dan moet je op zoek gaan naar een class die deze methods heeft, of anders gezegd: een class die de List interface implementeert. Die classes kan je vinden bovenin de pagina onder de tekst "Known indirect Subclasses". Daar zie je onder andere de class ArrayList<E> staan, een beschrijving ervan zie je op deze pagina. Als je naar de superclass van de ArrayList<E> kijkt op deze pagina (AbstractList), dan zie je bovenin staan: Implements List<E>. Dus de superclass van ArrayList<E> heeft de methods van de List interface (zoek anders even de methods die ik eerder noemde ook even op op de pagina van de AbstractList). En omdat de ArrayList<E> een subclass is van de AbstractList class is de ArrayList<E> class zelf ook een List. Als je de ArrayList<E> class wil gebruiken moet je aangeven met welke extra type object je hem wil gebruiken, dat geef je aan tussen de kleiner dan '<' - en groter dan '>' haakjes. Kijk maar eens bijvoorbeeld naar de get method van de ArrayList<E> class: Die E die je schrijft in de naam van de ArrayList<E> class vind je terug als return type van de get method: als je de get method aanroept en als extra bericht een positie van de List meegeeft krijg je een object terug van het type E dat staat op die positie in de List. Het type van E mag je zelf bepalen als je een ArrayList definieert en aanmaakt. Wanneer je dan de volgende code zou gebruiken: [gist id=1275197] Dan zorg je ervoor dat de get method wordt veranderd van public E get naar public Tweet get zodat je dan deze method hebt: Op dezelfde manier worden in alle andere methods ook het type E vervangen door het type Tweet. Je zou tweetsList dan op de volgende manier kunnen gebruiken: [gist id=1275227] Dit zorgt ervoor dat van alle vier toegevoegde Tweet objecten er een Toast wordt getoond met daarin de tekst uit het Tweet object.

BaseAdapter subclassen in TweetsListAdapter

Deze paragraaf gaat over de BaseAdapter: dat is de class die de verbinding vormt tussen een lijst van objecten (de te tonen gegevens) en een ListView waarin ze getoond worden. Aan de ene kant heeft een BaseAdapter verbinding met de ListView en de individuele layout die geladen moet worden voor elk object dat getoond wordt in de ListView. Aan de andere kant heeft de BaseAdapter verbinding met een lijst (List) van gegevens die hij verwerkt in de ListView. Als je een normale simpele layout gebruikt met bijvoorbeeld één TextView en één Button en één EditText, dan laad je de layout in je Activity en koppel je daarna de aparte Views aan je code via de method findViewById. Maar omdat je met een ListView een extra layout gebruikt en een aparte List van gegevens, is het handig om daar nog een extra class (de BaseAdapter class dus) bij te gebruiken om dit alles aan elkaar te koppelen. Als je een BaseAdapter wilt gebruiken, dan kan je hem het beste subclassen in een eigen class die je vervolgens zelf aanpast:

Ik vertelde je net over de twee kanten van het een BaseAdapter class: het bijhouden van de lijstgegevens én het genereren/bijhouden van individuele Views voor de ListView. Voor het bijhouden van de lijstgegevens kan je een variabele van het type ArrayList<Tweet> gebruiken die je mTweets noemt.

[gist id=1275239]

Voor het genereren van Views die aan de ListView toegevoegd gaan worden heb je een LayoutInflator nodig. De LayoutInflator class kan je via de method inflate een layout bestand om laten zetten naar een View zoals een RelativeLayout of een LinearLayout of een Button. Als tweede parameter geef je in de method de View mee waar hij in geladen moet worden (dat wordt dan het root element), of een null object als je hem 'los' wilt genereren: [gist id=1275400]

[gist id=1275413] We gaan nu de constructor method aanmaken. In de constructor willen we de variabelen mTweets en mInflator vullen. mTweets kan je gewoon vullen met een bericht die via de method parameters binnenkomt. Als je een Inflator object wil aanmaken kan je dat doen via de statische methode from, en als extra bericht (parameter) heb je hiervoor een Context object nodig: Een Context object is een object dat de informatie bevat over de app die op dat moment actief is en het is vanuit je code de verbinding naar het Androidapparaat waar je app op draait. Je gebruikt het Context object bijvoorbeeld ook als je GPS toegang wil opvragen of bestanden wil inlezen of wegschrijven. Als je een LayoutInflator wilt gebruiken heeft de LayoutInflator class een Context nodig om de layout bestanden te kunnen laden die je in de method inflate aangeeft via de R class (bijv R.layout.main). Als je namelijk geen Context object meegeeft kan het LayoutInflator object niet zelf uitzoeken van welke app je R.layout.main wilt laden (want het kan best zijn dat er ook een andere app op het Androidapparaat staat waarin een layout bestand wordt gebruikt die ook main.xml heet). Je moet dus aangeven dat je met de LayoutInflator de layouts wil gebruiken die horen bij je huidige app. Om deze reden heb je bij de constructor ook een Context object nodig zodat je je LayoutInflator kan aanmaken.

[gist id=1275561]

[gist id=1275571]

Je hebt nu de data die hoort bij deze TweetsListAdapter gevuld met de nieuwe data die je kreeg uit de parameter van de constructor method. Zodra je de data hebt aangepast in een subclass van een BaseAdapter moet je de method notifyDataSetChanged aanroepen zodat er een berichtje gestuurd wordt naar de ListView dat de data die in de ListView getoond wordt ververst is:

  • Voeg deze code toe:

[gist id=1275583]

[gist id=1275575]

Er staan nog vier lege methods in je class, we gaan het nu hebben over de eerste drie. Dit zijn de methods die de methods in de BaseAdapter class gebruiken om informatie te krijgen over de data die je gebruikt (want wanneer je een subclass maakt dan doe je dat omdat je je eigen methods wil laten samen werken met de methods uit de superclass). De data die je gebruikt heb je opgeslagen in de variabele mTweets. Al die drie methods gaan dus eigenlijk over de variabele mTweets. De method getCount wil weten hoeveel objecten je in je variabele mTweets hebt. Deze method moet je zelf afmaken zodat hij verbonden is aan je mTweets object. De ArrayList<E> class (of eigenlijk de List interface) heeft de method size die je een int teruggeeft die berekent hoeveel objecten er in de List zitten, in dit geval de ArrayList<Tweet>. De method getCount kan je nu afschrijven.

[gist id=1275684] De method getItem is bedoeld om een object op te vragen uit de lijst van gegevens, dus in dit geval het opvragen van een object uit je mTweets List. Het object dat opgevraagd wordt, is in de method parameter aangeven met een int die als naam position heeft. Bij een List kan je een element uit de rij van objecten opvragen door de method get te gebruiken en een int te geven. De int geeft dan aan van welke positie je in de rij het object wilt opvragen. De method getItem in je TweetsListAdapter heeft nu nog als return type een object van het type Object. Omdat je weet dat je toch altijd een Tweet object teruggeeft met deze method kan je het return type van de method veranderen naar het Tweet object.

[gist id=1275872]

De method getItemId is een makkelijke: deze vraagt welke uniek getal hoort bij een positie uit de lijst. Maar we gebruiken geen speciale unieke getallen in de List, we gebruiken alleen maar een rij van opeenvolgende nummers om een Tweet op te vragen via de method mTweets.get op een bepaalde positie. Om die reden kan je gewoon het gevraagde getal terug geven. Omdat je als return type een long hebt kan je ook gewoon de int teruggeven: een long is net zoals een int een afgerond getal maar kan getallen opslaan tot een veel hoger getal.

[gist id=1275896]

Nu heb je bijna je TweetsListAdapter class af. Je hoeft alleen nog maar de method getView af te maken (implementeren).

De getView method van de BaseAdapter class

De method getView wordt aangeroepen elke keer als er een View gegenereerd moet worden voor een object in de List, in dit geval is dat voor elk Tweet object in mTweets. Bij het aanroepen van deze method worden drie extra berichten (parameters) meegegeven. De int position berekent voor welk object in de List een View gegenereerd moet worden, gebaseerd op in welke volgorde de objecten (Tweet objecten in dit geval) getoond moeten worden. De derde parameter is ViewGroep parent, dat is de ViewGroup waarin de View die je in deze method genereert wordt opgeslagen. Deze derde parameter wordt niet in deze DevTutorials behandeld en kan je voor nu vergeten (je gebruikt hem namelijk niet zo vaak). De tweede parameter, View convertView, is een View die Android onthouden heeft van de vorige keren dat deze method was aangeroepen en je een View teruggaf als antwoord. Het kan ook zijn dat deze convertView leeg is, dus dat hij de waarde null heeft. Als de View convertView leeg is, dan moet je hem zelf aanmaken via de LayoutInflator mInflator die je gevuld had in de constructor method. Aan het begin van deze DevTutorial had je een layout gemaakt die je tweet_listitem.xml noemde. Dit is de layout die je nu moet gebruiken om die Views te genereren zodat je ze vervolgens kan invullen vanuit een Tweet object. De volgende zinnen zou je nu moeten kunnen begrijpen: het eerste wat je dan zou moeten doen is dus kijken of convertView leeg is (of niet) en hem vervolgens wel of niet aanmaken. Daarna zou je twee TextViews moeten definiëren en deze vullen door de method findViewById op de View convertView aan te roepen (de method findViewById is ook aanwezig op het View object zelf zoals je hier kan zien). Daarna zou je de method getText en getCreatedAt moeten gebruiken om de TextViews te vullen. Je zou dan ongeveer op het volgende uitkomen: [gist id=1276022] Maar deze method kan nog efficiënter. De regels code die onnodig veel tijd kosten in deze method zijn de volgende twee: [gist id=1276080] Elke keer opnieuw twee TextViews opzoeken in je View is eigenlijk niet nodig, want vaak heb je al de twee TextViews opgezocht de vorige keer dat je deze View genereerde. Misschien is het handig dat we de twee TextViews ergens kunnen opslaan zodat we alleen maar de eerste keer dat we een View gaan genereren de method findViewById hoeven te gebruiken. Dit gaan we in twee stappen doen: eerst maken we één class met de naam Holder die twee TextViews op kan slaan, en daarna gebruiken we de method setTag op de View om het object op te slaan.

[gist id=1276162]

Zoals je ziet zijn de twee TextViews gedefinieerd in combinatie met het woordje public. Dat betekent dat je de TextViews van buitenaf af kan aanpassen en je niet aparte get methods en set methods nodig hebt om de inhoud aan te passen. We maken expres geen constructor aan omdat we geen extra berichten nodig hebben bij de constructor, dan kunnen we de standaard constructor gebruiken waar geen parameters in zitten: [gist id=1276192]

Dit is een van de weinige keren dat je variabelen die je in het class block definieert het woordje public mag meegeven. Omdat deze inner class alleen maar draait om de inhoud van de twee TextViews is het in dit geval geoorloofd. In bijna alle andere gevallen moet je het woordje public niet gebruiken en er speciale public methods voor maken die beginnen met get en set (dus bijvoorbeeld getName en setName). Het idee erachter is dat je andere objecten niet zomaar toegang mag geven tot de 'binnenkant' van je class, daarmee verlies je de controle over hoe objecten aangepast kunnen worden waardoor er gemakkelijk bugs kunnen optreden in je code. En het moedigt aan tot slechte codekwaliteit als je zomaar van elke class alle variabelen vandaan kan plukken en aan kan passen. Soms is er nog wel een goede reden om statische variabelen public te maken.

Dit holder object van het type ViewHolder gaan we koppelen aan de View door middel van de method setTag als we net een View hebben gegenereerd. Daarna koppelen we de TextViews die we via findViewById opzoeken aan de holder variabele. Of als we al een View meekrijgen in method parameter convertView dan gebruiken de method getTag op de View convertView om het holder object wat we eerder hadden opgeslagen op te vragen. Deze twee dingen voeren we uit met een if else block. Daarna word alsnog de juiste Tweet opgevraagd en worden de gegevens uitgelezen naar de TextViews.

[gist id=1276208] Je class TweetsListAdapter is nu af.

Methods kopiëren en variabelen definiëren in TweetsActivity

Alle benodigde classes heb je nu af. Je hoeft ze alleen nog maar op de juiste manier met elkaar te combineren. Eerst moeten we de bekende methods en variabelen toevoegen aan de TweetsActivity class. We voegen een aantal variabelen toe in het class block, zodat je de ListView, de Twitter-gebruikersnaam, de List van Tweet-objecten en de ProgressDialog bij kan houden in je Activity. Daarna kopiëren we de method en inner class die nodig zijn voor het downloaden van de JSON-gegevens, die de tweets bevatten. Verder heb je nog de method nodig die het downloaden opstart.

Het begin van je class moet er nu als volgt uitzien:

[gist id=1276310]

De volgende opdrachten gaan over de DownloadTweetsTask:

[gist id=1276333]

[gist id=1276347]

[gist id=1276385]

  • Zorg ervoor dat deze method wordt opgestart aan het einde van de method onCreate, anders start deze Activity op en gebeurt er verder niks.

Het resultaat verwerken: processResults en updateView

Naast de bekende methods, heb je alleen nog maar twee methods nodig. Als eerste de method processResults waarbij je de JSON-String omzet naar een ArrayList<Tweet> object met de naam mTweets. Als tweede, de extra code in de method updateView die mTweets koppelt aan een object van het Type TweetsListAdapter en deze koppelt aan je ListView. Zoals je weet krijg je niet een String terug waarin een JSON-object in zit, maar krijg je een String terug waar een JSON-array in zit: een opsomming van meerdere JSON-objecten. Deze String kan je daarom het beste gebruiken om een nieuw object van het type JSONArray aan te maken. JSONArray heeft niet de methods van een List interface, maar heeft wel de method length die aangeeft hoeveel objecten er in de JSON zitten en de method getJSONObject, waarmee je via een int een object van het type JSONObject op kan vragen op een bepaalde positie. Met een for block gaan we alle getallen langs, afhankelijk van hoe lang de JSON-array is. Voor elk getal vragen we het JSONObject op en daarmee maken we steeds een Tweet-object: we roepen met het JSONObject de constructor van de Tweet class aan. Vervolgens stoppen we elke keer als we een Tweet object hebben aangemaakt deze in de mTweets met de method add.

Je method processResults moet er nu als volgt uitzien:

[gist id=1276432]

Je hoeft nu alleen nog maar het JSONArray object met de naam jSONArray uit te lezen. Dat kan je doen met een for block. Een for block had je al eens gebruik in DevTutorial 4. Je moet ervoor zorgen dat je het for block net zo vaak uitvoert als het aantal JSONObject objecten dat in het JSONArray object zit. Hiervoor moet je in het for block een counter aanmaken die je op 0 zet. Het for block moet net zolang uitgevoerd worden, totdat het getal gelijk is aan de lengte van het JSONArray object met de naam jSONArray. Als deze getallen gelijk zijn, dan betekent dat dat het laatste JSONObject net daarvoor heeft uitgelezen (Dit heeft te maken met het feit dat ook het tellen van de elementen in een JSONArray object begint bij het getal 0).

[gist id=1276472] Elke keer als het for block wordt uitgevoerd, moet een JSONObject worden uitgelezen uit het JSONArray object via de method getJSONObject. Als extra parameter moet je bij deze method de int counter meegeven, zodat steeds het volgende JSONObject uitgelezen wordt. Dit JSONObject moet je vervolgens gebruiken om een Tweet Object met de naam tweet aan te maken, zodat je deze kan toevoegen aan mTweets via de method get.

[gist id=1276482]

Ok... eindelijk ;) het resultaat kan je nu bijna gaan zien:

Nu moet je de constructor van de TweetsListAdapter class gebruiken. Zoals je weet, want je hebt deze constructor net zelf gemaakt, moet je een Context object meegeven en een ArrayList<Tweet> object. Je kan hier bij "Known Indirect Subclasses" zien dat de Activity class een indirecte subclass is van de Context class, dus je kan de huidige class (TweetsActivity) meegeven aan de constructor. Dat doe je met het woordje this. Voor het ArrayList<Tweet> object moet je uiteraard de variabele mTweets gebruiken.

[gist id=1276573]

Nu heb je een TweetsListAdapter object die je kan gebruiken op de ListView door de volgende method aan te roepen (hoera:) ):

[gist id=1276591]

Je app is nu af. Je kan hem uitproberen door hem op te starten in de emulator: .

Je hebt nu twee Activities gemaakt die opzich best goed werken. Een puntje dat wel belangrijk is en nog niet aan bod is gekomen, is in welke methods je bepaalde code stopt en in welke classes je bepaalde methods stopt. Het maakt bijvoorbeeld erg veel uit of je alle code in één class probeert te stoppen, of dat je je code op een logische manier verdeelt over verschillende classes. Een optimalisatie in deze app zou bijvoorbeeld kunnen zijn dat je maar één class gebruikt om je internetverbinding in te stellen en uit te voeren, in plaats van in twee Activity classes hiervoor bijna dezelfde code te schrijven. Wanneer je nadenkt over waar je code plaatst in welke methods en classes, dan denk je na over het design pattern van je app. Design patterns is een onderwerp op zichzelf, maar is uiteindelijk essentieel voor het schrijven van goede code. Inzicht in welke design patterns je wanneer gebruikt, bouw je op in de loop van de tijd dat je zelf apps schrijft, of wanneer je in andere talen schrijft, waardoor je patterns met elkaar kan vergelijken en ze daardoor beter begrijpt. Een overzicht van een aantal patterns kan hier vinden. Belangrijke patterns zijn onder andere MVC, Observer, Singleton en Utility.

Statische variabelen

Je app doet het nu wel aardig. Alleen duurt het verwerken van de JSON-Tweet gegevens best wel lang. Als je tijdens het downloaden in je Logcat kijkt (zie DevTutorial 4), dan zie je meerdere keren onderstaande regels: [gist id=1280784] Als je één zo'n regel bekijkt: "Loaded timezone names for en in 335 ms", dan zie je dat dat te maken heeft met het laden van namen voor de Engelse tijdzone. Dat gebeurt wanneer je in de Tweet class een Locale object aanmaakt en deze gebruikt om een SimpleDateFormat aan te maken. Als je 30 tweets downloadt, dan moet je 30 keer ongeveer 333 milliseconden wachten, dat is in totaal minimaal 9,99 seconden. Dit is eigenlijk veel te lang en is niet nodig, als je bedenkt dat je toch steeds hetzelfde SimpleDateFormat aanmaakt met precies dezelfde instellingen. Het zou handig zijn als je het SimpleDateFormat object maar één keer hoeft aan te maken en dat je datzelfde object kan hergebruiken, elke keer als je een Date object wil inlezen. Op de manier hoe je je class hebt geschreven is dat niet mogelijk: elke keer als er een nieuw Tweet object wordt aangemaakt, staan alle gegevens op de standaardwaarde (meestal is dat null) en moet je deze opnieuw vullen. Dit vullen ervan doe je steeds in de constructor method. Wat je wel kan doen, is een variabele statisch maken. Wanneer je een variabele statisch maakt, dan wordt hij ingesteld zodra je de app opstart en blijft hij bestaan, net zolang totdat je je app afsluit. De statische variabele heeft dan eigenlijk zijn 'eigen leven' en is dan niet afhankelijk van de class waarin hij is gedefiniëerd. Een gewone variabele wordt opnieuw ingesteld als je een nieuwe class maakt, maar een statische variabele blijft dan zijn waarde behouden. Als je nou de variabele die de SimpleDateFormat onthoudt statisch maakt, dan hoef je deze maar één keer in te vullen. Dat kost je in totaal ongeveer 333 milliseconden, in plaats van 9,99 seconden. Een variabele die je statisch maakt, moet je definiëren in het class block van je class. Voeg de statische variabele SimpleDateFormat sEnglishSimpleDateFormat toe aan het class block van je Tweet class en gebruik daarvoor het woordje static: [gist id=1280869]

Je ziet nu dat de naam van de variabele begint met een s in plaats van een m wat je anders altijd doet. Dit heeft te maken met hoe je in Android namen geeft aan variabelen: namen van variabelen die je in het class block van je class definieert laat je altijd beginnen met de letter m. Maar als ze statisch zijn en je definieert ze in je class block dan begint de naam met een s. Op deze manier weet je van deze soorten variabelen snel wat ze zijn als je ze later tegenkomt in je code.

Kijk nu met een if block in je constructor of de statische variabele sEnglishSimpleDateFormat de waarde null heeft. Als dat het geval is, moet je hem invullen in dat if block. Als dat niet het geval is, dan kan je de statische variabele gebruiken zonder hem opnieuw in te hoeven vullen.

[gist id=1280893]

[gist id=1280894] Als je nu TweetsActivity opent, hoef je veel minder lang te wachten.

Extra oefening

Als extra oefening, kan je proberen een zoekscherm voor tweets te maken. Je kan een Button maken in MainActivity die de Activity SearchActivity opstart. In de Activity SearchActivity plaats je een EditText, een Button en een ListView. Zodra de gebruiker een zoekopdracht heeft ingevuld in de EditText en op de Button drukt, wordt de zoekopdracht gestart. Tijdens het zoeken (en het downloaden van de tweets) toon je weer een ProgressDialog. Na het zoeken en downloaden, roep je de methods processResults en updateView aan, zodat het zoekresultaat wordt weergegeven in de ListView. Voor het zoeken moet je deze url gebruiken: [gist id=1280922] Je hebt nu redelijk wat kennis van Android. Als je nu specifieke dingen wilt weten over Androidprogrammeren, kan je langzamerhand ook gaan zoeken naar Engelstalige tutorials via Google. Dan kom je waarschijnlijk wel nieuwe onderwerpen tegen, maar ook bekende onderwerpen die je hier geleerd hebt, zodat de kennis aan elkaar kan koppelen :) Oh, maar wacht nog wel even met het zoeken naar tutorials voor het downloaden van afbeeldingen en het opslaan objecten in de lokale SQLite-database. Dat komt namelijk in de volgende DevTutorials ;) Tot de volgende week!

Sourcecode

Als je vragen hebt over deze DevTutorial dan kan je ze stellen aan mij en andere programmeurs in deze forum thread. Als je wil weten hoe de code van deze app er uiteindelijk uit moet zien, kan je hier de sourcecode bekijken op github.

Lees meer over:
DevTutorial

Plaats reactie

666

0 reacties

Laad meer reacties

Je bekijkt nu de reacties waarvoor je een notificatie hebt ontvangen, wil je alle reacties bij dit artikel zien, klik dan op onderstaande knop.

Bekijk alle reacties

Meest gelezen

ING kampt met storing: mobiel bankieren niet mogelijk (update)

Veel klanten van de ING hebben sinds maandagochtend last van problemen met internetbankieren. De bank geeft aan dat er ze wer...

App van de Week: de Nike Run Club viert de verjaardag van Nike

Sportmerk Nike wordt dit jaar 50 en dat is zeker een leeftijd om even bij stil te staan. Een halve eeuw is het merk al bezig...

Samsung Galaxy Watch 5: alle geruchten op een rij

De lancering van de Samsung Galaxy Watch 5-serie komt er aan, en daar kijken veel mensen naar uit. Voor iedereen die geïntere...

ANWB geeft 20 keer een jaar gratis parkeren weg: zo kan je deelnemen

De ANWB Onderweg-app viert een feestje vanwege het tienjarig bestaan. De app geeft daarom 20 keer een jaar lang gratis parker...

'Vlaamse stem van Google Maps in ontwikkeling, zo hoor je ze'

Google werkt aan een Vlaamse stem voor de routebegeleiding in Maps. Als je de navigatie-app gebruikt kan je de stem in specif...

Beveiligingsexperts vinden dit EU-voorstel voor chatapps "griezelig"

Een voorstel van de Europese Commissie over de strijd tegen kindermisbruik op berichtenapps doet bij beveiligingsexperts de w...

Motorola Razr 3 krijgt veel grotere vouwbare schermen

Voor zijn volgende generatie van bouwbare telefoons gebruikt Motorola twee grotere schermen. De Motorola Razr 3 heeft een 6,7...

WhatsApp Status laat straks linkvoorbeelden zien met meer details

Vorige week konden we de emoji-reacties, en de mogelijkheid om grotere WhatsApp-groepen te maken en grotere bestanden verstur...

Sony WH-1000XM5 review: meer van het beste voor een hogere prijs

De Sony WH-1000XM4 uit 2020 heeft een opvolger gekregen. Die opvolger heet, hoe kan het ook anders, Sony WH-1000XM5. Deze n...

Lees meer

Net binnen

Sony WH-1000XM5 review: meer van het beste voor een hogere prijs

De Sony WH-1000XM4 uit 2020 heeft een opvolger gekregen. Die opvolger heet, hoe kan het ook anders, Sony WH-1000XM5. Deze n...

Samsung Galaxy Watch 5: alle geruchten op een rij

De lancering van de Samsung Galaxy Watch 5-serie komt er aan, en daar kijken veel mensen naar uit. Voor iedereen die geïntere...

Beveiligingsexperts vinden dit EU-voorstel voor chatapps "griezelig"

Een voorstel van de Europese Commissie over de strijd tegen kindermisbruik op berichtenapps doet bij beveiligingsexperts de w...

ANWB geeft 20 keer een jaar gratis parkeren weg: zo kan je deelnemen

De ANWB Onderweg-app viert een feestje vanwege het tienjarig bestaan. De app geeft daarom 20 keer een jaar lang gratis parker...

'Vlaamse stem van Google Maps in ontwikkeling, zo hoor je ze'

Google werkt aan een Vlaamse stem voor de routebegeleiding in Maps. Als je de navigatie-app gebruikt kan je de stem in specif...

Lees meer