Naar content
Trending apps
  • Google Meet

  • Google Duo: videogesprekken van hoge kwaliteit

  • Maps: Navigatie en OV

  • WhatsApp Messenger

  • Messenger

Trending games
  • Fortnite

  • Minecraft Earth

  • Dr. Mario World

  • Harry Potter: Wizards Unite

  • Breaking Bad: Criminal Elements

Trending smartphones
  • OPPO Find X2 Pro

  • Nokia 8.3

  • Samsung Galaxy A51

  • Poco F2 Pro

  • Xiaomi Mi Note 10 Pro

Nieuwste tablets
  • Samsung Galaxy Tab S6

  • Samsung Galaxy Tab A 10.5

  • Samsung Galaxy Tab S4

  • Samsung Galaxy Tab S3 9.7

  • Asus Zenpad 3S 10

DevTutorial 10 - Structuur aanbrengen in je broncode

· 09 november 2011

Deze laatste DevTutorial leert je het beginsel van hoe je moet nadenken over het indelen van je packages, classes en methods. Dit is vooral een DevTutorial waar theorie-uitleg een belangrijke plaats inneemt. Ook een belangrijk element in deze DevTutorial is een nette manier hoe je internetverbinding gebruikt. Dit heb je in vorige DevTutorials ook al gedaan, maar dat was meer om een idee te krijgen wat je er allemaal voor nodig hebt. Nu wordt je uitgelegd hoe je die code netjes verwerkt in je app, in combinatie met een Service.

Wat gaan we doen

De nadruk van deze DevTutorial ligt op het aanbrengen van structuur in je broncode ('source code') en als (belangrijk) voorbeeld wordt uitgelegd hoe je door middel van de IntentService class een elegante structuur aanbrengt bij het downloaden van gegevens. Hierbij worden de volgende vragen beantwoord:
  • Wat is structuur aanbrengen in je source code en waarom is het zo belangrijk?
  • Hoe stel je de hoofdstructuur van je app vast door middel van packages?
  • Wat voor belangrijke rol speelt de IntentService class in het structureren van je internetverbinding?
  • Hoe kan je de ResultReceiver class in combinatie met een eigen interface gebruiken met de IntentService class?
  • Hoe verwerk je al deze classes en interface in je Twitter-app
Succes er weer mee!

Waarom je source code structuur nodig heeft

Als je kijkt naar de code van de vorige DevTutorial en dan vooral naar MainActivity.java en TweetsActivity.java, dan zie je dat er best wat methods zijn die voor een groot deel dezelfde regels code bevatten. Dit geldt voor alle methods van ASyncTask class en de createHttpClient method. Dit heet code-duplicatie en is slecht voor de kwaliteit van je code. Stel dat je in de toekomst deze app wil gaan aanpassen: in plaats van api.twitter.com is de url veranderd naar json.twitter.com, of je wil een wachtwoord meesturen met de internetverbinding, zodat je privé-gegevens van Twitter-gebruikers wil kunnen opvragen. Dan zou je in beide Activity classes de code moeten aanpassen. Dat is om twee redenen geen goed idee. Ten eerste wil je zo min mogelijk werk hoeven doen als je je code gaat aanpassen. Anders kosten code-aanpassingen je teveel tijd, deze kan je als programmeur beter voor ingewikkeldere problemen gebruiken. Het tweede probleem als je bijna dezelfde code op meerdere plekken hebt zitten, is dat je het overzicht kwijtraakt van hoe je code wordt uitgevoerd (welke methods worden aangeroepen in combinatie met welke classes). Het kan bijvoorbeeld zijn dat in de Asynctasks van de ene class je eerst nog een andere method moet aanroepen voordat je de AsyncTask moet uitvoeren, of dat je een variabele eerst nog moet vullen. Door dezelfde code neer te zetten op verschillende plekken moedig je het jezelf aan door dezelfde code nét even ietsje aan te passen zodat hij beter lijkt te passen in die specifieke class. Als je dan later die code op verschillende plekken wil aanpassen ben je meestal vergeten hoe die code precies werkt. Hierdoor kan je je code kapot maken als je er later opnieuw aan werkt. Je raakt het overzicht op je code kwijt: dat is een van de ergste dingen die je als programmeur kan gebeuren. Bugs in overzichtelijke code heb je meestal snel opgelost, onoverzichtelijke code zonder bugs werkt op zich wel, maar zal je veel sneller bugs opleveren tijdens toekomstige aanpassingen. Een belangrijk voordeel van het zo min mogelijk code kopieën te hebben is dat je automatisch gaat nadenken over wat je code eigenlijk doet. Heb je bijvoorbeeld twee methods die langer dan 20 regels zijn en die voor 30 procent hetzelfde zijn, dan kan je je afvragen of je niet teveel werk wil gaan doen in je code. Als gevolg daarvan ga je nadenken over waarom de code zoveel op elkaar lijkt en vaak kan je dan niet een goede logische reden vinden waarom je twee keer voor een deel hetzelfde hebt getypt. Vervolgens ga je nadenken wat de overeenkomsten en de verschillen zijn tussen de twee methods, waardoor je op een grondig niveau onderzoekt wat voor soort werking je methods hebben. Als resultaat daarvan krijg je vaak beter inzicht in hoe je de taken tussen verschillende methods in je class verdeelt. Voorbeelden zijn:
  • In welke method bouw je je scherm op en koppel je je Views.
  • In welke method gebruik je je scherm om de juiste tekst en kleuren in te stellen op je Views.
  • In welke method(s) reageer je op het indrukken van knoppen.
  • Welke methods hebben verbinding naar ander methods.
  • Welke methods hebben alleen een interne functie.
  • Welke methods gebruik je om waarden in te lezen.
  • Welke methods gebruik je om een resultaat te produceren of aan te vragen.
  • Welke methods gebruik je om een resultaat te verwerken.
  • Welke methods gebruik je om waarden uit te lezen.
