• Programmazione Android
  • CORSI ONLINE
  • Web Agency

Logo

Corsi di programmazione web e mobile online
Navigation
  • Home
  • CORSI ONLINE
  • Tutorial Pratici
  • GUIDE COMPLETE
    • Corso completo di C
    • Corso videogame con Cocos2d
    • Programmazione Cocoa Touch
  • Sezioni
    • Libri e manuali
    • Tips & Tricks
    • Risorse utili
    • Strumenti di Sviluppo
    • Materiale OpenSource
    • Framework
    • Guide Teoriche
    • Guide varie
    • Grafica e Design
    • iPad
    • News
    • Video Tutorial
    • Windows Phone
  • Pubblicità
  • About
    • Chi siamo
    • Pubblicazioni
    • Collabora
    • Sostieni devAPP

T#109 – Key-Value Observing: Costruiamo un semplice lettore multimediale

By Costantino Pistagna | on 22 Maggio 2012 | 8 Comments
Senza categoria

Se si potesse stilare una classifica dei meccanismi meno conosciuti di Cocoa, il Key-Value Observing pattern o KVO sicuramente ricoprirebbe uno dei primi posti. KVO è un meccanismo molto utile che permette agli oggetti di essere notificati rispetto a cambiamenti di specifiche proprietà di altri oggetti. Come è facile immaginare, questo genere di meccanismi si prestano molto bene al pattern di programmazione Model-View-Controller. In quest’articolo cercheremo di capire meglio come usare questa tecnica affiancando la teoria ad un esempio pratico che ne dimostri appieno le funzionalità.

Cosa vogliamo realizzare.

Visto che una delle cose che mi viene chiesta più spesso è la costruzione di un lettore MP3 personalizzato, per l’esempio di quest’articolo ho pensato di sfruttare KVO per costruire un semplice e snello lettore di stream audio per web radio.
In letteratura e sul web esistono tanti esempi di lettori streaming per iOS. Il più famoso di essi è sicuramente quello descritto da Matt Gallagher nel suo articolo “Streaming and
playing an MP3 stream
” [http://cocoawithlove.com/2008/09/streaming-and-playinglive-
mp3-stream.html
]. Il metodo descritto da Matt nel suo articolo si basa su un complesso meccanismo di download e caching gestito da CFSocket. Sebbene il risultato ottenuto sia egregio, l’inclusione o l’adattamento del progetto a particolari casi d’uso può risultare molto laborioso per via della natura stessa dell’approccio. Nel nostro articolo, invece, sfrutteremo un oggetto nativo di Cocoa: AVPlayer. Il risultato sarà una classe facilmente trasportabile che potremo includere in qualunque progetto, sia esso destinato al mondo iOS o al mondo Mac. Per trarre il massimo profitto da quest’approccio, faremo uso del meccanismo KVO unitamente alla costruzione di un protocollo adhoc.

Prima di andare avanti, proviamo a semplificare con una grafica quello che vogliamo ottenere.

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-01

Fig. 1 – Diagramma di massima del nostro oggetto

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-02

Fig.2 – Spaccato del Model e sue interazioni con il mondo esterno

Il nostro oggetto, CPStreamPlayer, dovrà essere in grado di notificare i cambiamento di stato (titolo del brano in riproduzione, buffer in esaurimento, buffer riempito, etc.) all’interfaccia grafica o a qualunque altro oggetto ne faccia richiesta. Per ottenere questo scopo utilizzeremo un pattern di programmazione molto interessante esposto dal framework Cocoa, denominato Key-Value Coding.

KVC è un meccanismo che permette di impostare o leggere un valore di variabile usando il nome stesso. Il nome è una semplice stringa e ci riferiremo ad essa con il termine chiave o key. Ogni oggetto che eredita da NSObject può fare accesso alle sue proprietà attraverso coppie chiave-valore.

Cosa significa?

Immaginate di avere un oggetto che espone una proprietà denominata ‘title’. Per poter configurare a dovere questa proprietà è possibile usare il tradizionale costrutto:

[myObject setTitle: @"qualcosa"];

oppure, seguendo la notazione dotted introdotta da Objective-C 2.0:

myObject.title = @"qualcosa";

esiste un’altro metodo che sfrutta il concetto di Key-Value Coding:

[myObject setValue:@"qualcosa" forKey:@"title"];

