Naar content
Trending apps
  • Inbox by Gmail

  • Maps: Navigatie en OV

  • WhatsApp Messenger

  • Messenger

  • Facebook

Trending games
  • Dr. Mario World

  • Harry Potter: Wizards Unite

  • Breaking Bad: Criminal Elements

  • The Elder Scrolls: Blades

  • Ghostbusters World

Trending smartphones
  • ASUS ZenFone 6

  • OnePlus 7 Pro

  • Google Pixel 3a XL

  • Xiaomi Mi 9T

  • Huawei P30 Pro

Nieuwste tablets
  • Samsung Galaxy Tab A 10.5

  • Samsung Galaxy Tab S4

  • Samsung Galaxy Tab S3 9.7

  • Asus Zenpad 3S 10

  • Sony Xperia Z4 Tablet

DevTutorial 7 - ArrayList en ItemLayout via BaseAdapter aan ListView koppelen

· 13 oktober 2011

In deze zevende aflevering gaan we verder bouwen op de Twitter-app uit de vorige twee DevTutorials. Een mooie toevoeging aan deze app is het tonen van tweets in het tweede scherm. Dit is de laatste lange DevTutorial, de DevTutorials 8, 9 en 10 zijn meer code-voorbeelden. Ik weet dat ik beloofd heb om deze DevTutorial ook al korter te maken, maar kon het niet laten om her en der toch wat meer dingen uit te leggen. Maar als je deze DevTutorial goed doorleest, leer je weer best een hoop nieuwe nuttige stukken Androidkennis.

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:

  • Een aparte layout aanmaken voor elk item in je ListView.
  • Een Tweet class maken die de JSON-string kan uitlezen die je downloadt vanaf Twitter.com.
  • Een datum vanaf een JSON-string inlezen naar een Date object in je Tweet class.
  • Meerdere objecten van hetzelfde type (in dit geval van het type Tweet) opslaan in een ArrayList<E> of beter gezegd: ArrayList<Tweet>.
  • Wat een interface is, en welke methods de List interface bevat.
  • Hoe de BaseAdapter class dé koppeling is tussen een layout van een item en een List van objecten zodat je ze samen kan tonen in een ListView.
  • Welke methods je van de BaseAdapter moet invullen, inclusief de getView method.
  • Wat de JSONArray class is, en hoe je deze met een for block kan uitlezen.

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:

  • Open het layout-bestand tweets.xml in de Graphical Layout Editor Tab.
  • Verwijder de RelativeLayout met de naam relativeLayout1
  • Voeg een ListView toe en geef hem als id @+id/lvTweets
  • Zet van lvTweets de property layout_width op fill_parent en de property layout_height ook op fill_parent
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:

  • Maak een nieuwe Android XML file aan door met de rechtermuisknop te klikken op de map layout in het Package Explorer Window, en klik op New --> Android XML File.
  • Geef het xml-bestand de naam tweet_listitem.xml, selecteer als type resource een Layout en selecteer als root element de RelativeLayout: .
  • Voeg nu twee TextViews toe.
  • Zet van de eerste TextView de Id property op @+id/txtvText, de Max lines property op 4, de Min lines property op 2, de Style property op @style/text_p, de Layout_height property op wrap_content, de Layout_width property opfill_parent en de Layout margin property op 10dip. De Text property moet je leeg maken.
  • Zet van de tweede TextView de Id property op @+id/txtvCreatedAt, de Padding bottom property op 10dip, Layout align parent right property op true, de Layout below property op @+id/txtvText, de Style property op @style/text_p, de Layout height property op wrap_content, de Layout width property opwrap_content en de Layout margin property op 10dip. De Textproperty  moet je leeg maken.
  • Van de RelativeLayout, zet de Background property op @drawable/bg_app, de Layout height property op wrap_content en de Layout width property op fill_parent.
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:Als je dit vergelijkt met de JSON uit de vorige DevTutorial: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:Aan het einde van één zo'n JSON-Tweet object staan de elementen text en created_at: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: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: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: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: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: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:

  • Net zoals het aanmaken van een TwitterUser class in de de vorige DevTutorial; klik met de rechtermuisknop op je package me.moop.mytwitter in je Package Explorer Window en klik op New --> Class.
  • Vul bij Name de naam Tweet in, en klik op Finish: .
  • Open je class Tweet die staat in het bestand Tweet.java, en voeg twee class variabelen toe: Date mCreatedAt en String mText. Voeg deze toe bovenin je class block.
  • Nu moet je een constructor toevoegen. Dat is een method zonder return type (zie DevTutorial 6 paragraaf "Methods en return types: getUserName en getWebsite" en de paragraaf "Constructor: een speciale method om je class aan te maken en als resultaat een object ervan terug te krijgen"). Als parameter moet de constructor een object van het type JSONObject accepteren met de naam jSONObject.
  • Gebruik de method optString op het JSONObject met de naam jSONObject om het element created_at uit te lezen naar een String in je method block die je dateString noemt.
  • Maak een Locale object aan met de naam englishLocale, en gebruik hem in combinatie met de beschrijving van de datum/tijd afkorting om een SimpleDateFormat object aan te maken. Dit nieuwe SimpleDateFormat object moet je de naam englishSimpleDateFormat geven:
  • Gebruik met dateString als extra bericht (parameter) de method parse op het englishSimpleDateFormat om een Date aan te maken. Stop deze Date in de class variabele mCreatedAt die je al eerder had gedefinieerd in het class block. Je krijgt een suggestie voor het aanmaken van een try/catch block, maak deze ook aan:
  • Vul aan het einde van de method de class variabele mText met het element text dat je uit jSONObject haalt via de method optString.
  • Voeg ook de methods getCreatedAt en getText toe. Zie de vorige DevTutorial voor inspiratie.
  • Maak ook de nodige imports.