Deze inzichten kan je dan weer gebruiken om een duidelijkere scheiding te maken tussen wat voor taken je  methods uitvoeren. Zodra je dat in duidelijke programmeertaal hebt opgeschreven, is de code die je dan hebt herschreven een stuk duidelijker, waardoor je per method vaak dicht bij de essentie komt van wat je eigenlijk wil bereiken per method. Alleen de essentie van een method in een method uitvoeren helpt enorm voor de kwaliteit van je code: je schrijft geen overbodige code waardoor een method alleen maar doet wat hij zal moeten doen. Daardoor heb je als programmeur snel en goed je eigen code door, waardoor je geen onverwachte resultaten krijgt. Een ander woord voor onverwachte resultaten zijn "uitzonderingen", misschien ken je het Engelse woord hiervoor nog wel: Exceptions ;) (help!). In packages (groepen van classes, zoals je nog weet) zet je classes samen die op dezelfde manier werken, of een aantal classes die samen een bepaalde soort taak kunnen vervullen (bijvoorbeeld dat je alle subclasses van de Activity class in dezelfde package stopt of dat je alle classes die meewerken aan het opzetten en verwerken van een internetverbinding in dezelfde package stopt). In de classes zelf programmeer je de specifieke taak (of het takenpakket) van een class. Methods die je in je class schrijft hebben dan als taak het aan kunnen gaan van verbinding met andere classes. Dit zijn public methods zoals startDownload of onCreate of get-methods en set-methods. De andere soort taak die methods kunnen hebben, is intern de taak uitvoeren waarvoor de class bestaat. Vaak komt het voor dat methods beide soorten taken vervullen: methods zijn vaak best simpel (als je ze goed schrijft tenminste) waardoor het niet nodig is tot in den treure deze verantwoordelijkheden te blijven scheiden. Je kan vanuit verschillende invalshoeken de structuur van je code aanscherpen. Inspiratie hiervoor kan je vinden in dit boek, of op pagina's als deze. Inspiratie hoe het NIET moet kan je op deze pagina opdoen, en bijvoorbeeld deze, deze en dezepagina. Tijdens deze DevTutorial gaan we een elegante structuur aanbrengen in de code de we gebruiken voor de internetverbinding en het downloaden ermee. Alle structuren doorspreken kost teveel tijd, maar de structuur uit deze DevTutorial geeft je een aardig beeld over hoe je na kan denken over stevige code.

De hoofdstructuur van je app vaststellen

Als je nu links naar het Package Explorer window kijkt dan zie je 6 classes die allemaal in dezelfde package staan, namelijk me.moop.mytwitter. Als je ooit een project van iemand anders opent in Eclipse en je ziet maar één package waar ongeveer 6 classes (of meer) in staan dan is dat een teken dat er iets mist in de source code, namelijk structuur. We gaan onze source code indelen in classes die ongeveer dezelfde taak hebben en in classes die samenwerken om een bepaald takenpakket te kunnen vervullen. De eerste package die we aanmaken bevat alle classes die beschrijven hoe de belangrijke blokken informatie uit je app eruit ziet. In programmeertaal noem je dat de models (van het Engelse woord model): classes die verantwoordelijk zijn voor het bij kunnen houden van gegevens. In dit geval heeft je app twee models: het TwitterUser model, deze staat in de TwitterUser class, en het Tweet model, deze staat in de Tweet class. Deze twee classes gaan we stoppen in de package me.moop.mytwitter.model:
  • Klik met je rechtermuisknop op de package me.moop.mytwitter die in het Package Explorer window staat en klik op New --> Package: .
  • Vul als package naam me.moop.mytwitter.model in: .
  • Sleep nu de class Tweet vanaf de package me.moop.mytwitter naar de packge me.moop.mytwitter.model en laat hem daar los. Bij het scherm "update references to 'Tweet.java'" moet je op OK drukken zodat alle verwijzingen naar Tweet.java ook worden aangepast: .
  • Doe hetzelfde met de de class TwitterUser: stop deze ook in de package me.moop.mytwitter.model.
Nu heb je één package aangemaakt en gevuld zodat je één plek hebt waar je classes opslaat die gegevens bij kunnen houden. De tweede package die we aanmaken, maken we speciaal voor de class TweetsListAdapter, in bijna elke app maak je wel één of meerdere Adapter classes die je gebruikt om een lijst of een andere soort View te vullen.
  • Maak de package me.moop.twitter.helper aan en sleep de TweetsListAdapter class hierin.