Allo stesso modo, se vorremo recuperare il valore della proprietà title potremo usare i costrutti noti di Objective-C, oppure fare ricorso al meccanismo KVC:

NSString *myTitle = [myObject valueForKey:@"title"];

A questo punto dell’articolo potrebbe venire naturale una domanda: perché stiamo introducendo un nuovo modo per impostare e recuperare il valore delle proprietà? Non bastano i metodi già noti?

Per poter rispondere a questa domanda è necessario introdurre un altro concetto che snoccioleremo nel corso dell’articolo: Il meccanismo di Key-Value Observing o KVO. Ogni oggetto che configura le sue proprietà attraverso il meccanismo sopra esposto di KVC, può fare uso del meccanismo di KVO. Questo meccanismo permette di osservare gli eventuali cambiamenti delle proprietà di un oggetto da un’altro oggetto, nell’ipotesi di alterare i valori delle proprietà attraverso il meccanismo di KVC.

NSNotificationCenter e Key-Value Observing

Alcuni di voi avranno già avuto modo di dialogare ed interagire con le notifiche. Questa soluzione coinvolge il centro di notifica o NSNotificationCenter. In questo approccio, il nostro oggetto CPStreamPlayer invierà dei messaggi al centro di notifica che, a sua volta, girerà queste informazioni a tutti gli oggetti che si sono registrati per riceverle.

Ritornando al nostro esempio, sfruttando questo approccio, le entità coinvolte saranno tre: NSNotificationCenter, CPStreamPlayer e gli eventuali Observer.

Nell’approccio con KVO, invece, quello che faremo sarà aggiungere un observer sull’oggetto player, in
maniera da essere notificati direttamente in base a quello che succede dietro le quinte dell’oggetto CPStreamPlayer. In quest’ultimo caso, le entità coinvolte saranno due: CPStreamPlayer ed Observer.

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-03

Fig.3 – Pattern di notifica basato su NSNotificationCenter.

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-04

Fig.4- Pattern di notifica basato su Key-Value Observing.

Sebbene entrambi i pattern di programmazione sembrano essere simili, il loro funzionamento a basso livello è molto diverso. Come regola generale di programmazione il pattern con Notification Center dovrebbe essere impiegato solo quando sussiste l’esigenza di notificare uno stesso messaggio a più osservatori assolutamente slegati tra loro. Questi messaggi dovrebbero essere di tipo allarme e tutto l’overhead di trasferire informazioni aggiuntive sarà a carico nostro, attraverso il concetto di NSNotification e dizionario di userInfo (vedremo questo pattern di programmazione in dettaglio in un altro articolo).

Inoltre, utilizzando un’entità esterna (NSNotificationCenter) le comunicazioni tra gli oggetti potrebbero essere ritardate, causando dei piccoli glitch sulla nostra user interface. La comunicazione KVO, invece, non coinvolge altri attori ed è diretta tra i due oggetti che la instaurano. Diremo che due oggetti che fanno KVO tra loro sono legati o “bindati”.

Un ultima considerazione relativamente all’oggetto che cercheremo di costruire nel nostro articolo. Per rendere il nostro esempio il più portabile possibile, costruiremo un oggetto wrapper a più alto livello, che si preoccuperà per noi di dialogare con AVPlayer.

Nella grafica sotto è mostrata una semplificazione di quello che sarà la classe CPStreamPlayer insieme al meccanismo di Key-Value Observing:

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-05

Fig.5 – Semplificazione classe CPStreamPlayer e suoi oggetti interni.

Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-06

Fig. 6 – KVO tra la classe CPStreamPlayer e le sue istanze interne.

KVO in pratica