Uiteindelijk moet je class er zo uit zien:

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:

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: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 deremove 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: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: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:

  • Klik in het Package Explorer Window met de rechtermuisknop op je package me.moop.mytwitter en maak via New --> Class een nieuwe class aan.
  • Geef de class TweetsListAdapter als naam en vul bij Superclass de class android.widget.BaseAdapter in. Zorg ervoor dat het vinkje bij "Inherited abstract methods" aan staat en klik op Finish: .
  • Als je nu class opent in het bestand TweetsListAdapter.java zie je dat er al vier methods voor je aangemaakt zijn: getCount, getItem, getItemId en getView. Deze methods zijn verplicht voor het kunnen subclassen van de BaseAdapter class. Waarom dat zo is wordt je in DevTutorial 9 uitgelegd.

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.

  • Voeg deze variabele toe bovenin het class block van je TweetsListAdapter class:

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:

  • Dit ga je straks gebruiken in de method getView van je TweetsListAdapter class. Daarvoor moet je nu een variabele van dit type definieren in het class block van je TweetsListAdapter class met de naam mInflator:

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.

  • Maak nu een constructor method aan in je TweetsListAdapter class en beschrijf twee parameters tussen de haakjes van de constructor method: Context context en ArrayList<Tweet> tweets:

  • Vul in je constructor nu het mTweets object met een nieuwe ArrayList<Tweet> door de constructor ervan aan te roepen. Daarna moet je de method addAll aanroepen op mTweets en als parameter het tweets object meegeven dat je als parameter in de method beschreef:
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:
  • Gebruik nu de statische method Layout.from en vul mLayoutInflator die je als resultaat van die method terugkrijgt:

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.

  • Zorg ervoor dat de method getCount er als volgt uit ziet:

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.

  • Gebruik de method get op het mTweets object om het gevraagde element terug te geven, en verander het return type van je method getItemId naar een Tweet object:

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.

  • Maak de method getItemId af door de int parameter als return value te gebruiken:

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:Maar deze method kan nog efficiënter. De regels code die onnodig veel tijd kosten in deze method zijn de volgende twee: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.

  • Maak eerst de volgende inner class aan, onderin het class block van je class TweetsListAdapter:

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:

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.

  • Probeer de volgende code aan de hand van bovenstaande uitleg te begrijpen en vervang de oude method getView voor deze:

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.

  • Tijdens het downloaden van de tweets willen we de gebruiker opzettelijk laten wachten, terwijl we een laad-boodschap tonen. Definieer een variabele van het type ProgressDialog, met de naam mProgressDialog, in het class block van je class TweetsActivity.
  • De gebruikersnaam hebben we straks ook nodig buiten de method onCreate. Definieer daarom een variabele van het type String, met de naam mUserName, in het class block van je class TweetsActivity.
  • De Tweet-objecten moeten ook bijgehouden worden in de Activity. Definieer daarom een variabele van het type ArrayList<Tweet>, met de naam mTweets, in het class block van je class TweetsActivity.
  • De ListView die je hebt toegevoegd in de layout tweets.xml moet ook bijgehouden worden in de Activity. Definieer daarom een variabele van het type ListView, met de naam mListView, in het class block van je class TweetsActivity.
  • De gebruikersnaam die je opvraagt uit extrasBundle: stop die niet meer direct in mTxtTitle, maar stop die in de variable mUserName.
  • Het instellen van de tekst op mTxtvTitle kan je weghalen uit de method onCreate, verplaats dit naar een nieuwe method die je updateView noemt. In die method moet je mUsername gebruiken om de tekst in te stellen op de mTxtvTitle. Je moet deze method nog wel aanroepen aan het einde van je method onCreate.