Als je deze app ooit nog gaat uitbreiden en je maakt een nieuwe Adapter class aan, dan hoort hij in deze package thuis. De derde package is voor het downloaden en opslaan van de gegevens. Nu is de code hiervoor nog verspreid over classes MainActivity, TweetsActivity en DatabaseHelper, maar we gaan deze code beter structureren door deze uiteindelijk allemaal te plaatsen in de nieuwe package me.moop.mytwitter.persistance. Persistance is het Engelse woord voor volharding of voortduren en heeft in deze context te maken met het feit dat gegevens bewaard blijven na het afsluiten van je app en ook los van de toestand waarin je Activities zich bevinden.
  • Maak de package me.moop.mytwitter.persistance aan en sleep de class DatabaseHelper hierin.
Je Package Explorer Window moet er nu als volgt uitzien: . We moeten nu op een nette manier de code voor de internetverbinding uit MainActivity en TweetsActivity halen en deze plaatsen in de package me.moop.mytwitter.persistance. Als je een hele simpele app maakt (zoals de app uit DevTutorial vijf) dan kan je nog wel een AsyncTask gebruiken voor het gebruiken van een internetverbinding en deze desnoods in dezelfde Activity plaatsen. Maar als je app langzaam verder groeit moet je om eerder genoemde redenen structuur gaan aanbrengen in je code. In de volgende paragraaf gaan we dit doen door de IntentServiceclass te gebruiken.

De IntentService class

Een service kan je het beste begrijpen als een Activity die in de achtergrond draait, zonder scherm. Als je een beetje grote app hebt die sterk leunt op het gebruik van een internetverbinding, kan je de internetverbinding beter niet direct in je Activity plaatsen maar in een aparte class. Of nog beter, je stopt het in een Activity zonder scherm die je automatisch in de achtergrond opstart met een Intent object, waarbij je extra informatie in de Intent meegeeft over wat er gedownload moet worden. Wanneer die Activity in de achtergrond klaar is het met het downloaden, zorg je ervoor dat je Activity die de gegevens nodig had een bericht terugkrijgt dat de gegevens in de database zijn opgeslagen (of een bericht dat er iets fout ging bij het downloaden). Een Activity zonder scherm die je in de achtergrond kan opstarten heet een Service (hier kan je er meer over lezen). Een bijkomend voordeel van het gebruiken van een aparte Service is dat de downloadtaak wordt uitgevoerd onafhankelijk van wat er gebeurt met de Activity. De Service zorgt er zelfstandig voor dat hij gegevens wegschrijft naar de database en de Activity kan op zijn beurt ook zelfstandig gegevens uitlezen van de database. Dit principe heet loose-coupling: je maakt door alleen een beetje informatie in een Intent te stoppen een erge losse koppeling tussen verschillende classes (ik kom hier later nog op terug). We gaan de IntentService class (link) gebruiken als superclass om onze eigen Service class te maken.
  • Maak in de package me.moop.mytwitter.persistance een nieuwe class aan. Geef hem de naam SyncService en zorg dat het een subclass is van IntentService: .
  • Je krijgt deze foutmelding: . Haal deze foutmelding weg door de onderstaande constructor toe te voegen. De naam die je meegeeft aan de super constructor staat voor de naam die wordt weergeven aan de Service in je Debug Perspective (zie DevTutorial 4) als je bezig bent met debuggen.
Je ziet verder in je class dat er automatisch de method onHandleIntent is aangemaakt. Dit is de method die wordt aangeroepen zodra er een Intent naar deze service wordt gestuurd. Het handige van deze method (en van de IntentService class) is dat als er twee Intent objecten vlak achter elkaar naar deze service worden gestuurd, ze in een wachtrij worden gestopt. De Intent die het eerste was verzonden wordt als eerste uitgevoerd in de method onHandleIntent. Als de method klaar is met uitvoeren, wordt vervolgens de method opnieuw uitgevoerd met het tweede Intent object. Dit gaat net zo lang door totdat er geen Intent objecten meer in de wachtrij staan: daarna stopt deze service zichzelf automatisch. Als er na verloop van tijd weer een Intent wordt verstuurd naar deze service dan wordt deze service automatisch weer opgestart en dan wordt hetzelfde proces opnieuw uitgevoerd. Vanuit je Activities kan je hierdoor willekeurig Intents sturen naar deze service en het antwoord afwachten. De code die in de IntentService class staat zorgt voor deze handige automatisch afhandeling, dat komt omdat je deze service hebt geëxtend (zie eventueel DevTutorial 3 om even terug te lezen wat 'extend' betekent). De enige code die je in onHandleIntent moet stoppen is het uitlezen van de extra informatie die in de Intent staat en vervolgens de juiste download starten. Ook moet je de code schrijven die een bericht teruggeeft aan de Activity die de Intent had verstuurd. Terwijl je hiermee bezig bent, ben je alleen meer code aan het schrijven die zich richt op het uitvoeren van één Intent. Het controleren en aansturen van de verschillende Intents naar de method onHandleIntent wordt al automatisch gedaan door de code die in de IntentService class zit. Handig hè? In je app kan je twee soorten downloads opstarten: het downloaden van een Twitter-gebruiker en het downloaden van Tweets. In de Intent die we vanuit een Activity naar deze service sturen moet je deze keuze als extra informatie meesturen. Dit kunnen we eenvoudig doen door een int (een afgerond getal) mee te geven. Dit stukje informatie moeten we een naam geven en vervolgens moeten we een waarde invoeren voor deze informatie. Als naam kan je gewoon een woordje gebruiken, maar in dit geval kan je beter werken met een String die je public, final en static maakt. public betekent zoals je weet dat je de variabele ook kan aanroepen vanuit andere classes, en static betekent dat de waarde hetzelfde blijft, onafhankelijk van welk object van deze class hem gebruikt. final betekent dat de waarde van deze variabele niet aangepast mag worden. Door altijd als naam te verwijzen naar deze variabelen weet je zeker dat je altijd dezelfde naam gebruikt. En je krijgt een error in Eclipse als je de naam van de variabele verkeerd spelt.
  • voeg onderstaande variabele toe bovenin in je class block:
Als je straks in je Activity aangeeft wat voor downloadactie er wordt meegeven kan je als naam van de extra informatie verwijzen naar deze variabele. Voor de inhoud van de informatie kunnen we een int gebruiken die ook public, static en final is.
  • Voeg ook deze code toe bovenin je class block:
Je wil ook de Twitter-gebruikersnaam mee kunnen geven in de Intent.
  • Voeg deze code ook toe:
Nu kan je in de method onHandleIntent de benodigde informatie uitlezen.
  • Voeg deze code toe in onHandleIntent:
(Het getal 0 wat als extra parameter wordt meegegeven aan getIntExtra is de waarde die teruggegeven moet worden als er geen int gevonden was die onder die naam was opgeslagen). Ho, wacht even, ik heb je ook meteen de method download aangesmeerd! Terwijl die nog niet eens bestaat... Hetzelfde geldt voor de int met de naam result. Met de informatie die net is uitgelezen wil ik een method opstarten met de naam download die een int teruggeeft als resultaat. Deze int geven we daarna terug aan de Activity die deze Intent had verstuurd (dat teruggeven gaan we later doen). Wat je al wel kan zien aan de extra toegevoegde regel is dat er een method met de naam download moet bestaan die een String en een int als parameter accepteert en een int teruggeeft als resultaat. Eclipse heeft dat gelukkig ook door en als je met je muis boven deze regel blijft hangen kan je hem automatisch aan laten maken: . Wanneer je daar op klikt kan je nog met tab door alle vierkantjes heenspringen om de namen van variabelen aan te passen. Je kan bijvoorbeeld hier op uitkomen:Straks gaan we de download method implementeren (de inhoud van de method vullen). We blijven ons even focussen op de communicatie tussen de Activity die deze IntentService aanriep en diezelfde IntentService die nog een antwoord terug moet geven. Wanneer de download method straks is geïmplementeerd, is de int result gevuld met een getal dat aangeeft of de download succesvol was of niet. Het enige wat er moet gebeuren is dit getal teruggeven aan de vragende Activity. De communicatie náár de IntentService wordt geregeld via een Intent, de communicatie naar de Activity terug wordt geregeld via een ResultReceiver object. Dit wordt besproken in de volgende paragraaf.

SyncResultReceiver en SyncCallbackReceiver

De ResultReceiver is de class die een verbinding kan houden naar de Activity die een Intent verstuurd heeft. In dit geval gaat de ResultReceiver class een verbinding houden naar óf MainActivity óf TweetsActivity als een van deze Activities een Intent gestuurd heeft naar de SyncService class. Zoals je weet kan je extra informatie meegeven in een Intent voordat je deze gaat versturen naar een Activity of een Service. Dit kan je doen met de method putExtra waar je bijvoorbeeld een String kan meegeven of een int. Maar je kan er niet elk willekeurig object aan meegeven zoals bijvoorbeeld een TwitterUser object. Dat is ook de reden waarom in de method showTweets van de MainActivity class een String wordt meegegeven in plaats van het TwitterUser object. Wanneer we een Intent willen sturen aan onze service met de naam SyncService willen we wél op een of andere manier informatie meegeven. Dit zodat in SyncService van de Intent bepaald kan worden aan welke Activity het resultaatbericht teruggestuurd kan worden. Omdat we geen Activity mee kunnen geven als extra informatie in de Intent moeten we een andere manier zoeken. Gelukkig kan het ResultReceiver object wél als extra informatie meegegeven worden in een Intent. En als je nou een subclass maakt van de ResultReceiver class kan je hem aanpassen zodat hij kan onthouden naar wie het resultaat van de SyncService teruggestuurd moet worden.
  • Maak een nieuwe class aan die een subclass is van android.os.ResultReceiver. Geef hem de naam SyncResultReceiver en plaats hem in de package me.moop.mytwitter.persistance.
Je krijgt een foutmelding omdat er nog een constructor nodig is.
  • Voeg de benodige constructor toe en pas hem aan zodat hij geen parameters heeft.
  • Zorg er zelf voor dat de constructor van deze superclass wordt aangeroepen en geef als parameter een nieuw Handler object mee dat je zelf ter plekke aanmaakt met het woordje new:
Nu moet je ervoor zorgen dat deze class kan onthouden naar wie het resultaat bericht gestuurd gaat worden dat je krijgt van je SyncService class. In plaats van een compleet eigen gemaakte Activity object te gaan onthouden gebruiken we hiervoor een interface die we zelf gaan maken. Deze geven we de naam SyncCallbackReceiver en daarin schrijven we dat daarin op zijn minst een method onSyncCallback bestaat. Deze method moet als parameters een int en een Bundle-object kunnen ontvangen.
  • Maak een nieuwe interface aan (New --> Interface), geef hem de naam SyncCallbackReceiver en plaats hem in de package me.moop.mytwitter.persistance:
Zodra we nu een SyncCallbackReceiver object hebben, betekent dat dat die class deze interface implementeert en dus sowieso de method onSyncCallback heeft. Zorg er nu voor in de SyncResultReceiver class dat je een object kan onthouden dat de interface SyncCallbackReceiver implementeert. Je hebt hiervoor een variabele nodig en een set-method.
  • Voeg deze code toe op de juiste plek in je class SyncResultReceiver:
Het enige dat je nu weet als je de variabele mSyncCallBackReceiver gebruikt in je code is dat je er de method onSyncCallback op kan aanroepen. Wanneer de SyncResultReceiver class een bericht terugkrijgt van in dit geval onze SyncService class, wordt de method onReceiveResult aangeroepen met een int en een Bundle parameter. Dat kan je ook zien op deze pagina helemaal onderaan, de beschrijving van de ResultReceiver class. We gaan deze method overschrijven: we gaan er een nieuwe method in zetten. In deze method willen we dat de parameters worden doorgegeven aan onze variabele mSyncCallBackReceiver. Als controle kijken we eerst nog even of de variabele mSyncCallBackReceiver gevuld is.
  • Probeer eerst zelf deze method te overschrijven: typ de complete beschrijving van de method over zoals je hem op de link ziet die je net kreeg. Schrijf binnen in die method de code waar ik het net over had en voeg boven de method de @Override annotatie toe.
Je komt ongeveer op het volgende uit:Wanneer een bericht vanuit de SyncService class wordt gestuurd aan onze SyncResultReceiver class wordt deze meteen doorgespeeld naar de method onSyncCallback. We gaan in deze DevTutorial alleen maar de int paramater gebruiken. We gaan niet zomaar met bepaalde getallen werken, we willen deze getallen een naam geven zodat de code beter leesbaar is.
  • Voeg de volgende public static final ints toe:
Zoals je ziet zijn dit de resultaatcodes die je onder andere terug kan krijgen van een internetverbinding. In de SyncService class moet nu de ResultReceiver class uit de Intent gehaald worden en gebruikt worden om het resultaat van het downloaden naar door te sturen. Als je nog eens naar de ResultReceiver class kijkt (hier), dan zie je dat hij de interface Parceable implementeert. Als je leest wat het doel is van die interface (hier), dan kan je lezen dat een class die deze interface implementeert, methods heeft om zichzelf om te kunnen zetten naar een Parcel object én methods heeft om zichzelf in te lezen vanaf een Parcel object. Als je dan verder leest wat een Parcel object is, dan kan je lezen dat een Parcel object een object is dat tussen verschillende Android-processen heen en weer gestuurd kan worden. Dat is wat je nodig hebt als je via Intents informatie wilt sturen naar andere Activities en Services. Zometeen gaan we een Intent aanmaken waarin we als extra informatie onder andere een SyncResultReceiver object meesturen. Voor nu gaan we in de method onHandleIntent van de SyncService class het resultaat van de method download als bericht naar de SyncResultReceiver class sturen (die hem weer doorstuurt naar een class die de SyncCallbackReceiver interface implementeert). Je kan een object dat de Parceable interface implementeert opvragen vanuit een Intent object met de method getParceableExtra, waarbij je als extra bericht een naam meegeeft waaronder je het Parcaeble object hebt opgeslagen in de Intent. Voor het opslaan onder een naam gebruiken we weer een public static final String.
  • Voeg deze public static final String toe op de juiste plek in je SyncService class:
  • Gebruik nu deze public static final String om in de method onHandleIntent het SyncResultReceiver object op te vragen:
Nu kunnen we de method send gebruiken op ons SyncResultReceiver om het resultaat van de download method terug te sturen. Hiervoor gebruiken we de int result. De send method heeft als parameter ook een Bundle object nodig. Als je wil kan je zelf een Bundle object aanmaken en nog meer informatie teruggegeven, maar dat hebben we nu niet nodig.
  • Zorg ervoor dat aan het einde van de method onHandleIntent het resultaat van de method download wordt doorgestuurd naar je SyncResultReceiver object:
Je vraagt je misschien af waarom we niet de complete inhoud van de JSON als String in een bundle meegeven. Dit doen we expres niet omdat we namelijk willen dat de SyncService enkel verantwoordelijk moet blijven voor het wegschrijven van zijn eigen werk naar de database. Hierdoor hoeven de Activities zich alleen maar druk te maken over hoe ze hun gegevens opvragen van de database. De SyncService zorgt voor het downloaden en verwerken van de gegevens, de Activities zorgen alleen maar voor het opvragen van de gegevens en het tonen ervan. Dit scheiden van verantwoordelijkheden heeft te maken met de eerdere argumenten die ik gaf voor het structuur aanbrengen in je app. En waarom het juist nu verdeeld is tussen Activity classes en Service classes? De manier hoe de IntentService class werkt maakt het erg geschikt om als losstaande class downloadtaken uit te voeren. De Activity class is juist bedoeld voor het werken met het scherm en de gebruiker die daarbij hoort, niet voor het uitvoeren van taken die veel tijd kosten.

De download-methods herstructeren naar SyncService

De basis van onze classes SyncService en SyncResultReceiver én onze interface SyncCallbackReceiver gaan we nu gebruiken om methods te verplaatsen naar de juiste plek. We willen dat de Activities alleen maar verantwoordelijk zijn voor het tonen van gegevens én het doorgeven van download-aanvragen naar onze SyncService class. De SyncService moet alleen verantwoordelijk zijn voor het downloaden en opslaan van de gegevens. Als je nu naar MainActivity.java en TweetsActivity.java kijkt, dan zie je dat onder andere de volgende methods te maken hebben met het downloaden en opslaan van gegevens:
  • createHttpClient
  • doInBackground (van de inner classes die een subclass zijn van AsyncTask)
  • saveToDbAndShow
  • onDestroy
  • getDatabaseHelper
Kopieer van beide classes deze methods naar je class SyncService. Nu gaan we de methods aanpassen zodat we dubbele code weghalen inclusief code die in twee methods erg veel op elkaar lijkt.
  • Je kan er sowieso voor zorgen dat er van de volgende methods nog maar één is: methods onDestroy, getDatabaseHelper en createHttpClient (deze zijn namelijk identiek).
  • In de class SyncService is de method onDestroy niet protected maar public (kijk maar hier), dit moet je ook aanpassen in je method onDestroy die je net gekopieerd hebt.
  • Ook kan je de variabele mDatabaseHelper alvast toevoegen, deze heb je sowieso nodig.
Je hebt nu nog 5 methods die aangepast moeten worden:
  • doInBackground die je hebt gekopieerd van MainActivity
  • saveToDbAndShow die je hebt gekopieerd van MainActivity
  • doInBackground die je hebt gekopieerd van TweetsActivity
  • saveToDbAndShow die je hebt gekopieerd van TweetsActivity
  • download die je al had aangemaakt
Het uiteindelijke resultaat weet je al: vanaf de method onHandleIntent wordt de method download aangeroepen. Deze method geeft als resultaat een intterug en deze wordt teruggestuurd naar een Activity zodat die weet of de download succesvol was of niet. Bijna alle code die je nodig hebt staat in de vijf methods die je nog moet samenvoegen.
  • Probeer te kijken of je zelf deze methods kan samenvoegen en kijk daarna naar het voorbeeld hieronder.
  • Je kan de code van doInBackground van beide methods doorkopiëren naar de method download en met een if block kijken wat de fetchUrl wordt waar je vandaan moet downloaden. Omdat de code van doInBackground aanwezig is in de method download, kan je nu de beide methods doInBackground verwijderen.
  • De methods saveToDbAndShow kan je hernoemen naar saveTwitterUser en save Tweets.
  • Probeer de methods op elkaar af te stemmen zodat er geen errors meer zijn, zonder dat je belangrijke regels code weghaalt.
Je zou op de volgende code kunnen uitkomen: link. De volgende punten zouden je kunnen opvallen:
  • De constructor van de TwitterUser class is aangepast zodat deze een JSON Object accepteert in plaats van een String die in de constructor wordt omgezet naar een JSON Object. Als gevolg daarvan hebben de constructors van de TwitterUser class en de Tweet class dezelfde structuur. Het begin van de constructor van de TwitterUser class ziet er nu als volgt uit (de rest zou je nu zelf moeten kunnen begrijpen, of kijk anders naar de source code van deze DevTutorial):
  • Ik gebruik zoveel mogelijk de public final static ints die gedefinieerd zijn in de SyncResultReceiver class. Door deze namen te gebruiken kan je makkelijker lezen wat er gebeurt in de code.
  • De methods saveTweets en saveTwitterUser hebben geen try-catch block, maar hebben in hun method definitie (de eerst regel voorafgaan aan het code block van de method) een throws beschrijving. Dit betekent dat als er een exceptie voorkomt hij wordt doorgegeven aan de method die die method aanriep, in dit geval is dat de method download. Hierdoor worden alle excepties in de download method opgevangen. Dit maakt de code overzichtelijker (als je dit trucje later zelf ook gebruikt in je andere apps: je mag een exceptie niet te vaak doorgeven, daar wordt je code juist onoverzichtelijk van).
  • Ik gebruik een switch block in de download method om de variabele fetchUrl in te vullen en verderop ook om te bepalen welke method aangeroepen moet worden om het resultaat op te slaan. Dit is een soort if block, maar werkt alleen maar met ints en nog een paar andere primitieve types (dat zijn variabelen die geen objecten zijn). Je kan hiervoor ook een if-else block maken, alleen vind ik in dit geval het gebruiken van een switch block overzichtelijker (dit is puur persoonlijk).