Ci sono tre passi da effettuare per configurare correttamente un osservatore.

  1. Prima di tutto, bisogna accertarsi di essere davanti ad uno scenario in cui l’approccio KVO risulti utile. Ad esempio, un oggetto che ha bisogno di essere notificato quando viene effettuata una modifica ad una proprietà di un altro oggetto. Nel nostro caso, l’oggetto CPStreamPlayer dovrà essere a conoscenza dei cambiamenti che avvengono sullo stream attualmente in corso gestito dagli oggetti AVPlayer ed AVPlayerItem.
  2. L’oggetto CPStreamPlayer deve registrarsi come osservatore sugli oggetti che intende monitorare, inviando un messaggio addObserver:forKeyPath:options:context:
    Il messaggio addObserver:forKeyPath:options:context: stabilisce una interconnessione tra gli oggetti che specifichiamo. La connessione non viene specificata tra le due classi, bensì tra le istanza degli oggetti a runtime. Tornando al nostro esempio, l’oggetto wrapper CPStreamPlayer sarà osservatore dei suoi oggetti privati in maniera da poterne monitorare lo stato.
  3. Per rispondere alle notifiche di cambiamento, l’osservatore deve implementare il metodo observeValueForKeyPath:ofObject:change:context:
    Questo metodo definisce come l’osservatore risponde alle notifiche di cambiamento. In pratica, è il metodo su cui dobbiamo lavorare per reagire ai cambiamenti che arriveranno.
    Il metodo observeValueForKeyPath:ofObject:change:context: viene invocato automaticamente quando una proprietà risulta modificata seguendo lo standard KVO.

Il primo beneficio di quest’approccio consiste sicuramente nel non (re)implementare un nostro personale schema per l’invio di notifiche ogni qualvolta una proprietà dei nostri oggetti verrà cambiata. Tipicamente non ci sono altre azioni oltre a quelle elencate nei quattro passi sopra per adottare il meccanismo di notifica basato su KVO. A differenza del meccanismo di notifica centrale attraverso NSNotificationCenter, inoltre, le notifiche vengono inviate direttamente agli osservatori registrati. NSObject fornisce un’implementazione base per questo meccanismo e, raramente, si avrà l’esigenza di
modicarne l’implementazione.

AVPlayer ed AVItem – Come funzionano gli stream MP3

Dalla versione 4.0, iOS mette a disposizione un oggetto utilissimo per la fruizione di contenuti multimediali, compresi quelli che arrivano da un flusso MP3 trasmesso via http.
L’oggetto in questione si chiama AVPlayer. Esso funziona equamente bene sia con oggetti
locali che con oggetti remoti e/o in stream. E’ possibile anche creare una playlist con oggetti multipli ed avere controllo sui singoli elementi.

L’organizzazione degli oggetti relativamente ad uno stream musicale può essere semplificata nel seguente diagramma:


Key-Value-Observing-kvo-costruire-un-lettore-mp3-ios-07

La prima cosa da fare per poter sfruttare l’oggetto AVPlayer è allocarne un’istanza generica in memoria:

player = [[AVPlayer alloc] init];

Il prossimo passo è quello di creare un’istanza di AVPlayerItem, ovvero un’istanza che conosca cosa vogliamo suonare (file locale o stream remoto). Nel nostro caso, passeremo ad AVPlayerItem un oggetto NSURL, contenente la stringa dell’endpoint http remoto da suonare.

NSURL *streamUrl = [NSURL URLWithString:streamAddress];
AVPlayerItem *anItem = [AVPlayerItem playerItemWithURL:streamUrl];

Una volta ottenuta un’istanza valida di AVPlayerItem possiamo rimpiazzare il currentItem della nostra istanza di AVPlayer, come segue:

[player replaceCurrentItemWithPlayerItem:anItem];

Infine, lanciamo un messaggio di play all’istanza di AVPlayer, per iniziare la riproduzione:

[player play];

E’ necessario spendere qualche altra parola relativamente alle modalità di streaming MP3.
I server di stream MP3, tipicamente, seguono una falsa riga incentrata su un sistema di unicast per ogni singolo client connesso. In pratica, ogni cliente che richiede lo stream avrà riservato un suo spazio per lo streaming ma non potrà comunicare con il server.

Per poter costruire un lettore da zero, sarebbe necessario scendere in dettaglio, relativamente alla codifica dei dati ricevuti e come vengono generati i blocchi (chunks) di informazione. Questa è una della parti più corpose dell’articolo di Matt Gallagher a cui mi riferivo sopra. Visto che noi useremo un oggetto a più alto livello, disponibile nel framework Cocoa/CocoaTouch, non avremo bisogno di scendere a questo livello di dettaglio. Ci basterà conoscere l’indirizzo http dello stream che vogliamo usare.

Risolto questo problema, se ne presenta un’altro non di meno conto. Uno stream di dati ha alcune difficoltà concettuali da risolvere. I dati, infatti, vengono inseriti in un buffer tampone che serve per evitare un’interruzione dell’ascolto in caso di un’improvviso calo di banda nel nostro dispositivo. Questo buffer viene aggiustato dinamicamente in funzione delle condizioni di banda ed il lettore deve adattarsi a queste condizioni dinamiche.