Het begin van je class moet er nu als volgt uitzien:
  • Voor het instellen van de internetverbinding verandert er niks. Kopieer de method createHttpClient één op één door naar je TweetsActivity class.
  • Kopieer ook de inner class DownloadUserInfoTask één op één door naar je TweetsActivity class, maar pas nog wel de naam van de class aan naar DownloadTweetsTask.
De volgende opdrachten gaan over de DownloadTweetsTask:
  • Je hoeft nu geen aparte string username meer te gebruiken in de method doInBackground voor het uitlezen van de Twitter-gebruikersnaam. Gebruik in plaats daarvan bij het aanroepen van de method URLEncoder.encode gewoon de variable mUsername.
  • Voor het downloaden van de tweets moet je een ander internetadres gebruiken. Gebruik nu deze String als fetchUrl:
  • Statuscode 404 zul je niet meer krijgen, maar statuscode 401 nu soms wel. Verander in de method onPostExecute het getal 404 naar 401 en verander het bericht naar "De timeline van deze gebruiker is niet publiekelijk toegankelijk."
  • We gaan een aparte method aanmaken voor het verwerken van de JSON die je in een String-object terugkrijgt. Verander de regel die een nieuw TwitterUser-object aanmaakt naar een method-aanroep naar de method processResults, waarbij je mResultString als extra bericht meegeeft.
  • De method-aanroep naar updateView kan je laten staan bij statuscode 200, maar bij alle andere blocks waar je een Toast aanmaakt kunnen deze weg, het scherm isnamelijk toch al leeg als er zich een fout voordoet.
Je inner class DownloadTweetsTask moet er nu als volgt uitzien:
  • Maak een aparte method aan die je de naam startDownloadingTweets geeft. Maak in deze method een nieuwe ProgressDialog, zoals je dat ook deed in de vorige DevTutorial en start op dezelfde manier de het downloaden van de tweets:
  • 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.

  • Maak een method processResults aan, die als parameter een String heeft die je jSONArrayString noemt.
  • Maak in die method een nieuw ArrayList<Tweet> object aan en stop die in de class variabele mTweets. Het aanroepen van een constructor kan je typen door het woord new te gebruiken en daarna de naam van de class en daarachter een haakje openen '(' en sluiten ')'. Hier geef je mee aan dat je de constructor wilt gebruiken die geen parameters nodig heeft, want je geeft immers geen parameters mee.
  • Maak een nieuw object aan van het type JSONArray en geef hem de naam jSONArray. Typ daarachter meteen een '='-teken, waarna je de constructor aanroept van de JSONArray class met het woordje new. Tussen de haakjes moet je als parameter de String resultString meegeven, want je wil immers een nieuw JSONArray object aanmaken op basis van die JSON-string.
  • Plaats het aanmaken van een nieuw JSONArray object in een try catch block. In het catch block moet je een error opvangen van het type JSONException, die je de naam e geeft.
Je method processResults moet er nu als volgt uitzien:

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).

  • Dit kan je laten doen door het volgende for block toe te voegen, onderin het try block:

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.

  • Kijk eerst eens of je dit zelf kan maken (en voeg je code toe op de juiste plek) en kijk daarna pas hoe ik het gedaan heb:

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

  • Voeg een if block toe onderin de method updateView en laat deze kijken of de variabele mTweets niet de waarde null heeft (dat doe je met != in plaats van ==). Als mTweets namelijk niet de null waarde heeft, dan zitten er Tweet objecten in :)
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.
  • Gebruik de constructor van de TweetsListAdapter en plaats hem in de variabele van het type TweetsListAdapter met de naam tweetsListAdapter:

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

  • Gebruik deze method door deze code toe te voegen, onderin het if block:

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: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:

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.

  • Voeg dit if block toe en laat hem de andere regels code overschrijven die de englishSimpleDateFormat aanmaken en invullen:

  • En zorg ervoor dat je bij het inlezen van het Date object nu de statische variabele gebruikt:

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: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.

Spelfouten, taalfouten of inhoudelijke fouten ontdekt? Stuur dan een mailtje naar de auteur van dit artikel!

Reacties (11)
Bezig met laden van reacties...