Wanneer je een variabele final en static maakt, dan wordt hij ook wel een constante (of Engels: constant) genoemd.

Weergave-methods herstructureren in je Activities

Voordat we de puntjes op de i gaan zetten in deze DevTutorial, moet er nog een bug gefixt worden: in je classes MainActivity en TweetsActivity en TwitterUser gebruik je de method SystemClock.elapsedRealTime om controles uit te voeren die te maken hebben met tijd. Deze method is eigenlijk niet de juiste, het getal dat je terugkrijgt uit deze method heeft te maken met hoe lang het geleden was dat het Androidapparaat opstartte. Dus als je je emulator (of je Androidapparaat) opnieuw opstart kloppen de waarden niet meer die horen bij je TwitterUser object. In plaats van SystemClock.elapsedRealTime kan je beter System.currentTimeMillisgebruiken. Dit getal is een berekening op basis van de datum en tijd die is ingesteld op je AndroidToestel of AndroidEmulator.
  • Vervang op 4 plekken SystemClock.elapsedRealTime door System.currentTimeMillis: in MainActivity.java, TweetsActivity.java, TwitterUser.java en SyncService.java.
Nu gaan we de puntjes op de i zetten van onze Twitter-app. Het afmaken van MainActivity.java doen we samen, daarna moet je TweetsActivity.java zelf doen. De volgende code hebben we niet meer nodig, deze kan je verwijderen uit je MainActivity class:
  • De inner class DownloadUserInfoTask inclusief de methods
  • De method saveToDbAndShow
  • In de method downloadUserInfo wordt de variabele van het Type String met de naam username gebruikt. Verplaats het definiëren van deze variabele naar je class block en verander de naam naar mUsername. Je kan de String parameter van de method downloadOrShowFromDb ook weghalen en de rest van method downloadOrShowFromDb daarop aanpassen.
Er staat nu nog één foutmelding bij de method downloadOrShowFromDb, namelijk dat er geen class DownloadUserInfoTaskmeer bestaat.
  • Haal deze regel weg en vervang hem voor het aanroepen van de method callSyncService.
  • Voeg de method callSyncService toe en zet er het woordje private voor. Als return type moet je void (Engels voor "leegte") meegeven: hij geeft dus geen waarde terug, dat regelen we in een andere method (namelijk onSyncCallback die we gaan implementeren vanuit de interface SyncCallbackReceiver).
In de method callSyncService willen we een Intent aanmaken en deze gebruiken om de SyncService aan te roepen via de method startService. Eerst moet er een SyncResultReceiver object aangemaakt worden, voeg dit toe in de method callSyncService:Nu gaan we de method setReceiver gebruiken op het SyncResultReceiver object zodat een bericht ontvangen kan worden over hoe de download is verlopen. We willen deze terugkoppeling ontvangen in MainActivity, dus daarom moet setSyncCallbackReceiver worden aangeroepen met als parameter de class zelf (dat doe je met het woordje this).
  • Voeg deze code toe:
Nu zie je een foutmelding staan: . Op zich ook wel logisch, want je probeert de method setSyncCallbackReceiver aan te roepen terwijl je dat moet doen met een class die SyncCallbackReceiver implementeert. Pas dan weet je zeker dat je er de method onSyncCallback op kan aanroepen vanuit de method onReceiveResult van de SyncResultReceiver class. Als oplossing stelt Eclipse onder andere voor om MainActivity de interface SyncCallbackReceiver te laten implementeren.
  • Kies voor deze oplossing (Let 'MainActivity' implement 'SyncCallbackReceiver').
Als je nu bovenaan je class kijkt, zie je achter je class definitie staan "implements SyncCallbackReceiver". Wat betekent dat je de interface SyncCallbackReceiver implementeert en dus ook de method onSyncCallback in je class hebt. Tenminste, je zegt dat je dat doet terwijl je die method nog niet hebt in je class. Ook Eclipse heeft door dat dat niet klopt: . Je moet deze method nog aanmaken. Dat kan je doen door door te klikken op "Add unimplemented methods". En nú heb je wel deze method in je class. Die method gaan we zo verder vullen, eerst maken we callSyncService verder af. In de method callSyncService gaan we nu de Intent aanmaken en vullen met de juiste informatie. Intent is het Engelse woord voor "intentie" wat hier betekent dat je iets van plan bent en dat via het Intent object wil aanvragen. Omdat je de IntentService aanroept moet je een speciale Intent aanmaken, waarbij je aangeeft wat voor actie je wil aanvragen met de Intent. Dat doe je met onderstaande code:De eerste parameter die je meegeeft, gebruik je om aan te geven dat je een data-synchronisatie actie wil uitvoeren. Als tweede parameter zou je nog een adres kunnen meegeven, maar dat zoekt onze SyncService zelf uit, dus geef je een leeg object (null) mee. De derde parameter betekent waarvandaan je de Intent wil aanvragen, dat is de class waar je nu in zit (this). De derde is de class beschrijving van de Service die je wil aanvragen.
  • Voeg bovenstaande code toe aan je class.