Un altro aspetto che merita attenzione riguarda l’aggiornamento del brano attualmente in ascolto.
Gli attuali sistemi di stream permettono l’invio di un pacchetto ad inizio brano, unitamente ai dati dell’audio, contenente le informazioni relative al brano attualmente in riproduzione.

L’idea è quella di aggiornare la nostra vista soltanto quando se ne verifica una reale esigenza (ad esempio, quando cambia il brano del nostro stream o quando stiamo effettuando il buffering dello stream). E’ in questo contesto che trova spazio quanto esposto relativamente a KVO e KVC. Senza un meccanismo di questo tipo, infatti, l’aggiornamento del brano attualmente riprodotto diventerebbe una sfida senza soluzioni. Se aggiornassimo il titolo ad intervalli regolari, ci troveremmo quasi sempre in una condizione di inconsistenza tra il brano riprodotto e quello visualizzato, stessa cosa per quanto riguarda i messaggi di sistema relativi al buffering. Ancora, utilizzando un paradigma basato sul polling ad intervalli regolari, tutta la user interface ne risentirebbe a livello di fluidità e reattività.

Supponendo che l’istanza di AVPlayerItem da suonare sia denominata anItem, potremo così procedere:

[anItem addObserver:self
         forKeyPath:@"timedMetadata"
            options:NSKeyValueObservingOptionNew
            context:NULL];

In questo modo, la classe CPStreamPlayer (self), sarà notificata relativamente a nuovi cambiamenti (NSKeyValueObservingOptionNew) che intervengono per le proprietà elencate sopra. I più attenti di voi avranno notato che il metodo utilizzato non è quello precedentemente illustrato ma una sua variante denominata addObserver:forKeyPath:options:context: che prende come parametro un keypath
e non una chiave.

Cosa è un Keypath?

Semplicemente è un percorso di chiavi, separate da punti, che esprimono un percorso sino ad arrivare ad una particolare proprietà. Visto che le proprietà che stiamo cercando sono direttamente esposte dall’istanza dell’oggetto AVPlayerItem, nel nostro caso il keypath corrisponderà con la key.

A questo punto l’istanza di CPStreamPlayer sarà legate all’istanza di AVPlayerItem denominata anItem. Questo significa che qualunque cambiamento delle proprietà sopra elencate, scatenerà un messaggio observeValueForKeyPath:ofObject:change:context: sull’oggetto observer CPStreamPlayer.

Ci resta dunque da implementare questo metodo e la relativa logica per reagire ai singoli eventi:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if([keyPath rangeOfString:@"playbackBufferEmpty"].location != NSNotFound && self.isPlaying)
    {
        [player pause];
    }
    if([keyPath rangeOfString:@"playbackLikelyToKeepUp"].location != NSNotFound)
    {
        [player play];
    }
    else if([keyPath rangeOfString:@"timedMetadata"].location != NSNotFound)
    {
        if([[change objectForKey:@"new"] isKindOfClass:[NSArray class]])
        {
            //update songTitle and Artist
        }
    }
}

Nel caso di playbackBufferEmpty, metteremo in pausa l’istanza del player e proveremo a lanciare un messaggio di CPStreamPlayerDidPaused al delegato designato. Nel caso di playbackLikelyToKeeup, ossia quando il buffer è quasi pieno, ripristineremo l’ascolto.
Infine, nel caso di una variazione alla proprietà timedMetadata, ossia quando variano i metadati associati allo stream, reagiremo popolando correttamente i corrispondenti campi del brano: titolo ed artista.

UIViewController e gestione del player

Ci resta da gestire il controllore di vista, responsabile del player. Anche in questo caso, si tratterà di un’operazione davvero semplice. La classe CPStreamPlayer, implementa un protocollo informale:

@protocol CPStreamPlayerDelegate
@optional
- (void)CPStreamPlayerDidStarted:(CPStreamPlayer *)actPlayer;
- (void)CPStreamPlayerDidPaused:(CPStreamPlayer *)actPlayer;
- (void)CPStreamPlayerMetadataDidUpdated:(CPStreamPlayer *)actPlayer;
@end

La nostra istanza di UIViewController dovrà implementarlo se desidera ricevere notifiche relativamente a start e stop dello stream e cambiamento dei metadati. Nel nostro esempio base, ci basterà aggiornare il titolo del pulsante di play ed i campi che indicano il brano in riproduzione:

#pragma mark -
#pragma mark CPStreamPlayerDelegate

- (void)CPStreamPlayerDidStarted:(CPStreamPlayer *)actPlayer {
    [aButton setTitle:@"Buffering" forState:UIControlStateNormal];
}
- (void)CPStreamPlayerDidPaused:(CPStreamPlayer *)actPlayer {
    [aButton setTitle:@"Play" forState:UIControlStateNormal];
}
- (void)CPStreamPlayerMetadataDidUpdated:(CPStreamPlayer *)actPlayer {
    [aButton setTitle:@"Stop" forState:UIControlStateNormal];
    songTitle.text = actPlayer.songTitle;
    songArtist.text = actPlayer.artistTitle;
    channel.text = actPlayer.channelTitle;
}

Un ultimo tocco: il controllo remoto

iOS ci permette di sfruttare i controlli multimediali disponibili sulla cuffia o quelli che compaiono quando il telefono è bloccato, premendo rapidamente due volte il tasto home.
Per fare questo è necessario impostare il nostro UIViewController come primo risponditore implementando il seguente metodo:

/* required for remote control */
- (BOOL)canBecomeFirstResponder {
return YES;
}

A questo punto, la nostra classe UIViewController è in grado di rispondere agli eventi inviati dal controllore multimediale remoto. Il metodo da implementare per reagire correttamente a questi eventi è remoteControlReceivedWithEvent:. Ci basterà controllare l’oggetto UIEvent passato come parametro e reagire di conseguenza:

#pragma mark -
#pragma mark Remote Control Event Handling

- (void)remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
    if (receivedEvent.type == UIEventTypeRemoteControl) {
        NSLog(@"subtype: %d", receivedEvent.subtype);
        switch (receivedEvent.subtype) {
            case UIEventSubtypeRemoteControlPlay:
                [streamPlayer startPlay];
                break;
            case UIEventSubtypeRemoteControlPause:
                [streamPlayer startPlay];
                break;
            case UIEventSubtypeRemoteControlStop:
                [streamPlayer startPlay];
                break;
            case UIEventSubtypeRemoteControlTogglePlayPause:
                [streamPlayer startPlay];
                break;
            default:
            break;
        }
    }
}

Github: Dove trovate l’esempio

L’esempio completo, citato in quest’articolo è disponibile su github all’indirizzo: https://github.com/valvoline/CPStreamPlayer

Vi è piaciuto l’articolo? Ringraziate l’autore Costantino Pistagna scaricando la sua applicazione “Strips” (GRATIS in App Store) e VOTATELA come migliore applicazione italiana.


Strips-iPhone

Share this story:
  • tweet

Tags: AVPlayerDesign Pattern iOSKey-Value CodingKey-Value Observingkvckvo iosModel View ControllerNSNotificationCenterTutorial Pratici

Recent Posts

  • Parte il percorso programmatori iOS in Swift su devACADEMY.it

    20 Dicembre 2017 - 0 Comment
  • Android, crittografare dati velocemente con Encryption

    24 Settembre 2018 - 0 Comment
  • Sql2o, accesso immediato ai database tramite Java

    3 Settembre 2018 - 0 Comment
  • Okio, libreria per ottimizzare l’input/output in Java

    27 Agosto 2018 - 0 Comment

Related Posts

  • Manuali e testi consigliati per sviluppatori iOS iPhone e iPad e Mac OS X

    15 Febbraio 2012 - 3 Comments
  • L#020 – Far comunicare oggetti diversi in Objective-C: quale metodo scegliere?

    3 Novembre 2011 - 5 Comments
  • 6. Creiamo una semplice calcolatrice in XCode e Objective-C (parte 2)

    27 Settembre 2011 - 2 Comments

Author Description