Nu hoef je alleen nog maar de juiste informatie toe te voegen aan de Intent en hem vervolgens opstarten via de method startService die je aanroept op de Activity class:Je geeft zoals je ziet drie stukken informatie mee. Het eerste stuk informatie dat je mee geeft, geef je de naam die staat in de constante RECEIVER die staat in de class SyncService, dat is dus "resultreceiver". De inhoud van die informatie is de SyncResultReceiver die je net hebt aangemaakt. Voor de andere twee stukken informatie gebruik je op dezelfde manier ook als naam constanten die staan in de class SyncService. De inhoud ervan snap je waarschijnlijk nu zelf wel. Op het laatst wordt de Intent verstuurd naar de SyncService class, waar die Intent in een wachtrij komt die wordt gericht naar de method onHandelIntent. Wat er verder gebeurt zal je nu moeten weten. ;) Aan het einde van de method onHandleIntent wordt de method send aangeroepen, waarbij als extra bericht de resultaatcode van de internetverbinding wordt teruggestuurd, of het getal dat staat in de constante RESULT_ERROR in de SyncResultReceiver class. Dit getal krijg je terug in de method onSyncCallback in je class MainActivity.
  • Voeg bovenstaande code toe in de method callSyncService
Om je MainActivity class af te maken hoef je alleen nog maar de method onSyncCallback te implementeren. Maak deze method af en zorg dat er de volgende dingen gebeuren:
  • Het verbergen van de ProgressDialog (gebruik hiervoor de dismiss method).
  • Kijken welk getal er in de int result zit en op basis daarvan opnieuw de method downloadOrShowFromDb aanroepen of de juiste Toast laten zien.
Je kan op de volgende code uitkomen:Het enige dat je nog moet doen is Android laten weten dat je een Service hebt die aangeroepen kan worden vanuit je Activities. Dat doe je Door AndroidManifest.xml te openen in de Application Editor Tab, vervolgens bij Application Nodes te klikken op Add en dan te kiezen voor een Service. Vervolgens moet je als naam bij de Service .persistance.SyncService invullen: (vergeet die punt . vooraan niet!). Daarna kan je de wijziging opslaan en de app opstarten.

Extra oefening

Om voor jezelf te bewijzen dat je inderdaad wat geleerd hebt in deze tien DevTutorials kan je proberen om de TweetsActivity ook downloads te laten aanvragen en verwerken via de SyncService class. De basics voor een Androidapp maken heb je nu echt wel onder de knie en je kan door alle code tot nu toe slim te combineren al best veel verschillende apps maken. Her en der moet je dan nog soms wel iets opzoeken, maar met enige inspanning heb je een goede kans het te begrijpen. Blijf er alsjeblieft wel op letten dat je uiteindelijk goed nadenkt over welke methods je wat voor soort werk laat doen, en in welke classes je dat doet. ;) De tutorials die je op het internet tegenkomt, gooien vaak namelijk alles zomaar bij elkaar. Je zou verder nog een aparte method kunnen maken in de SyncService class die alle afbeeldingen downloadt die bij de tweets horen. Daarvoor moet je op het internet opzoeken hoe je een afbeelding downloadt, je moet het adres van de afbeelding opzoeken in de JSON, je moet een ImageView toevoegen aan tweet_listitem.xml en je moet het afbeeldingsbestand via de TweetsListAdapter inladen in de ImageView.

Source code en forum

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.

Voorlopig de laatste DevTutorial

Met voldoening heb ik deze DevTutorials geschreven. Ik heb geprobeerd jullie verschillende hoeken te laten zien van Android. Hopelijk hebben jullie dat idee ook, want erg veel fundamentele andere onderwerpen zijn er eigenlijk niet meer, voor wat je moet weten als beginnende Androidprogrammeur. Je zou je nog wel kunnen verdiepen in animaties, GridView, ninepatch afbeeldingen, downloaden van bestanden en Android xml-taalbestanden. Een iets geavanceerder onderwerp is het versturen van gegevens naar een server op het internet, dan krijg je te maken met HTTP POST, HTTP PUT en HTTP DELETE. Daarmee zou je bijvoorbeeld een tweet kunnen versturen als je dat betrekt op de MyTwitter app. Er zijn nog wel ideëen om wat geavanceerdere DevTutorials te schrijven, maar ik neem eerst even rust wat de DevTutorials betreft. Oh trouwens, als je vijf of meer van deze DevTutorials hebt gevolgd (nu of in de toekomst), zou je dan deze enquête even willen invullen? Ik zit er over te denken om de DevTutorials uit te brengen in een boek en hoor graag of jullie tips hiervoor hebben aan mij. Ook als je deze DevTutorial bijvoorbeeld ergens in september 2012 leest ben ik ook nog erg benieuwd naar je mening! Ik wil in het bijzonder Elmer bedanken voor alle spellingscontrole die hij gedaan heeft voor de DevTutorials. De spellingscontrole was best wat werk, zeker als ik mijn werk niet altijd op tijd had ingeleverd... En jullie, lezers, natuurlijk ook bedankt voor het lezen van mijn werk. Ik hoop dat jullie het leuk vonden om te doen!

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

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