8 Responses to “T#109 – Key-Value Observing: Costruiamo un semplice lettore multimediale”

  1. 22 Maggio 2012

    Andrea

    Bel progetto, e se io avessi un flusso audio proveniente da shoutcast o icecast? Riuscirei ad utilizzarlo in questo progetto?

  2. 22 Maggio 2012

    valvoline

    stessa cosa. icecast/shoutcat/whatever.

  3. 23 Maggio 2012

    Andrea

    Buongiorno
    ho seguito alcuni tuoi tutorial su youtube e sono parecchio interessanti e aiutano a capire meglio.
    Ti volevo chiedere un favore mi sono fermato sui codici di questa calcolatrice e volevo chiedere se mi potevi dare una mano
    Grazie
    http://lnx.fantasylands.net/aiuto-dislessia/wp-content/uploads/2010/08/SHARP-EL-W531G.pdf
    Questo è quello che volevo fare.

  4. 23 Maggio 2012

    Luca

    bellissimo articolo!!
    ma quali sono le differenze tra kvo e il sistema dei delegati?! non conosco l’oggetto AVPlayer ma, in generale, quasi tutti gli oggetti inclusi in ios hanno il supporto per i delegate, per cui si potrebbe utilizzare quella tecnica al posto di questa kvo. sbaglio!?

  5. 24 Maggio 2012

    andreaberns


    valvoline:

    stessa cosa. icecast/shoutcat/whatever.

    quindi se nn ho capito male questo NSURL *streamUrl = [NSURL URLWithString:streamAddress];
    diventerà [NSURL http:000.000.000.000:0000]; o sbaglio
    grazie

  6. 3 Giugno 2012

    Carlo

    funziona questo tutoria su xcode 4.3?

  7. 6 Giugno 2012

    Francesco Burelli


    Luca:

    bellissimo articolo!!
    ma quali sono le differenze tra kvo e il sistema dei delegati?! non conosco l’oggetto AVPlayer ma, in generale, quasi tutti gli oggetti inclusi in ios hanno il supporto per i delegate, per cui si potrebbe utilizzare quella tecnica al posto di questa kvo. sbaglio!?

    Quello che dici è vero, la differenza tra il delegato e il KVO è leggera ma sostanziale, soprattutto se parliamo di classi che implementi tu.
    Metti che costruisci una classe che col tempo cambia e vuoi che la vista lo sappia e si aggiorni, col delegato dovresti creare una variabile che punti al delegato e un protocollo che questa deve implementare, mentre col KVO puoi avere N delegati che si aggangiano al cambiamento di una o più proprietà del tuo oggetto GRATIS, senza scrivere una linea di codice!

  8. 17 Maggio 2013

    roma-traslochi

    Questo è il blog giusto per tutti coloro che vogliono capire qualcosa su questo argomento. Trovo quasi difficile discutere con te (cosa che io in realtà vorrei… haha). Avete sicuramente dato nuova vita a un tema di cui si è parlato per anni. Grandi cose, semplicemente fantastico!

Leave a Reply

Your email address will not be published. Required fields are marked *


*
*

Corso online di programmazione android e java

SEZIONI

  • Android
  • Comunicazioni
  • Contest
  • Corsi ed Eventi
  • Corso completo di C
  • Corso programmazione videogiochi
  • Framework
  • Grafica e Design
  • Guida rapida alla programmazione Cocoa Touch
  • Guide Teoriche
  • Guide varie
  • iPad
  • Le nostre applicazioni
  • Libri e manuali
  • Materiale OpenSource
  • News
  • Pillole di C++
  • Progetti completi
  • Risorse utili
  • Strumenti di Sviluppo
  • Swift
  • Tips & Tricks
  • Tutorial Pratici
  • Video Tutorial
  • Windows Phone

Siti Amici

  • Adrirobot
  • Allmobileworld
  • Apple Notizie
  • Apple Tribù
  • Avvocato360
  • Blog informatico 360°
  • bubi devs
  • fotogriPhone
  • GiovaTech
  • iApp-Mac
  • iOS Developer Program
  • iPodMania
  • MelaRumors
  • Meritocracy
  • SoloTablet
  • TecnoUser
  • Privacy & Cookie Policy
©2009-2018 devAPP - All Rights Reserved | Contattaci
devAPP.it è un progetto di DEVAPP S.R.L. - Web & Mobile Agency di Torino
Str. Volpiano, 54 - 10040 Leini (TO) - C.F. e P.IVA 11263180017 - REA TO1199665 - Cap. Soc. € 10.000,00 i.v.

devACADEMY.it

Vuoi imparare a programmare?

Iscriviti e accedi a TUTTI i corsi con un’unica iscrizione.
Oltre 70 corsi e migliaia di videolezioni online e in italiano a tua disposizione.

ISCRIVITI SUBITO